From d466aab4451a89b2d69a2618f0a739b2e89903c5 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Fri, 10 Jul 2026 21:46:40 -0700 Subject: invoices: sell merchant-supplied preimages via a usePreimage queue MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merchants POST /preimages (auth) to pre-load 32-byte hex preimages into a FIFO queue; creating an invoice with usePreimage: true consumes the next one so the invoice's payment hash is sha256(preimage). Paying the invoice reveals the merchant-chosen secret to the buyer — via their own wallet on external settlements, and via GET /invoice/:id once settled — enabling backend-less digital download stores (encrypted files on static hosting, preimage + public string = decryption key). - GET /preimages lists the queue with payment hashes so the merchant can map files to future invoices; POST /preimages/delete clears it - empty queue fails invoice creation rather than issuing a random preimage the buyer would pay for but couldn't decrypt with - preimages cln rejects as already used are discarded and the next tried - consumed preimages persist at preimage:${payment_hash} (TTL = invoice expiry) and credit() prefers them as the settlement ref, so internal coinos-to-coinos payments reveal the real preimage instead of the sender's uid Co-Authored-By: Claude Fable 5 --- routes/preimages.ts | 76 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 routes/preimages.ts (limited to 'routes') diff --git a/routes/preimages.ts b/routes/preimages.ts new file mode 100644 index 00000000..ec58c064 --- /dev/null +++ b/routes/preimages.ts @@ -0,0 +1,76 @@ +import { createHash } from "node:crypto"; +import { db } from "$lib/db"; +import { bail, fail } from "$lib/utils"; + +// Merchants pre-load payment preimages here, then have buyers create invoices +// with usePreimage: true — each such invoice consumes the next queued preimage +// so its payment hash is sha256(preimage). Once paid, the preimage is exposed +// on the invoice record, letting a static storefront sell decryption secrets +// with no backend of its own. + +const MAX_QUEUE = 1000; +const key = (uid) => `${uid}:preimages`; +const sha256 = (hex) => + createHash("sha256").update(Buffer.from(hex, "hex")).digest("hex"); + +export default { + async add(req, res) { + try { + const { preimages } = req.body; + if (!Array.isArray(preimages) || !preimages.length) + fail("preimages array required"); + if ( + !preimages.every( + (p) => typeof p === "string" && /^[0-9a-f]{64}$/i.test(p), + ) + ) + fail("preimages must be 64-character hex strings"); + + const k = key(req.user.id); + const existing = await db.lRange(k, 0, -1); + const seen = new Set(existing); + const fresh = []; + for (let p of preimages) { + p = p.toLowerCase(); + if (seen.has(p)) continue; + seen.add(p); + fresh.push(p); + } + + if (existing.length + fresh.length > MAX_QUEUE) + fail(`queue limited to ${MAX_QUEUE} preimages`); + if (fresh.length) await db.rPush(k, fresh); + + res.send({ + queued: existing.length + fresh.length, + added: fresh.map((preimage) => ({ preimage, hash: sha256(preimage) })), + }); + } catch (e) { + bail(res, e.message); + } + }, + + async list(req, res) { + try { + const preimages = await db.lRange(key(req.user.id), 0, -1); + res.send({ + queued: preimages.length, + preimages: preimages.map((preimage) => ({ + preimage, + hash: sha256(preimage), + })), + }); + } catch (e) { + bail(res, e.message); + } + }, + + async clear(req, res) { + try { + await db.del(key(req.user.id)); + res.send({ queued: 0 }); + } catch (e) { + bail(res, e.message); + } + }, +}; -- cgit v1.2.3