sessionOver
A Session over a way to ask the account's PDS as them: its records and service-auth tokens are requests
made with send, and an answer that isn't OK is Refused, saying what the PDS said.
import { Refused } from "#codebase/tcmts/atproto"; import type { Session } from "#codebase/tcmts/atproto"; export function sessionOver(o: { did: string; handle?: string; scope: string; send: (path: string, init?: RequestInit) => Promise<Response>; signOut: () => Promise<void>; }): Session { const who = o.handle ?? o.did; const answer = async <T>(res: Response, what: string): Promise<T> => { const body = (await res.json().catch(() => ({}))) as T & { error?: string; message?: string }; if (!res.ok) throw new Refused(`${what} as ${who}: ${body.message ?? body.error ?? `HTTP ${res.status}`}`); return body; }; const post = async <T>(method: string, body: unknown, what: string) => answer<T>( await o.send(`/xrpc/${method}`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body), }), what, ); const typed = (collection: string, record: Record<string, unknown>) => ({ $type: collection, ...record, }); return { did: o.did, ...(o.handle ? { handle: o.handle } : {}), scope: o.scope, fetch: o.send, async getRecord<T>(collection: string, rkey: string) { const q = new URLSearchParams({ repo: o.did, collection, rkey }); const res = await o.send(`/xrpc/com.atproto.repo.getRecord?${q}`); if (res.status === 400 || res.status === 404) { const e = (await res .clone() .json() .catch(() => ({}))) as { error?: string }; if (e.error === "RecordNotFound" || res.status === 404) return undefined; } return answer<{ uri: string; cid: string; value: T }>(res, `reading ${collection}/${rkey}`); }, putRecord: (collection, rkey, record) => post( "com.atproto.repo.putRecord", { repo: o.did, collection, rkey, record: typed(collection, record) }, `writing ${collection}/${rkey}`, ), createRecord: (collection, record, rkey) => post( "com.atproto.repo.createRecord", { repo: o.did, collection, ...(rkey ? { rkey } : {}), record: typed(collection, record) }, `writing a ${collection}`, ), async deleteRecord(collection, rkey) { await post( "com.atproto.repo.deleteRecord", { repo: o.did, collection, rkey }, `deleting ${collection}/${rkey}`, ); }, async listRecords<T>(collection: string, l: { limit?: number; cursor?: string } = {}) { const q = new URLSearchParams({ repo: o.did, collection, ...(l.limit ? { limit: String(l.limit) } : {}), ...(l.cursor ? { cursor: l.cursor } : {}), }); return answer<{ records: { uri: string; cid: string; value: T }[]; cursor?: string }>( await o.send(`/xrpc/com.atproto.repo.listRecords?${q}`), `listing ${collection}`, ); }, async serviceAuth(s) { const q = new URLSearchParams({ aud: s.aud, lxm: s.lxm, exp: String(Math.floor((s.exp ?? Date.now() + 60_000) / 1000)), }); const body = await answer<{ token?: string }>( await o.send(`/xrpc/com.atproto.server.getServiceAuth?${q}`), `signing in to ${s.aud}`, ); if (!body.token) throw new Refused(`${who}'s PDS signed no token for ${s.aud}`); return body.token; }, signOut: o.signOut, }; }