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.ts";
import { mintServiceAuth, serviceDidForHost } from "#lib/auth/agent.ts";
import { isDeadSessionError } from "#lib/auth/session.ts";
import { nsidOf } from "#lib/api/syntax.ts";
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: nsidOf("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 directly
export 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;
};
export const agentActingAs = async (
agent: OAuthUserAgent | null,
actAs: Did | null | undefined
): Promise<OAuthUserAgent | null> =>
agent && actAs ? getOrCreateDelegatedSession(agent, actAs) : agent;