import type { ActorIdentifier, Did } from "@atcute/lexicons/syntax"; import { OAuthUserAgent, createAuthorizationUrl, deleteStoredSession, finalizeAuthorization, getSession, type Session } from "@atcute/oauth-browser-client"; import { markOrgDid, orgControllerFor } from "$lib/auth/accounts"; import { mintServiceAuth, serviceDidForHost } from "$lib/auth/agent"; import { isDeadSessionError } from "$lib/auth/session"; interface ConsentScope { scope: string; restricted?: boolean; } interface ConsentPermissionSet { include_scope: string; restricted?: boolean; } interface ConsentData { scopes: ConsentScope[]; permission_sets?: ConsentPermissionSet[]; } interface ApprovalData { redirect_uri?: string; error?: string; error_description?: string; } type AccessTokenClaims = { act?: { sub?: unknown }; }; export const delegatedSessionController = (session: Pick): Did | null => { try { const encodedPayload = session.token.access.split(".")[1]; if (!encodedPayload) return null; const base64 = encodedPayload.replace(/-/g, "+").replace(/_/g, "/"); const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, "="); const claims = JSON.parse(atob(padded)) as AccessTokenClaims; const controller = claims.act?.sub; return typeof controller === "string" && controller.startsWith("did:") ? (controller as Did) : null; } catch { return null; } }; const approvedScopesOf = (consent: ConsentData): string[] => { const scopes = new Set([ ...consent.scopes.filter((s) => !s.restricted).map((s) => s.scope), ...(consent.permission_sets ?? []).filter((s) => !s.restricted).map((s) => s.include_scope) ]); if (consent.scopes.some((s) => s.scope === "atproto")) { scopes.add("atproto"); } return scopes.size > 0 ? [...scopes] : ["atproto"]; }; const createAuthorizationRequest = async ( controllerAgent: OAuthUserAgent, delegatedDid: Did ): Promise => { const authorizationUrl = await createAuthorizationUrl({ target: { type: "account", identifier: delegatedDid as ActorIdentifier }, scope: controllerAgent.session.token.scope }); const requestUri = authorizationUrl.searchParams.get("request_uri"); if (!requestUri) throw new Error("tranquil did not return a request_uri for the org account."); return authorizationUrl; }; const bindAuthorizationRequest = async ( controllerAgent: OAuthUserAgent, delegatedDid: Did, requestUri: string, pds: URL ): Promise => { const token = await mintServiceAuth(controllerAgent, { aud: serviceDidForHost(pds.host), lxm: "farm.tranquil.delegation.authorize" }); const response = await fetch(new URL("/oauth/delegation/auth-token", pds), { method: "POST", headers: { "content-type": "application/json", authorization: `Bearer ${token}` }, body: JSON.stringify({ request_uri: requestUri, delegated_did: delegatedDid }) }); const fallback = "Could not bind the org account's authorization request."; // error bodies may not be json (proxy pages, timeouts), so parse leniently const data: { success?: boolean; error?: string } = await response.json().catch(() => ({})); if (!response.ok || !data.success) throw new Error(data.error || fallback); }; const consentEndpoint = (pds: URL): URL => new URL("/oauth/authorize/consent", pds); const loadConsent = async (pds: URL, requestUri: string): Promise => { const url = consentEndpoint(pds); url.searchParams.set("request_uri", requestUri); const response = await fetch(url); if (!response.ok) throw new Error("Could not load the org account's authorization request."); return response.json(); }; const approveConsent = async ( pds: URL, requestUri: string, consent: ConsentData ): Promise => { const response = await fetch(consentEndpoint(pds), { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ request_uri: requestUri, approved_scopes: approvedScopesOf(consent), remember: false }) }); const data: ApprovalData = await response.json(); if (!data.redirect_uri) { throw new Error( data.error_description || data.error || "Authorizing the org account failed." ); } return data.redirect_uri; }; const callbackParams = (intermediateRedirect: string, issuer: string): URLSearchParams => { const intermediate = new URL(intermediateRedirect); const code = intermediate.searchParams.get("code"); if (!code) throw new Error("tranquil did not return an authorization code."); const params = new URLSearchParams({ iss: new URL(issuer).origin, code }); const state = intermediate.searchParams.get("state"); if (state) params.set("state", state); return params; }; export const createDelegatedSession = async ( controllerAgent: OAuthUserAgent, delegatedDid: Did ): Promise => { const authorizationUrl = await createAuthorizationRequest(controllerAgent, delegatedDid); const requestUri = authorizationUrl.searchParams.get("request_uri")!; await bindAuthorizationRequest(controllerAgent, delegatedDid, requestUri, authorizationUrl); const consent = await loadConsent(authorizationUrl, requestUri); const redirect = await approveConsent(authorizationUrl, requestUri, consent); const params = callbackParams(redirect, authorizationUrl.origin); const { session } = await finalizeAuthorization(params); if ( session.info.sub !== delegatedDid || delegatedSessionController(session) !== controllerAgent.sub ) { deleteStoredSession(session.info.sub); throw new Error("The authorized organization or controller did not match the request."); } return new OAuthUserAgent(session); }; const storedDelegatedSession = async ( controllerDid: Did, delegatedDid: Did ): Promise => { const recordedController = orgControllerFor(delegatedDid); if (recordedController && recordedController !== controllerDid) { deleteStoredSession(delegatedDid); return null; } try { const session = await getSession(delegatedDid); if (delegatedSessionController(session) !== controllerDid) { deleteStoredSession(delegatedDid); return null; } return session; } catch (cause) { if (!isDeadSessionError(cause)) throw cause; return null; } }; // prefer this over createDelegatedSession directly export const getOrCreateDelegatedSession = async ( controllerAgent: OAuthUserAgent, delegatedDid: Did ): Promise => { const controllerDid = controllerAgent.sub as Did; if (delegatedDid === controllerDid) return controllerAgent; const stored = await storedDelegatedSession(controllerDid, delegatedDid); const agent = stored ? new OAuthUserAgent(stored) : await createDelegatedSession(controllerAgent, delegatedDid); markOrgDid(delegatedDid, controllerDid); return agent; };