This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / web / src / lib / api / appview.ts
2.7 kB 80 lines
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};