diff options
| author | Adam Soltys <[email protected]> | 2026-07-30 10:37:53 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-07-30 10:38:43 -0700 |
| commit | 1d48335f47bbb673b89b42a9ef2984eaae7b02d2 (patch) | |
| tree | 584476eee0df6e8dc9e7a7038dda41036343a297 /mintdemux.ts | |
| parent | fd1617ea66e01557e71debc7e45ca08cfe01bf70 (diff) | |
| download | coinos-server-1d48335f47bbb673b89b42a9ef2984eaae7b02d2.tar.gz coinos-server-1d48335f47bbb673b89b42a9ef2984eaae7b02d2.zip | |
mint-recovery: add meltguard + mintdemux proxies; ignore local *.bak
- meltguard: out-of-band CLNRest spend limiter (per-melt cap + cumulative
hard-freeze) that sits between the panic-mode mint and the node
- mintdemux: fronts the mint hostname, routing between the retired and current
mint instances after the hostname swap
- gitignore *.bak / *.bak-* so local backups (some hold secrets) never commit
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Diffstat (limited to 'mintdemux.ts')
| -rw-r--r-- | mintdemux.ts | 95 |
1 files changed, 95 insertions, 0 deletions
diff --git a/mintdemux.ts b/mintdemux.ts new file mode 100644 index 00000000..79e0fdc3 --- /dev/null +++ b/mintdemux.ts @@ -0,0 +1,95 @@ +// mintdemux — sits in front of the OLD (panic-mode) mint at mint.coinos.io. +// +// After the 2026-07-30 hostname swap, wallets that minted at the NEW mint during +// its ~1 day at mint.coinos.io still point here. Their proofs reference the new +// mint's keysets, which the old mint doesn't know — instead of a cryptic +// "keyset unknown" they get a clear tell-them-where-to-go error, and each hit is +// logged so we can size the cohort. +// +// Everything else is proxied through to the old mint untouched. +// +// Env: OLD_MINT (http://mint-old:3338), NEW_MINT (http://mint:3338), +// LOG_DIR (/demux), PORT (3341) +import { appendFileSync, mkdirSync } from "fs"; + +const OLD = process.env.OLD_MINT || "http://mint-old:3338"; +const NEW = process.env.NEW_MINT || "http://mint:3338"; +const DIR = process.env.LOG_DIR || "/demux"; +const PORT = Number(process.env.PORT || 3341); +mkdirSync(DIR, { recursive: true }); + +const REDIRECT_DETAIL = + "This ecash is from the current coinos mint, not the old one. " + + "Change this mint's URL in your wallet to https://newmint.coinos.io and try again. " + + "(mint.coinos.io now serves only pre-2026-07-20 ecash recovery)"; + +// keyset ids of the NEW mint; refreshed hourly in case it rotates +let newKeysets = new Set<string>(); +async function refreshKeysets() { + try { + const r = await fetch(`${NEW}/v1/keysets`); + const j: any = await r.json(); + const ids = (j.keysets || []).map((k: any) => String(k.id)); + if (ids.length) newKeysets = new Set(ids); + } catch (e: any) { + console.error("keyset refresh failed:", e.message); + } +} +await refreshKeysets(); +setInterval(refreshKeysets, 3600_000); +console.log(`mintdemux on :${PORT} -> ${OLD} | new-mint keysets: [${[...newKeysets].join(", ")}]`); + +const log = (m: string) => { + try { appendFileSync(`${DIR}/newmint-hits.log`, `${new Date().toISOString()} ${m}\n`); } catch {} + console.log("demux:", m); +}; + +// endpoints whose bodies carry proof/output keyset ids worth inspecting +const INSPECT = new Set(["/v1/swap", "/v1/melt/bolt11", "/v1/restore", "/v1/mint/bolt11"]); + +function findNewMintIds(body: any): string[] { + const ids = new Set<string>(); + for (const arr of [body?.inputs, body?.outputs, body?.proofs]) { + if (!Array.isArray(arr)) continue; + for (const it of arr) { + const id = String(it?.id ?? ""); + if (newKeysets.has(id)) ids.add(id); + } + } + return [...ids]; +} + +Bun.serve({ + port: PORT, hostname: "0.0.0.0", idleTimeout: 0, + async fetch(req) { + const url = new URL(req.url); + + if (req.method === "POST" && INSPECT.has(url.pathname)) { + const raw = await req.arrayBuffer(); + let body: any = {}; + try { body = JSON.parse(new TextDecoder().decode(raw)); } catch {} + const hit = findNewMintIds(body); + if (hit.length) { + const n = (body.inputs || body.proofs || body.outputs || []).length; + log(`${url.pathname} blocked: ${n} items on new-mint keyset(s) ${hit.join(",")} ua="${req.headers.get("user-agent") || "?"}"`); + return new Response(JSON.stringify({ detail: REDIRECT_DETAIL, code: 12002 }), + { status: 400, headers: { "content-type": "application/json" } }); + } + const resp = await fetch(`${OLD}${url.pathname}${url.search}`, { + method: "POST", + headers: { "content-type": req.headers.get("content-type") || "application/json" }, + body: raw, + }); + return new Response(await resp.arrayBuffer(), { status: resp.status, headers: { "content-type": resp.headers.get("content-type") || "application/json" } }); + } + + // transparent proxy for everything else + const body = req.method === "GET" || req.method === "HEAD" ? undefined : await req.arrayBuffer(); + const resp = await fetch(`${OLD}${url.pathname}${url.search}`, { + method: req.method, + headers: { ...(req.headers.get("content-type") ? { "content-type": req.headers.get("content-type")! } : {}) }, + body, + }); + return new Response(await resp.arrayBuffer(), { status: resp.status, headers: { "content-type": resp.headers.get("content-type") || "application/json" } }); + }, +}); |
