Something went wrong. Try again.
Monorepo for Tangled tangled.org
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114import { Client, simpleFetchHandler } from "@atcute/client";import type { OAuthUserAgent } from "@atcute/oauth-browser-client";import { isNsid } from "@atcute/lexicons/syntax";import { mintServiceAuth, serviceDidForHost } from "#lib/auth/agent.ts";import { didOf, nsidOf, type Nsid } from "#lib/api/syntax.ts";import { normalizeHandle } from "#lib/handle.ts";
export { ClientResponseError, isXRPCErrorPayload, ok } from "@atcute/client";export type { XRPCErrorPayload } from "@atcute/client";
// generated lexicons register ambient types; don't import the barrel at runtime.
export interface BobbinContext { readonly xrpc: Client; readonly serviceUrl: string; readonly fetch: typeof globalThis.fetch;}
export interface CreateBobbinOptions { serviceUrl: string; fetch?: typeof globalThis.fetch; /** when set, every xrpc request carries a freshly minted service-auth jwt */ agent?: OAuthUserAgent; /** off for bodies with no handles that must keep streaming, see withNormalizedHandles */ normalizeHandles?: boolean;}
export const nsidFromUrl = (input: RequestInfo | URL): Nsid | null => { const href = input instanceof Request ? input.url : String(input); let pathname: string; try { pathname = new URL(href).pathname; } catch { return null; } const [, prefix, nsid, ...rest] = pathname.split("/"); return prefix === "xrpc" && nsid && rest.length === 0 && isNsid(nsid) ? nsidOf(nsid) : null;};
// pocket keys stored preferences by the token's `aud`export const PREFERENCES_AUD = didOf("did:web:tangled.org");export const PREFERENCES_LXMS: readonly Nsid[] = [ nsidOf("com.bad-example.pocket.getPreferences"), nsidOf("com.bad-example.pocket.putPreferences")];
export const withServiceAuth = ( base: typeof globalThis.fetch, agent: OAuthUserAgent, origin: string): typeof globalThis.fetch => { return async (input, init) => { const lxm = nsidFromUrl(input); if (!lxm) return base(input, init);
const aud = PREFERENCES_LXMS.includes(lxm) ? PREFERENCES_AUD : serviceDidForHost(new URL(origin).host); const token = await mintServiceAuth(agent, { aud, lxm }); const headers = new Headers(init?.headers); headers.set("authorization", `Bearer ${token}`); return base(input, { ...init, headers }); };};
// the handles the ui shows come out of bobbin as a `handle` field somewhere in a json// body, so normalizing them here covers the views at once. it buffers the body, so// non-json (patches, archives, blobs) passes through, and git reads opt outexport const withNormalizedHandles = (base: typeof globalThis.fetch): typeof globalThis.fetch => { return async (input, init) => { const response = await base(input, init); if (!response.headers.get("content-type")?.includes("application/json")) return response; const text = await response.text(); if (!text.includes('"handle"')) return new Response(text, response); const body = JSON.stringify( JSON.parse(text, (key, value: unknown) => key === "handle" && typeof value === "string" ? normalizeHandle(value) : value ) ); // the body is decoded and rewritten, so the wire's encoding and length no longer apply const headers = new Headers(response.headers); headers.delete("content-encoding"); headers.delete("content-length"); return new Response(body, { status: response.status, statusText: response.statusText, headers }); };};
export const createBobbinClient = ({ serviceUrl, fetch, agent, normalizeHandles = true}: CreateBobbinOptions): BobbinContext => { const origin = serviceUrl.replace(/\/+$/, ""); const rawFetch = fetch ?? globalThis.fetch; const baseFetch = normalizeHandles ? withNormalizedHandles(rawFetch) : rawFetch; const boundFetch = agent ? withServiceAuth(baseFetch, agent, origin) : baseFetch; const xrpc = new Client({ handler: simpleFetchHandler({ service: origin, fetch: boundFetch }) }); return { xrpc, serviceUrl: origin, fetch: boundFetch };};
export type QueryValue = string | number | boolean | readonly (string | number)[] | undefined;
export interface XrpcRequestInit { signal?: AbortSignal; headers?: HeadersInit;}