// Signing in: a public OAuth client with DPoP and PKCE, its keys and tokens in // the library's own IndexedDB store. import { AtprotoDohHandleResolver, BrowserOAuthClient, buildAtprotoLoopbackClientMetadata, isLoopbackHost, type HandleResolver, type OAuthSession, } from "@atproto/oauth-client-browser"; import { isValidDid } from "@atproto/syntax"; import { clientMetadata, SCOPE } from "./oauth-config.ts"; export type { OAuthSession }; // Who sees what at sign-in: // - A handle is resolved over DNS-over-HTTPS at Cloudflare (1.1.1.1), which // sees the name `_atproto.` and the browser's address. The handle's // own domain is asked for /.well-known/atproto-did at the same time. // - A DID skips both. The OAuth library would otherwise look up the handle in // the DID document to verify it, so it is given a resolver that never // answers; the page shows the handle the operator's own PDS reports instead. // - Every sign-in then resolves the DID document: at plc.directory for // did:plc, or at the DID's own domain for did:web. const DOH_ENDPOINT = "https://cloudflare-dns.com/dns-query"; const handles = new AtprotoDohHandleResolver({ dohEndpoint: DOH_ENDPOINT }); const unverifiedHandles: HandleResolver = { resolve: async () => null }; // Served from a loopback address, the page signs in as the atproto loopback // client: the same scope, redirected back to wherever the page is. // See https://atproto.com/specs/oauth#localhost-client-development. function metadataForThisOrigin() { if (!isLoopbackHost(location.hostname)) return clientMetadata; const port = location.port ? `:${location.port}` : ""; return buildAtprotoLoopbackClientMetadata({ scope: SCOPE, redirect_uris: [`http://127.0.0.1${port}/`], }); } export const client = new BrowserOAuthClient({ clientMetadata: metadataForThisOrigin(), handleResolver: unverifiedHandles, }); /** * Restores a stored session, or completes a sign-in the URL is returning * from. `state` is what that sign-in was started with, if it was this one. */ export async function resume(): Promise<{ session?: OAuthSession; state?: string | null }> { return (await client.init()) ?? {}; } /** * Leaves the page for the operator's authorization server. The library keeps * `state` in its own store, keyed by the request, and hands it back to * resume() when the operator returns: here, the path to go on from. */ export async function signIn(identity: string, state?: string): Promise { const input = identity.trim().replace(/^@/, ""); const did = isValidDid(input) ? input : await handles.resolve(input.toLowerCase()); if (!did) throw new Error(`${input} does not resolve to a DID`); await client.signIn(did, { scope: SCOPE, state }); } /** Revokes the session's tokens at its authorization server and forgets it. */ export async function signOut(session: OAuthSession): Promise { await session.signOut(); } /** The PDS the session's tokens are for. */ export async function pdsUrl(session: OAuthSession): Promise { return (await session.getTokenInfo()).aud; }