aboutsummaryrefslogtreecommitdiffstats
path: root/routes
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-07-10 21:46:40 -0700
committerAdam Soltys <[email protected]>2026-07-10 21:46:40 -0700
commitd466aab4451a89b2d69a2618f0a739b2e89903c5 (patch)
tree083502eec7f32d3b142f92ee40d94528a0edaf55 /routes
parentc5bcf51d6adb1c7718f08ff11a9652762c51669d (diff)
downloadcoinos-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.ts76
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);
+ }
+ },
+};