aboutsummaryrefslogtreecommitdiffstats
path: root/routes
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-07-13 20:21:49 -0700
committerAdam Soltys <[email protected]>2026-07-13 20:22:06 -0700
commit0d55c0ccceb378a9c1992f6e7d82a6786cdd19b8 (patch)
treee6cce8802e6f6159cbe5647eba47be1306d34e01 /routes
parent2e90d709178ac718e4f32229951e6ea38dfcc371 (diff)
downloadcoinos-server-0d55c0ccceb378a9c1992f6e7d82a6786cdd19b8.tar.gz
coinos-server-0d55c0ccceb378a9c1992f6e7d82a6786cdd19b8.zip
preimages: enforce a merchant-set price per preimage batch
Invoice creation is unauthenticated, so buyers could name their own price for a queued preimage (create the usePreimage invoice with amount: 1). POST /preimages now takes an optional price (sats) that is stored per entry and overrides the requested amount when the preimage is consumed; omitting it keeps the old any-amount behavior. Also gate the stored-preimage reveal in credit() on the invoice being fully paid, since internal sends can settle partial amounts. Bare-hex entries queued before this change still parse (treated as priceless). Co-Authored-By: Claude Fable 5 <[email protected]>
Diffstat (limited to 'routes')
-rw-r--r--routes/preimages.ts35
1 files changed, 24 insertions, 11 deletions
diff --git a/routes/preimages.ts b/routes/preimages.ts
index ec58c064..f6632b38 100644
--- a/routes/preimages.ts
+++ b/routes/preimages.ts
@@ -1,12 +1,15 @@
import { createHash } from "node:crypto";
import { db } from "$lib/db";
+import { parseEntry } from "$lib/invoices";
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.
+// with no backend of its own. An optional per-batch price (sats) is enforced
+// at invoice creation — invoice creation is unauthenticated, so without it a
+// buyer could name their own price for the secret.
const MAX_QUEUE = 1000;
const key = (uid) => `${uid}:preimages`;
@@ -16,7 +19,7 @@ const sha256 = (hex) =>
export default {
async add(req, res) {
try {
- const { preimages } = req.body;
+ const { preimages, price = null } = req.body;
if (!Array.isArray(preimages) || !preimages.length)
fail("preimages array required");
if (
@@ -25,10 +28,12 @@ export default {
)
)
fail("preimages must be 64-character hex strings");
+ if (price !== null && (!Number.isInteger(price) || price <= 0))
+ fail("price must be a positive integer amount of sats");
const k = key(req.user.id);
const existing = await db.lRange(k, 0, -1);
- const seen = new Set(existing);
+ const seen = new Set(existing.map((e) => parseEntry(e).preimage));
const fresh = [];
for (let p of preimages) {
p = p.toLowerCase();
@@ -39,11 +44,19 @@ export default {
if (existing.length + fresh.length > MAX_QUEUE)
fail(`queue limited to ${MAX_QUEUE} preimages`);
- if (fresh.length) await db.rPush(k, fresh);
+ if (fresh.length)
+ await db.rPush(
+ k,
+ fresh.map((preimage) => JSON.stringify({ preimage, price })),
+ );
res.send({
queued: existing.length + fresh.length,
- added: fresh.map((preimage) => ({ preimage, hash: sha256(preimage) })),
+ added: fresh.map((preimage) => ({
+ preimage,
+ hash: sha256(preimage),
+ price,
+ })),
});
} catch (e) {
bail(res, e.message);
@@ -52,13 +65,13 @@ export default {
async list(req, res) {
try {
- const preimages = await db.lRange(key(req.user.id), 0, -1);
+ const entries = await db.lRange(key(req.user.id), 0, -1);
res.send({
- queued: preimages.length,
- preimages: preimages.map((preimage) => ({
- preimage,
- hash: sha256(preimage),
- })),
+ queued: entries.length,
+ preimages: entries.map((e) => {
+ const { preimage, price } = parseEntry(e);
+ return { preimage, hash: sha256(preimage), price };
+ }),
});
} catch (e) {
bail(res, e.message);