import type { DocumentUpdateResponse, DocumentVersion, DocumentVersionWithoutContent, FetchLatestDocumentsResponse, ListVaultsResponse, PingResponse, VaultHistoryResponse } from "./types"; async function fetchJsonWithToken( path: string, token: string, init?: RequestInit ): Promise { const response = await fetch(path, { ...init, headers: { Authorization: `Bearer ${token}`, "device-id": "history-ui", ...init?.headers } }); if (!response.ok) { const body = await response.text(); throw new Error(`HTTP ${response.status}: ${body}`); } return response.json() as Promise; } export async function listVaults(token: string): Promise { return fetchJsonWithToken("/vaults", token); } export class ApiClient { constructor( private vaultId: string, private token: string ) {} private get baseUrl(): string { return `/vaults/${encodeURIComponent(this.vaultId)}`; } private async fetchJson(path: string, init?: RequestInit): Promise { return fetchJsonWithToken(path, this.token, init); } async ping(): Promise { return this.fetchJson(`${this.baseUrl}/ping`); } async fetchLatestDocuments(): Promise { return this.fetchJson(`${this.baseUrl}/documents`); } async fetchDocumentVersions( documentId: string ): Promise { return this.fetchJson( `${this.baseUrl}/documents/${documentId}/versions` ); } async fetchDocumentVersion( documentId: string, vaultUpdateId: number ): Promise { return this.fetchJson( `${this.baseUrl}/documents/${documentId}/versions/${vaultUpdateId}` ); } async fetchDocumentVersionContent( documentId: string, vaultUpdateId: number ): Promise { const response = await fetch( `${this.baseUrl}/documents/${documentId}/versions/${vaultUpdateId}/content`, { headers: { Authorization: `Bearer ${this.token}`, "device-id": "history-ui" } } ); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } return response.arrayBuffer(); } async fetchVaultHistory( limit?: number, beforeUpdateId?: number ): Promise { const params = new URLSearchParams(); if (limit !== undefined) params.set("limit", String(limit)); if (beforeUpdateId !== undefined) params.set("before_update_id", String(beforeUpdateId)); const qs = params.toString(); return this.fetchJson(`${this.baseUrl}/history${qs ? `?${qs}` : ""}`); } /** * Upload a new version of an existing (non-deleted) document. The * server treats this like any other edit — server-side merging, * path dedupe, and broadcast still apply. Used by the UI to restore * an old version by re-submitting its bytes on top of the latest. */ async updateBinaryDocument( documentId: string, parentVersionId: number, relativePath: string, content: ArrayBuffer ): Promise { const form = new FormData(); form.append("parent_version_id", String(parentVersionId)); form.append("relative_path", relativePath); form.append("content", new Blob([content])); return this.fetchJson( `${this.baseUrl}/documents/${documentId}/binary`, { method: "PUT", body: form } ); } /** * Create a new document. Used by the UI to restore a deleted * document: `update_document` short-circuits on `is_deleted`, so * resurrection has to go through `create_document` — which detects * an existing doc at the same path, merges or dedupes as needed, * and returns the resulting version. */ async createDocument( lastSeenVaultUpdateId: number, relativePath: string, content: ArrayBuffer ): Promise { const form = new FormData(); form.append("last_seen_vault_update_id", String(lastSeenVaultUpdateId)); form.append("relative_path", relativePath); form.append("content", new Blob([content])); return this.fetchJson(`${this.baseUrl}/documents`, { method: "POST", body: form }); } }