/** * IPFS API Routes — Hono router mounted at /api/ipfs. * * GET /api/ipfs/status — IPFS node health + stats (authenticated) * GET /api/ipfs/:cid — Proxy gateway fetch (avoids CORS for clients) * POST /api/ipfs/pin — Pin arbitrary blob (authenticated, 50MB limit) * DELETE /api/ipfs/:cid — Unpin a CID (authenticated) */ import { Hono } from "hono"; import type { Context, Next } from "hono"; import { verifyToken, extractToken } from "./auth"; import type { EncryptIDClaims } from "./auth"; import { isIPFSEnabled, pinToIPFS, unpinFromIPFS, fetchFromIPFS, getIPFSStatus, ipfsGatewayUrl, } from "./ipfs"; const MAX_PIN_SIZE = 50 * 1024 * 1024; // 50 MB type IPFSEnv = { Variables: { userId: string; claims: EncryptIDClaims; }; }; export const ipfsRouter = new Hono(); /** Auth middleware for mutating endpoints. */ async function requireAuth(c: Context, next: Next) { const token = extractToken(c.req.raw.headers); if (!token) return c.json({ error: "Authentication required" }, 401); try { const claims = await verifyToken(token); c.set("userId", claims.sub); c.set("claims", claims); } catch { return c.json({ error: "Invalid or expired token" }, 401); } await next(); } /** GET /status — IPFS node health + stats */ ipfsRouter.get("/status", requireAuth, async (c) => { if (!isIPFSEnabled()) { return c.json({ enabled: false }, 503); } try { const status = await getIPFSStatus(); return c.json({ enabled: true, ...status }); } catch (e: any) { return c.json({ enabled: true, error: e.message }, 502); } }); /** POST /pin — Pin a blob to IPFS */ ipfsRouter.post("/pin", requireAuth, async (c) => { if (!isIPFSEnabled()) { return c.json({ error: "IPFS not enabled" }, 503); } const blob = await c.req.arrayBuffer(); if (blob.byteLength === 0) { return c.json({ error: "Empty body" }, 400); } if (blob.byteLength > MAX_PIN_SIZE) { return c.json({ error: `Blob too large (max ${MAX_PIN_SIZE} bytes)` }, 413); } const filename = c.req.header("X-Filename") || `upload-${Date.now()}.bin`; try { const cid = await pinToIPFS(new Uint8Array(blob), filename); return c.json({ cid, gateway: ipfsGatewayUrl(cid), size: blob.byteLength }); } catch (e: any) { console.error("[ipfs/pin]", e.message); return c.json({ error: "IPFS pin failed" }, 502); } }); /** DELETE /:cid — Unpin a CID */ ipfsRouter.delete("/:cid", requireAuth, async (c) => { if (!isIPFSEnabled()) { return c.json({ error: "IPFS not enabled" }, 503); } const cid = c.req.param("cid"); try { await unpinFromIPFS(cid); return c.json({ ok: true, cid }); } catch (e: any) { console.error("[ipfs/unpin]", e.message); return c.json({ error: "IPFS unpin failed" }, 502); } }); /** GET /:cid — Proxy gateway fetch (public, no auth needed) */ ipfsRouter.get("/:cid", async (c) => { const cid = c.req.param("cid"); try { const res = await fetchFromIPFS(cid); if (!res.ok) { return c.json({ error: "Not found on IPFS" }, 404); } const body = await res.arrayBuffer(); return new Response(body, { headers: { "Content-Type": res.headers.get("Content-Type") || "application/octet-stream", "Cache-Control": "public, max-age=31536000, immutable", }, }); } catch (e: any) { console.error("[ipfs/proxy]", e.message); return c.json({ error: "IPFS fetch failed" }, 502); } });