/** * Backup API Routes — Hono router for encrypted backup operations. * * All endpoints require EncryptID JWT authentication. * The server stores opaque ciphertext blobs it cannot decrypt. */ import { Hono } from "hono"; import type { Context, Next } from "hono"; import { verifyEncryptIDToken, extractToken, } from "@encryptid/sdk/server"; import type { EncryptIDClaims } from "@encryptid/sdk/server"; import { putBackup, getBackup, listBackups, deleteBackup, deleteAllBackups, getUsage, } from "./backup-store"; const MAX_BLOB_SIZE = 10 * 1024 * 1024; // 10 MB per blob type BackupEnv = { Variables: { userId: string; claims: EncryptIDClaims; }; }; const backupRouter = new Hono(); /** Auth middleware — extracts and verifies JWT, sets userId. */ backupRouter.use("*", async (c: Context, next: Next) => { const token = extractToken(c.req.raw.headers); if (!token) { return c.json({ error: "Authentication required" }, 401); } let claims: EncryptIDClaims; try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid or expired token" }, 401); } c.set("userId", claims.sub); c.set("claims", claims); await next(); }); /** PUT /api/backup/:space/:docId — upload encrypted blob */ backupRouter.put("/:space/:docId", async (c) => { const userId = c.get("userId"); const space = c.req.param("space"); const docId = decodeURIComponent(c.req.param("docId")); const blob = await c.req.arrayBuffer(); if (blob.byteLength > MAX_BLOB_SIZE) { return c.json({ error: `Blob too large (max ${MAX_BLOB_SIZE} bytes)` }, 413); } if (blob.byteLength === 0) { return c.json({ error: "Empty blob" }, 400); } await putBackup(userId, space, docId, new Uint8Array(blob)); return c.json({ ok: true, size: blob.byteLength }); }); /** GET /api/backup/:space/:docId — download encrypted blob */ backupRouter.get("/:space/:docId", async (c) => { const userId = c.get("userId"); const space = c.req.param("space"); const docId = decodeURIComponent(c.req.param("docId")); const blob = await getBackup(userId, space, docId); if (!blob) { return c.json({ error: "Not found" }, 404); } const body = new Uint8Array(blob).buffer as ArrayBuffer; return new Response(body, { headers: { "Content-Type": "application/octet-stream", "Content-Length": blob.byteLength.toString(), }, }); }); /** GET /api/backup/:space — list manifest for a space */ backupRouter.get("/:space", async (c) => { const userId = c.get("userId"); const space = c.req.param("space"); const manifest = await listBackups(userId, space); return c.json(manifest); }); /** DELETE /api/backup/:space/:docId — delete specific backup */ backupRouter.delete("/:space/:docId", async (c) => { const userId = c.get("userId"); const space = c.req.param("space"); const docId = decodeURIComponent(c.req.param("docId")); const ok = await deleteBackup(userId, space, docId); if (!ok) { return c.json({ error: "Not found or delete failed" }, 404); } return c.json({ ok: true }); }); /** DELETE /api/backup/:space — delete all backups for a space */ backupRouter.delete("/:space", async (c) => { const userId = c.get("userId"); const space = c.req.param("space"); await deleteAllBackups(userId, space); return c.json({ ok: true }); }); /** GET /api/backup/status — overall backup status for authenticated user */ backupRouter.get("/", async (c) => { const userId = c.get("userId"); const usage = await getUsage(userId); return c.json(usage); }); export { backupRouter };