import type { OAuthUserAgent } from "@atcute/oauth-browser-client"; import { mintServiceAuth, serviceDidForHost } from "$lib/auth/agent"; import { buildUrl, toResponseError } from "./_request"; import type { QueryValue, XrpcRequestInit } from "./client"; // Authenticated client for the go appview's org.tangled.temp.* xrpc methods. // Unlike bobbin (public reads) and the pds (record writes), these are authed // with an atproto service-auth jwt: the browser mints a per-call token via the // user's pds (getServiceAuth), scoped to the method (lxm) and the appview's did // (aud). The appview verifies the issuer's signature and the aud/lxm claims. export interface AppviewContext { readonly serviceUrl: string; readonly aud: string; readonly agent: OAuthUserAgent; readonly fetch: typeof globalThis.fetch; } export interface CreateAppviewOptions { apiUrl: string; agent: OAuthUserAgent; fetch?: typeof globalThis.fetch; } export const createAppviewClient = ({ apiUrl, agent, fetch }: CreateAppviewOptions): AppviewContext => { const serviceUrl = apiUrl.replace(/\/+$/, ""); // aud host must match the appview's TANGLED_APPVIEW_HOST, which is the host // it's reached at; derive both from the same url so they can't drift. const aud = serviceDidForHost(new URL(serviceUrl).host); return { serviceUrl, aud, agent, fetch: fetch ?? globalThis.fetch }; }; const authHeader = async (ctx: AppviewContext, nsid: string): Promise => { const token = await mintServiceAuth(ctx.agent, { aud: ctx.aud, lxm: nsid }); return `Bearer ${token}`; }; export const authedGet = async ( ctx: AppviewContext, nsid: string, params?: Record, init?: XrpcRequestInit ): Promise => { const response = await ctx.fetch(buildUrl(ctx.serviceUrl, nsid, params), { headers: { accept: "application/json", authorization: await authHeader(ctx, nsid), ...init?.headers }, signal: init?.signal }); if (!response.ok) throw await toResponseError(response); return (await response.json()) as T; }; export const authedPost = async ( ctx: AppviewContext, nsid: string, body: unknown, init?: XrpcRequestInit ): Promise => { const response = await ctx.fetch(buildUrl(ctx.serviceUrl, nsid), { method: "POST", headers: { "content-type": "application/json", accept: "application/json", authorization: await authHeader(ctx, nsid), ...init?.headers }, body: JSON.stringify(body ?? {}), signal: init?.signal }); if (!response.ok) throw await toResponseError(response); // most temp procedures return no body (200 with empty payload) const text = await response.text(); return text ? (JSON.parse(text) as T) : null; };