import { Client, ok } from "@atcute/client";
import { mainSchema as getServiceAuthSchema } from "@atcute/atproto/types/server/getServiceAuth";
import { isDid, type AtprotoAudience } from "@atcute/lexicons/syntax";
import { didOf, type Did, type Nsid } from "#lib/api/syntax.ts";
import type { OAuthUserAgent } from "@atcute/oauth-browser-client";
export const createClient = (agent: OAuthUserAgent, proxy?: AtprotoAudience): Client =>
new Client({ handler: agent, proxy: proxy ?? null });
export interface ServiceAuthOptions {
// service did for the jwt aud claim
aud: Did;
lxm: Nsid;
// clamped to at least 60s, matching appview's service client.
expiresInSeconds?: number;
signal?: AbortSignal;
}
// pds refuses did:web:https%3A//host as an invalid did
const hostOf = (hostOrUrl: string): string =>
/^[a-z][a-z0-9+.-]*:\/\//i.test(hostOrUrl)
? new URL(hostOrUrl).host
: hostOrUrl.replace(/\/+$/, "");
// did:web service id, with ports percent-encoded like serviceauth didweb.
export const serviceDidForHost = (hostOrUrl: string): Did =>
didOf(`did:web:${hostOf(hostOrUrl).replace(/:/g, "%3A")}`);
export const hostForServiceDid = (did: string): string | null => {
if (!isDid(did) || !did.startsWith("did:web:")) return null;
const encoded = did.slice("did:web:".length);
if (/%(?![0-9A-Fa-f]{2})/.test(encoded)) return null;
const host = encoded.replace(/%3A/gi, ":").replace(/\.+$/, "").toLowerCase();
return host || null;
};
// mint a service-auth jwt for knot/spindle xrpc calls.
export const mintServiceAuth = async (
agent: OAuthUserAgent,
{ aud, lxm, expiresInSeconds = 60, signal }: ServiceAuthOptions
): Promise<string> => {
const client = createClient(agent);
const exp = Math.floor(Date.now() / 1000) + Math.max(expiresInSeconds, 60);
const { token } = await ok(
client.call(getServiceAuthSchema, {
params: { aud, exp, lxm },
signal
})
);
return token;
};