Something went wrong. Try again.
Monorepo for Tangled tangled.org
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247import { parseCanonicalResourceUri } from "@atcute/lexicons/syntax";import type { OAuthUserAgent } from "@atcute/oauth-browser-client";import { mintServiceAuth } from "$lib/auth/agent";import { attest } from "$lib/api/attestation";import { clear } from "$lib/api/cache";import { buildUrl, toResponseError } from "$lib/api/_request";import { atUriOf, cidOf, didOf, gitOidOf, nsidOf, recordKeyOf, type AtUri, type Cid, type Did, type KnotOrigin, type Nsid, type Rkey} from "$lib/api/syntax";import { rkeyFromUri } from "$lib/api/uri";
export interface CobRef { uri: AtUri; cid: Cid;}
export interface Written { uri: AtUri; cid: Cid; rkey: Rkey;}
export const withRkey = (saved: { uri: string; cid: string }): Written => ({ uri: atUriOf(saved.uri), cid: cidOf(saved.cid), rkey: rkeyFromUri(saved.uri)});
const CREATE_NSID = nsidOf("com.atproto.repo.createRecord");const PUT_NSID = nsidOf("com.atproto.repo.putRecord");const DELETE_NSID = nsidOf("com.atproto.repo.deleteRecord");const GET_NSID = nsidOf("com.atproto.repo.getRecord");const UPLOAD_BLOB_NSID = nsidOf("com.atproto.repo.uploadBlob");const EDITOR_FIELD = "x-tngl-editor";const CHANGE_FIELD = "x-tngl-change";
interface KnotWrite { uri: string; cid: string; [CHANGE_FIELD]?: string;}
export const cobRefOf = (view: { uri: string; cid?: string | null }): CobRef => { if (!view.cid) throw new Error(`${view.uri} is missing its cid, so it can't be changed`); return { uri: atUriOf(view.uri), cid: cidOf(view.cid) };};
const knotPost = async <T>( agent: OAuthUserAgent, knot: KnotOrigin, repo: Did, nsid: Nsid, body: BodyInit, contentType?: string, init?: { signal?: AbortSignal; invalidate?: boolean }): Promise<T> => { // the knot checks aud against the repo did, not its own did:web const token = await mintServiceAuth(agent, { aud: repo, lxm: nsid }); const response = await fetch(buildUrl(`https://${knot}`, nsid), { method: "POST", headers: { ...(contentType ? { "content-type": contentType } : {}), accept: "application/json", authorization: `Bearer ${token}` }, body, signal: init?.signal }); if (!response.ok) throw await toResponseError(response); if (init?.invalidate !== false) clear(); return (await response.json()) as T;};
export const knotProcedure = async <T>( agent: OAuthUserAgent, knot: KnotOrigin, repo: Did, nsid: Nsid, input: Record<string, unknown>, init?: { signal?: AbortSignal; invalidate?: boolean }): Promise<T> => knotPost<T>( agent, knot, repo, nsid, JSON.stringify({ repo, ...input }), "application/json", init );
// the write already landed, so a bad id falls back to the lookup instead of failing itconst changeOf = (saved: KnotWrite) => { try { return saved[CHANGE_FIELD] ? gitOidOf(saved[CHANGE_FIELD]) : undefined; } catch { return undefined; }};
// every create and edit comes through here, so call sites never attest themselvesconst attested = (agent: OAuthUserAgent, knot: KnotOrigin, saved: KnotWrite): CobRef => { const ref = { uri: atUriOf(saved.uri), cid: cidOf(saved.cid) }; attest(agent, ref, knot, changeOf(saved)); return ref;};
export const createCob = async <T extends { $type: string }>( agent: OAuthUserAgent, knot: KnotOrigin, repo: Did, collection: Nsid, record: T, rkey?: Rkey): Promise<CobRef> => attested( agent, knot, await knotProcedure<KnotWrite>(agent, knot, repo, CREATE_NSID, { collection, ...(rkey ? { rkey } : {}), record }) );
export const editCob = async <T extends { $type: string }>( agent: OAuthUserAgent, knot: KnotOrigin, from: CobRef, record: T): Promise<CobRef> => { const { repo, collection, rkey } = parseCanonicalResourceUri(from.uri); const input = { collection: nsidOf(collection), rkey: recordKeyOf(rkey), swapRecord: from.cid, record }; return attested( agent, knot, await knotProcedure<KnotWrite>(agent, knot, didOf(repo), PUT_NSID, input) );};
export const eraseCob = async ( agent: OAuthUserAgent, knot: KnotOrigin, from: CobRef): Promise<void> => { const { repo, collection, rkey } = parseCanonicalResourceUri(from.uri); await knotProcedure(agent, knot, didOf(repo), DELETE_NSID, { collection: nsidOf(collection), rkey: recordKeyOf(rkey), swapRecord: from.cid });};
export const readCob = async <T>(knot: KnotOrigin, uri: AtUri): Promise<CobRecord<T>> => { const { repo, collection, rkey } = parseCanonicalResourceUri(uri); const response = await fetch( buildUrl(`https://${knot}`, GET_NSID, { repo, collection, rkey }), { headers: { accept: "application/json" } } ); if (!response.ok) throw await toResponseError(response); const read = (await response.json()) as { uri: string; cid?: string; value: T }; return { uri: atUriOf(read.uri), cid: cidOf(read.cid ?? ""), value: read.value };};
export interface CobRecord<T> extends CobRef { value: T;}
export interface KnotBlobReceipt { $type: "blob"; ref: { $link: Cid }; mimeType: string; size: number;}
const blobReceiptOf = (value: unknown): KnotBlobReceipt => { const candidate = value as Record<string, unknown>; const ref = candidate?.ref as Record<string, unknown> | undefined; if ( typeof candidate?.$type !== "string" || typeof ref?.$link !== "string" || typeof candidate.mimeType !== "string" || typeof candidate.size !== "number" ) throw new Error("The blobject returned by the knot is malformed."); return { $type: "blob", ref: { $link: cidOf(ref.$link) }, mimeType: candidate.mimeType, size: candidate.size };};
export const uploadBlobToKnot = async ( agent: OAuthUserAgent, knot: KnotOrigin, repo: Did, bytes: BufferSource, contentType?: string): Promise<KnotBlobReceipt> => blobReceiptOf( ( await knotPost<{ blob?: unknown }>( agent, knot, repo, UPLOAD_BLOB_NSID, bytes as BodyInit, contentType ) ).blob );
export const updateCob = async <T extends { $type: string }>( agent: OAuthUserAgent, knot: KnotOrigin, uri: AtUri, mutate: (current: T) => T): Promise<CobRecord<T>> => { const current = await readCob<T>(knot, uri); const { [EDITOR_FIELD]: _, ...stored } = current.value as T & { [EDITOR_FIELD]?: string }; const value = mutate(stored as T); return { ...(await editCob(agent, knot, current, value)), value };};