import { Client, simpleFetchHandler } from "@atcute/client";
import type { OAuthUserAgent } from "@atcute/oauth-browser-client";
import { knotProcedure } from "#lib/api/cob.ts";
import { nsidFromUrl, withServiceAuth } from "#lib/api/client.ts";
import { knotCapabilities } from "#lib/api/knot.ts";
import { knotFor } from "#lib/api/repo.ts";
import {
capabilityOf,
didOf,
nsidOf,
type Did,
type KnotOrigin,
type Nsid
} from "#lib/api/syntax.ts";
import { serviceDidForHost } from "#lib/auth/agent.ts";
import type { Fetch } from "#lib/fetch.ts";
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<string> => {
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: Fetch,
agent: OAuthUserAgent,
knot: KnotOrigin,
repoDid: Did,
spindle: URL,
userToken: Fetch
): 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<Client> => {
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 as typeof fetch })
});
};