import { Client, simpleFetchHandler } from "@atcute/client"; import type { OAuthUserAgent } from "@atcute/oauth-browser-client"; import { knotProcedure } from "$lib/api/cob"; import { nsidFromUrl, withServiceAuth } from "$lib/api/client"; import { knotCapabilities } from "$lib/api/knot"; import { knotFor } from "$lib/api/repo"; import { capabilityOf, didOf, nsidOf, type Did, type KnotOrigin, type Nsid } from "$lib/api/syntax"; import { serviceDidForHost } from "$lib/auth/agent"; export const REPO_TOKEN_MINT_CAPABILITY = capabilityOf("knot-repo-token-mint"); const MINT_TOKEN_NSID = nsidOf("org.tangled.repo.mintToken"); const REPO_TOKEN_LIFETIME_SECS = 55; export interface SpindleTarget { knot?: KnotOrigin; repoDid: Did; } export const spindleTargetOf = (repo: { repoDid?: string; knot?: string }): SpindleTarget | null => repo.repoDid ? repo.knot ? { knot: knotFor(repo.knot), repoDid: didOf(repo.repoDid) } : { repoDid: didOf(repo.repoDid) } : null; const mintRepoToken = async ( agent: OAuthUserAgent, knot: KnotOrigin, repoDid: Did, spindle: URL, lxm: Nsid ): Promise => { const { token } = await knotProcedure<{ token: string }>( agent, knot, repoDid, MINT_TOKEN_NSID, { aud: serviceDidForHost(spindle.host), lxm, exp: Math.floor(Date.now() / 1000) + REPO_TOKEN_LIFETIME_SECS }, { signal: AbortSignal.timeout(10_000), invalidate: false } ); return token; }; const withRepoToken = ( base: typeof globalThis.fetch, agent: OAuthUserAgent, knot: KnotOrigin, repoDid: Did, spindle: URL, userToken: typeof globalThis.fetch ): typeof globalThis.fetch => { return async (input, init) => { const lxm = nsidFromUrl(input); if (!lxm) return userToken(input, init); try { const token = await mintRepoToken(agent, knot, repoDid, spindle, lxm); const headers = new Headers(init?.headers); headers.set("authorization", `Bearer ${token}`); return base(input, { ...init, headers }); } catch { return userToken(input, init); } }; }; export const spindleXrpc = async ( agent: OAuthUserAgent, spindle: URL, target: SpindleTarget ): Promise => { const origin = spindle.toString(); const userToken = withServiceAuth(globalThis.fetch, agent, origin); const authenticated = target.knot && (await knotCapabilities(target.knot))?.includes(REPO_TOKEN_MINT_CAPABILITY) ? withRepoToken( globalThis.fetch, agent, target.knot, target.repoDid, spindle, userToken ) : userToken; return new Client({ handler: simpleFetchHandler({ service: origin, fetch: authenticated }) }); };