Something went wrong. Try again.
Monorepo for Tangled tangled.org
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201import 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<Session, "token">): 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<URL> => { 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<void> => { 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<ConsentData> => { 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<string> => { 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<OAuthUserAgent> => { 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<Session | null> => { 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 directlyexport const getOrCreateDelegatedSession = async ( controllerAgent: OAuthUserAgent, delegatedDid: Did): Promise<OAuthUserAgent> => { 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;};