1// Typed fetch client for the Worker API.23import type {4 ApiError,5 CommitRequest,6 CommitResponse,7 Manifest,8 PrepareResponse,9 SiteDetail,10 SiteInfo,11 StatusResponse,12 Target,13 VaultInfo,14} from "@nzip/shared";15import { assertVaultAllowed, type Config } from "./config.ts";1617export class ApiClient {18 constructor(private config: Config) {}1920 private async request<T>(method: string, path: string, body?: BodyInit): Promise<T> {21 const res = await fetch(`${this.config.server}${path}`, {22 method,23 headers: {24 authorization: `Bearer ${this.config.token}`,25 ...(typeof body === "string" ? { "content-type": "application/json" } : {}),26 },27 body,28 });29 const text = await res.text();30 if (!res.ok) {31 let message = text;32 try {33 message = (JSON.parse(text) as ApiError).error;34 } catch { /* not json */ }35 throw new Error(`${res.status}: ${message}`);36 }37 return JSON.parse(text) as T;38 }3940 status(): Promise<StatusResponse> {41 return this.request("GET", "/api/status");42 }4344 prepare(manifest: Manifest): Promise<PrepareResponse> {45 return this.request("POST", "/api/push/prepare", JSON.stringify({ manifest }));46 }4748 uploadBlob(hash: string, bytes: Uint8Array): Promise<{ ok: true }> {49 return this.request("PUT", `/api/blob/${hash}`, bytes as unknown as BodyInit);50 }5152 commit(req: CommitRequest): Promise<CommitResponse> {53 return this.request("POST", "/api/push/commit", JSON.stringify(req));54 }5556 listSites(vault?: string): Promise<SiteInfo[]> {57 const q = vault ? `?vault=${encodeURIComponent(vault)}` : "";58 return this.request("GET", `/api/sites${q}`);59 }6061 siteDetail(target: string): Promise<SiteDetail> {62 return this.request("GET", `/api/sites/${encodeURIComponent(target)}`);63 }6465 patchSite(66 target: string,67 patch: { ttl?: number | "forever"; password?: string | null },68 ): Promise<SiteDetail> {69 return this.request("PATCH", `/api/sites/${encodeURIComponent(target)}`, JSON.stringify(patch));70 }7172 deleteSite(target: string): Promise<{ ok: true; address: string }> {73 return this.request("DELETE", `/api/sites/${encodeURIComponent(target)}`);74 }7576 revert(target: string, toSeq?: number): Promise<CommitResponse & { revertedTo: number }> {77 return this.request(78 "POST",79 `/api/sites/${encodeURIComponent(target)}/revert`,80 JSON.stringify(toSeq === undefined ? {} : { toSeq }),81 );82 }8384 listVaults(): Promise<VaultInfo[]> {85 return this.request("GET", "/api/vaults");86 }8788 createVault(name: string, slot?: number): Promise<VaultInfo> {89 return this.request("POST", "/api/vaults", JSON.stringify({ name, slot }));90 }91}9293/**94 * Resolve a CLI target string to (API path target, commit body target).95 * A bare alias picks up the default vault from config.96 */97export function resolveCliTarget(raw: string, config: Config): string {98 if (/^[0-9a-f]{4}$/.test(raw)) return raw; // raw address names no vault99 if (raw.includes(":")) {100 const [vault] = raw.split(":");101 assertVaultAllowed(vault, config);102 return raw;103 }104 const vault = config.defaultVault;105 if (!vault) {106 throw new Error(107 `"${raw}" is a bare alias but no defaultVault is set — use vault:alias or set defaultVault in config`,108 );109 }110 assertVaultAllowed(vault, config);111 return `${vault}:${raw}`;112}113114export function commitTargetFor(raw: string | undefined, config: Config): Target {115 if (raw === undefined) {116 const vault = config.defaultVault;117 if (!vault) throw new Error("no target given and no defaultVault set");118 assertVaultAllowed(vault, config);119 return { vault };120 }121 if (/^[0-9a-f]{4}$/.test(raw)) return { address: parseInt(raw, 16) };122 const resolved = resolveCliTarget(raw, config);123 const [vault, alias] = resolved.split(":");124 return { vault, alias };125}