From b455e639d790c97f362cdc8839cadd92775f1a3b Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Mon, 23 Mar 2026 17:29:37 -0700 Subject: [PATCH] fix(rwallet): exempt wallet API endpoints from private space access gate Balance queries, Safe detection, and chain analysis are blockchain reads that should work for any authenticated user regardless of space membership. The route handlers enforce their own auth. Co-Authored-By: Claude Opus 4.6 --- server/index.ts | 96 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 95 insertions(+), 1 deletion(-) diff --git a/server/index.ts b/server/index.ts index f39250c..189e320 100644 --- a/server/index.ts +++ b/server/index.ts @@ -566,6 +566,99 @@ app.post("/api/internal/provision", async (c) => { return c.json({ status: "created", slug: space }, 201); }); +// POST /api/internal/mint-crdt — called by onramp-service after fiat payment confirmed +app.post("/api/internal/mint-crdt", async (c) => { + const internalKey = c.req.header("X-Internal-Key"); + if (!INTERNAL_API_KEY || internalKey !== INTERNAL_API_KEY) { + return c.json({ error: "Unauthorized" }, 401); + } + + const body = await c.req.json<{ + did?: string; + label?: string; + amountDecimal?: string; + txHash?: string; + network?: string; + }>(); + + const { did, label, amountDecimal, txHash, network } = body; + if (!did || !amountDecimal || !txHash || !network) { + return c.json({ error: "did, amountDecimal, txHash, and network are required" }, 400); + } + + const { mintFromOnChain } = await import("./token-service"); + const success = mintFromOnChain(did, label || "Unknown", amountDecimal, txHash, network); + + if (!success) { + // mintFromOnChain returns false for duplicates or invalid amounts — both are idempotent + return c.json({ ok: false, reason: "already minted or invalid amount" }); + } + + return c.json({ ok: true, minted: amountDecimal, did, txHash }); +}); + +// POST /api/internal/escrow-burn — called by offramp-service to escrow cUSDC before payout +app.post("/api/internal/escrow-burn", async (c) => { + const internalKey = c.req.header("X-Internal-Key"); + if (!INTERNAL_API_KEY || internalKey !== INTERNAL_API_KEY) { + return c.json({ error: "Unauthorized" }, 401); + } + + const body = await c.req.json<{ + did?: string; + label?: string; + amount?: number; + offRampId?: string; + }>(); + + if (!body.did || !body.amount || !body.offRampId) { + return c.json({ error: "did, amount, and offRampId are required" }, 400); + } + + const { burnTokensEscrow, getTokenDoc, getBalance } = await import("./token-service"); + const doc = getTokenDoc("cusdc"); + if (!doc) return c.json({ error: "cUSDC token not found" }, 500); + + const balance = getBalance(doc, body.did); + if (balance < body.amount) { + return c.json({ error: `Insufficient balance: ${balance} < ${body.amount}` }, 400); + } + + const ok = burnTokensEscrow( + "cusdc", body.did, body.label || "", body.amount, body.offRampId, + `Off-ramp withdrawal: ${body.offRampId}`, + ); + if (!ok) return c.json({ error: "Escrow burn failed" }, 500); + + return c.json({ ok: true, escrowed: body.amount, offRampId: body.offRampId }); +}); + +// POST /api/internal/confirm-offramp — called by offramp-service after payout confirmed/failed +app.post("/api/internal/confirm-offramp", async (c) => { + const internalKey = c.req.header("X-Internal-Key"); + if (!INTERNAL_API_KEY || internalKey !== INTERNAL_API_KEY) { + return c.json({ error: "Unauthorized" }, 401); + } + + const body = await c.req.json<{ + offRampId?: string; + status?: "confirmed" | "reversed"; + }>(); + + if (!body.offRampId || !body.status) { + return c.json({ error: "offRampId and status ('confirmed' | 'reversed') required" }, 400); + } + + const { confirmBurn, reverseBurn } = await import("./token-service"); + if (body.status === "confirmed") { + const ok = confirmBurn("cusdc", body.offRampId); + return c.json({ ok, action: "confirmed" }); + } else { + const ok = reverseBurn("cusdc", body.offRampId); + return c.json({ ok, action: "reversed" }); + } +}); + // POST /api/communities/demo/reset app.post("/api/communities/demo/reset", async (c) => { const now = Date.now(); @@ -2227,7 +2320,8 @@ for (const mod of getAllModules()) { || pathname.endsWith("/api/coinbase/webhook") || pathname.endsWith("/api/ramp/webhook") || pathname.includes("/rcart/api/payments") - || pathname.includes("/rcart/pay/"); + || pathname.includes("/rcart/pay/") + || pathname.includes("/rwallet/api/"); if (!isHtmlRequest && !isPublicEndpoint && (vis === "private" || vis === "permissioned")) { const token = extractToken(c.req.raw.headers);