import { useQuery } from '@tanstack/react-query'; import { useState } from 'react'; import { Button } from '#/components/atoms/button.jsx'; import { Notice } from '#/components/atoms/notice.jsx'; import { TextField } from '#/components/molecules/text-field.jsx'; import { documentHandle, resolveDid } from '#/lib/identity.js'; import { beginSignIn, clientId } from '#/lib/oauth.js'; import { SCOPES } from '#/lib/scopes.js'; /** * The account whose site is serving this page, or null where the page is served * from anywhere else. The field is not filled from it: a reader signs in as * whoever they are, which on a shared deployment is rarely the host. It is kept * for the fallback below, where a handle that resolves nowhere is the host's * own and its DID does resolve. */ function useHostAccount() { return useQuery({ queryKey: ['host-account'], queryFn: async () => { const response = await fetch('/.well-known/atproto-did'); if (!response.ok) return null; const did = (await response.text()).trim(); if (!did.startsWith('did:')) return null; return { did, handle: documentHandle(await resolveDid(did)) }; }, retry: false, staleTime: Number.POSITIVE_INFINITY, }); } /** * Whether this copy of the app is registered for the origin it is served from. * * An OAuth client is named by the URL its registration is served from, so a * copy deployed for one origin cannot sign in from another. The check is * skipped on loopback, where the client_id carries its own registration. */ function useRegistration() { const expected = clientId(SCOPES.read); return useQuery({ queryKey: ['registration', expected], queryFn: async () => { if (!expected.startsWith('https://')) return { ok: true }; const response = await fetch('/client-metadata.json'); if (!response.ok) return { ok: false, found: null }; const found = (await response.json()).client_id; return { ok: found === expected, found }; }, retry: false, staleTime: Number.POSITIVE_INFINITY, }); } export function SignInPage() { const { data: host } = useHostAccount(); const { data: registration } = useRegistration(); const [identifier, setIdentifier] = useState(''); const [access, setAccess] = useState('read'); const [error, setError] = useState(null); const [busy, setBusy] = useState(false); async function submit(event) { event.preventDefault(); setError(null); setBusy(true); try { await beginSignIn(identifier, { scope: SCOPES[access] }); } catch (err) { // The origin publishes its own account's DID, which resolves wherever the // handle beside it does not. A handle on a domain with no public DNS is // the case: a local stack, or an account mid-handle-change. if (host && identifier.trim().toLowerCase() === host.handle) { try { await beginSignIn(host.did, { scope: SCOPES[access] }); return; } catch { // The DID failed too, so the first message is the useful one. } } setError(err.message); setBusy(false); } } return (

Spaces

Sign in to browse the permissioned spaces your account holds: the records in each one, who writes to them, and what the authority says about them.

setIdentifier(event.target.value)} />
Access {[ { key: 'read', title: 'Read only', detail: 'List and open records. Nothing on the server changes.', }, { key: 'write', title: 'Read and write', detail: 'Also create, edit and delete records, and manage spaces and their members.', }, ].map((option) => ( ))}
{registration && !registration.ok && ( This copy is registered for{' '} {registration.found ? new URL(registration.found).origin : 'nowhere'} , not for {window.location.origin}. Deploy it again with SPACES_ORIGIN set to this origin before signing in. )} {error && {error}}

Your PDS asks you to approve this app before it sees anything. The access token stays in this browser and is bound to a key this page cannot read, so a copy of it is no use anywhere else.

); }