diff options
| author | Adam Soltys <[email protected]> | 2026-07-10 21:46:40 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-07-10 21:46:40 -0700 |
| commit | d466aab4451a89b2d69a2618f0a739b2e89903c5 (patch) | |
| tree | 083502eec7f32d3b142f92ee40d94528a0edaf55 /routes | |
| parent | c5bcf51d6adb1c7718f08ff11a9652762c51669d (diff) | |
| download | coinos-server-d466aab4451a89b2d69a2618f0a739b2e89903c5.tar.gz coinos-server-d466aab4451a89b2d69a2618f0a739b2e89903c5.zip | |
invoices: sell merchant-supplied preimages via a usePreimage queue
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 <[email protected]>
Diffstat (limited to 'routes')
| -rw-r--r-- | routes/preimages.ts | 76 |
1 files changed, 76 insertions, 0 deletions
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); + } + }, +}; |
