export interface ApiUser { id: string; email: string; plan: string; createdAt: string; } export interface ApiDevice { id: string; deviceId: string; name: string; type: string; avatar: string | null; linkedAt: string; lastSeenAt: string; } export interface MeResponse { user: ApiUser; devices: ApiDevice[]; } async function call(path: string, init?: RequestInit): Promise { return fetch(path, { credentials: "include", ...init, headers: { "Content-Type": "application/json", ...(init?.headers ?? {}), }, }); } export async function requestMagicLink(email: string): Promise { await call("/api/auth/request-link", { method: "POST", body: JSON.stringify({ email }), }); } export async function fetchMe(): Promise { const res = await call("/api/me"); if (res.status === 401) return null; if (!res.ok) throw new Error(`fetchMe failed: ${res.status}`); return (await res.json()) as MeResponse; } export async function registerDevice(input: { deviceId: string; name: string; type: string; avatar: string | null; }): Promise { const res = await call("/api/devices", { method: "POST", body: JSON.stringify(input), }); if (!res.ok) throw new Error(`registerDevice failed: ${res.status}`); return (await res.json()) as ApiDevice; } export async function unlinkDevice(id: string): Promise { const res = await call(`/api/devices/${encodeURIComponent(id)}`, { method: "DELETE" }); if (!res.ok && res.status !== 204) throw new Error(`unlinkDevice failed: ${res.status}`); } export async function signOut(): Promise { await call("/api/auth/logout", { method: "POST" }); } export interface CreateTransferResponse { transferId: string; uploadUrl: string; expiresAt: string; } export interface TransferHead { transferId: string; encryptedMetadata: string; sizeBytes: number; maxDownloads: number; downloadCount: number; expiresAt: string; requiresPassword: boolean; } export interface InboxTransfer { id: string; sizeBytes: number; encryptedMetadata: string; createdAt: string; expiresAt: string; maxDownloads: number; downloadCount: number; firstDownloadAt: string | null; senderUserId: string | null; recipientUserId: string | null; direction: "sent" | "received"; } export async function createTransfer(input: { sizeBytes: number; encryptedMetadata: string; recipientEmail?: string; maxDownloads?: number; expiresInDays?: number; deviceId?: string; password?: string; }): Promise { const res = await call("/api/transfers", { method: "POST", body: JSON.stringify(input), headers: input.deviceId ? { "X-Device-Id": input.deviceId } : {}, }); if (!res.ok) throw new Error(`createTransfer failed: ${res.status}`); return (await res.json()) as CreateTransferResponse; } export async function getTransferHead(id: string): Promise { const res = await call(`/api/transfers/${encodeURIComponent(id)}`); if (res.status === 404) throw new Error("transfer_not_found"); if (res.status === 410) { const body = await res.json().catch(() => ({})); throw new Error((body as { error?: string }).error ?? "transfer_gone"); } if (!res.ok) throw new Error(`getTransferHead failed: ${res.status}`); return (await res.json()) as TransferHead; } export async function consumeTransfer( id: string, password?: string, ): Promise<{ downloadUrl: string }> { const res = await call(`/api/transfers/${encodeURIComponent(id)}/consume`, { method: "POST", body: JSON.stringify(password ? { password } : {}), }); if (!res.ok) { const body = await res.json().catch(() => ({})); throw new Error((body as { error?: string }).error ?? `consume failed: ${res.status}`); } return (await res.json()) as { downloadUrl: string }; } export async function listInboxTransfers(): Promise { const res = await call("/api/transfers"); if (res.status === 401) return []; if (!res.ok) throw new Error(`listTransfers failed: ${res.status}`); const body = (await res.json()) as { transfers: InboxTransfer[] }; return body.transfers; } export async function deleteTransfer(id: string): Promise { const res = await call(`/api/transfers/${encodeURIComponent(id)}`, { method: "DELETE" }); if (!res.ok && res.status !== 204) throw new Error(`deleteTransfer failed: ${res.status}`); } export async function claimTransfer(id: string): Promise { const res = await call(`/api/transfers/${encodeURIComponent(id)}/claim`, { method: "POST" }); if (res.status === 401) return false; if (!res.ok) return false; const body = (await res.json()) as { claimed: boolean }; return body.claimed; }