sessionOver

tcmts.atproto.sessionOver · #5bhfflaq · function · runs anywhere

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,
  };
}

uses

used by