This repository has no description
1import type { OAuthUserAgent } from "@atcute/oauth-browser-client";
2import { mintServiceAuth, serviceDidForHost } from "$lib/auth/agent";
3import { buildUrl, toResponseError } from "./_request";
4import type { QueryValue, XrpcRequestInit } from "./client";
5
6// Authenticated client for the go appview's org.tangled.temp.* xrpc methods.
7// Unlike bobbin (public reads) and the pds (record writes), these are authed
8// with an atproto service-auth jwt: the browser mints a per-call token via the
9// user's pds (getServiceAuth), scoped to the method (lxm) and the appview's did
10// (aud). The appview verifies the issuer's signature and the aud/lxm claims.
11export interface AppviewContext {
12 readonly serviceUrl: string;
13 readonly aud: string;
14 readonly agent: OAuthUserAgent;
15 readonly fetch: typeof globalThis.fetch;
16}
17
18export interface CreateAppviewOptions {
19 apiUrl: string;
20 agent: OAuthUserAgent;
21 fetch?: typeof globalThis.fetch;
22}
23
24export const createAppviewClient = ({
25 apiUrl,
26 agent,
27 fetch
28}: CreateAppviewOptions): AppviewContext => {
29 const serviceUrl = apiUrl.replace(/\/+$/, "");
30 // aud host must match the appview's TANGLED_APPVIEW_HOST, which is the host
31 // it's reached at; derive both from the same url so they can't drift.
32 const aud = serviceDidForHost(new URL(serviceUrl).host);
33 return { serviceUrl, aud, agent, fetch: fetch ?? globalThis.fetch };
34};
35
36const authHeader = async (ctx: AppviewContext, nsid: string): Promise<string> => {
37 const token = await mintServiceAuth(ctx.agent, { aud: ctx.aud, lxm: nsid });
38 return `Bearer ${token}`;
39};
40
41export const authedGet = async <T>(
42 ctx: AppviewContext,
43 nsid: string,
44 params?: Record<string, QueryValue>,
45 init?: XrpcRequestInit
46): Promise<T> => {
47 const response = await ctx.fetch(buildUrl(ctx.serviceUrl, nsid, params), {
48 headers: {
49 accept: "application/json",
50 authorization: await authHeader(ctx, nsid),
51 ...init?.headers
52 },
53 signal: init?.signal
54 });
55 if (!response.ok) throw await toResponseError(response);
56 return (await response.json()) as T;
57};
58
59export const authedPost = async <T>(
60 ctx: AppviewContext,
61 nsid: string,
62 body: unknown,
63 init?: XrpcRequestInit
64): Promise<T | null> => {
65 const response = await ctx.fetch(buildUrl(ctx.serviceUrl, nsid), {
66 method: "POST",
67 headers: {
68 "content-type": "application/json",
69 accept: "application/json",
70 authorization: await authHeader(ctx, nsid),
71 ...init?.headers
72 },
73 body: JSON.stringify(body ?? {}),
74 signal: init?.signal
75 });
76 if (!response.ok) throw await toResponseError(response);
77 // most temp procedures return no body (200 with empty payload)
78 const text = await response.text();
79 return text ? (JSON.parse(text) as T) : null;
80};