Something went wrong. Try again.
A local-first note taking app
Something went wrong. Try again.
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184import { habitatAuth } from '../auth/habitatAuth';import { HABITAT_DOCS_COLLECTION, HABITAT_DOCS_EDIT_COLLECTION,} from './config';import { ListRecordsResponse, TypedRecord, XRPCError } from './types';
/** * Typed XRPC over `habitatAuth.fetch`. We hand-roll a tiny subset of the * endpoints we actually call rather than pulling in the upstream `internal` * package or `@atproto/api` codegen. Endpoints are validated at the type * level via the params/output maps below. * * TODO: When we replace `habitatAuth.fetch` with a proper ATProto client * (existing TODO in `src/auth/habitatAuth.ts`), fold this layer into it. */
type QueryEndpoints = { 'network.habitat.repo.listRecords': { params: { collection: string; limit?: number; cursor?: string; subjects?: string[]; includePermissions?: boolean; }; output: ListRecordsResponse; }; 'network.habitat.repo.getRecord': { params: { collection: string; rkey: string; repo: string; includePermissions?: boolean; }; output: TypedRecord; }; 'network.habitat.clique.getMembers': { params: { clique: string }; output: { members?: string[] }; }; 'com.atproto.server.getServiceAuth': { params: { aud: string; lxm?: string; exp?: number }; output: { token: string }; };};
type ProcedureEndpoints = { 'network.habitat.clique.createClique': { params: { members: string[] }; output: { clique: string }; }; 'network.habitat.repo.putRecord': { params: { repo: string; collection: string; rkey?: string; record: Record<string, unknown>; grantees?: Array<Record<string, unknown>>; }; output: { uri: string; cid: string }; }; 'network.habitat.repo.deleteRecord': { params: { repo: string; collection: string; rkey: string }; output: Record<string, never>; };};
function appendParam( params: URLSearchParams, key: string, value: unknown,): void { if (value === undefined || value === null) return; if (Array.isArray(value)) { for (const v of value) params.append(key, String(v)); return; } params.set(key, String(value));}
async function parseResponse<T>( response: Response, endpoint: string, requestPath: string,): Promise<T> { // Read once as text so we can both attempt JSON parsing AND include the // raw body in error messages. JSON-error envelopes from the habitat server // are small; the extra string allocation is negligible. const text = await response.text(); let data: unknown; try { data = text ? JSON.parse(text) : undefined; } catch { if (response.ok) return undefined as T; throw new XRPCError( response.status, { message: response.statusText }, { endpoint, requestPath, rawBody: text }, ); }
if (!response.ok) { const payload = data && typeof data === 'object' ? (data as { error?: string; message?: string }) : {}; throw new XRPCError(response.status, payload, { endpoint, requestPath, rawBody: text, }); }
return data as T;}
export async function query<K extends keyof QueryEndpoints>( endpoint: K, params: QueryEndpoints[K]['params'],): Promise<QueryEndpoints[K]['output']> { const search = new URLSearchParams(); for (const [key, value] of Object.entries(params)) { appendParam(search, key, value); } const requestPath = `/xrpc/${endpoint}?${search.toString()}`; const response = await habitatAuth.fetch(requestPath, { method: 'GET' }); return parseResponse<QueryEndpoints[K]['output']>( response, endpoint, requestPath, );}
export async function procedure<K extends keyof ProcedureEndpoints>( endpoint: K, body: ProcedureEndpoints[K]['params'],): Promise<ProcedureEndpoints[K]['output']> { const requestPath = `/xrpc/${endpoint}`; const response = await habitatAuth.fetch(requestPath, { method: 'POST', body: JSON.stringify(body), headers: { 'content-type': 'application/json' }, }); return parseResponse<ProcedureEndpoints[K]['output']>( response, endpoint, requestPath, );}
/** Typed helpers for the most-used "private" record paths. */
export async function listPrivateRecords<T = Record<string, unknown>>( collection: string, options: { limit?: number; cursor?: string; subjects?: string[] } = {},): Promise<ListRecordsResponse<T>> { const response = await query('network.habitat.repo.listRecords', { collection, limit: options.limit, cursor: options.cursor, subjects: options.subjects ?? [], }); return response as ListRecordsResponse<T>;}
export async function getPrivateRecord<T = Record<string, unknown>>( collection: string, rkey: string, repo: string, includePermissions = false,): Promise<TypedRecord<T>> { const response = await query('network.habitat.repo.getRecord', { collection, rkey, repo, includePermissions, }); return response as TypedRecord<T>;}
/** Re-exported for callers that want to grab the canonical collection names. */export { HABITAT_DOCS_COLLECTION, HABITAT_DOCS_EDIT_COLLECTION };