@forgesworn/moneyer 0.13.1 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.14.0 - 2026-09-11
4
+
5
+ - **A plain note is unsigned.** LUD-25 Part 2 certifies `cp1` notes only:
6
+ a hash has nothing to attest to without disclosing the secret. A rotate,
7
+ split or merge to a hash output now answers a bare `{"status":"OK"}`,
8
+ where it used to carry the old Part 1 signature over the hash. `cp1`
9
+ outputs are certified as before, with `cs1`, on the callback and on the
10
+ informational GET by `ck1` or `cp1`. The web client no longer attaches
11
+ the settlement receipt's signature to the note it mints; the receipt
12
+ still authenticates settlement, and `/verify` still carries it.
13
+ - Graded by lnurlcash-conformance 0.10.0, whose new Part 2 check rotates
14
+ a note into a `cp1` key, verifies its certificate, and rotates it home.
15
+
16
+ ## 0.13.2 - 2026-09-11
17
+
18
+ - An operator name that changes owner drops the branch the previous owner
19
+ set. `MONEYER_ZAP_NAMES` re-points a name at every startup, and until now
20
+ the old owner's `cx1` survived it, so the name went on paying the previous
21
+ owner's keys. The same owner keeps their branch and its index across a
22
+ restart, as before.
23
+
3
24
  ## 0.13.1 - 2026-09-11
4
25
 
5
26
  - A name's owner can set or clear its `cx1` while self-service registration
package/README.md CHANGED
@@ -151,11 +151,11 @@ against the same endpoints every wallet uses:
151
151
  before the invoice exists, invoice QR (tap opens a wallet), LUD-21
152
152
  polling with a countdown, and on settlement the note is claimed and
153
153
  **immediately rotated** - the preimage any invoice-observer could poll
154
- out of verify is dead before the note is shown. The rotated note's
155
- signature is verified against the mint's advertised key in front of the
156
- user, and the QR arrives under scratch-off silver foil, rubbed away like a scratch card.
154
+ out of verify is dead before the note is shown. The settlement receipt
155
+ is verified against the mint's advertised key in front of the user, and
156
+ the QR arrives under scratch-off silver foil, rubbed away like a scratch card.
157
157
  - **Check a note**: live value, spent/unknown/pending classified in plain
158
- words, offline signature verification.
158
+ words, and offline verification of a `cp1` note's certificate.
159
159
  - `MONEYER_WALLET_URL` (optional) links minted notes straight into a
160
160
  companion web wallet's `#/claim` route.
161
161
 
@@ -354,7 +354,8 @@ named or spent, the two kinds mix freely:
354
354
  - `GET /w?k1=<ck1>` or `GET /w?p=<cp1>` looks a Part 2 note up, and the
355
355
  answer carries `sig`, the mint's certificate for it as `cs1`.
356
356
  - `p1`/`p2` on the callback may be `cp1` keys, and the matching `sig`/`sig2`
357
- come back as `cs1`. A hash output still gets a plain hex signature.
357
+ come back as `cs1`. A hash output gets none: a plain note is unsigned by
358
+ design, since there is nothing to attest to without disclosing the secret.
358
359
  - A merge may combine Part 1 secrets and Part 2 `ck1`s in one request.
359
360
 
360
361
  A recipient checks a Part 2 note offline from its `ck1` and `cs1` alone.
package/dist/server.js CHANGED
@@ -91,10 +91,13 @@ export const createMoneyer = async (config, deps = {}) => {
91
91
  const store = deps.store ?? new NoteStore(config.dbPath);
92
92
  const backend = deps.backend ?? backendFor(config);
93
93
  const signer = createNoteSigner(config.signingKey);
94
- // A cp1 note's certificate is the same signature, carried as cs1.
95
- const certify = (ref, amountMsat) => {
96
- const sig = signer.sign(ref.id, amountMsat);
97
- return ref.cp1 ? encodeCs1(hexToBytes(sig)) : sig;
94
+ // LUD-25 Part 2 signs cp1 notes only, as cs1. A hash output is a plain
95
+ // Part 1 note: there is nothing to attest to without disclosing the
96
+ // secret, so it goes out unsigned.
97
+ const certify = (ref, amountMsat) => ref.cp1 ? encodeCs1(hexToBytes(signer.sign(ref.id, amountMsat))) : undefined;
98
+ const certified = (field, ref, amountMsat) => {
99
+ const sig = certify(ref, amountMsat);
100
+ return sig ? { [field]: sig } : {};
98
101
  };
99
102
  const webAssets = deps.webAssets === undefined ? loadWebAssets() : deps.webAssets;
100
103
  // Node identity for the discovery endpoint, fetched once, best-effort: a
@@ -928,7 +931,7 @@ export const createMoneyer = async (config, deps = {}) => {
928
931
  ...(signer ? { mintPubkey: signer.pubkey } : {}),
929
932
  // LUD-25 Part 2: a note named by cp1 or ck1 gets its certificate
930
933
  // here, so a wallet need not rotate just to obtain one.
931
- ...(ref.cp1 ? { sig: certify(ref, note.amountMsat) } : {})
934
+ ...certified('sig', ref, note.amountMsat)
932
935
  });
933
936
  }
934
937
  // ---- the mutating callback: melt / rotate / split / merge ----
@@ -1009,12 +1012,8 @@ export const createMoneyer = async (config, deps = {}) => {
1009
1012
  if (first) {
1010
1013
  return send({
1011
1014
  status: 'OK',
1012
- ...(signer
1013
- ? {
1014
- sig: certify(o1, first.amountMsat),
1015
- ...(second ? { sig2: certify(o2, second.amountMsat) } : {})
1016
- }
1017
- : {})
1015
+ ...certified('sig', o1, first.amountMsat),
1016
+ ...(second ? certified('sig2', o2, second.amountMsat) : {})
1018
1017
  });
1019
1018
  }
1020
1019
  }
@@ -1148,7 +1147,8 @@ export const createMoneyer = async (config, deps = {}) => {
1148
1147
  }
1149
1148
  return send({
1150
1149
  status: 'OK',
1151
- ...(signer ? { sig: certify(o1, amount), sig2: certify(o2, changeMsat) } : {})
1150
+ ...certified('sig', o1, amount),
1151
+ ...certified('sig2', o2, changeMsat)
1152
1152
  });
1153
1153
  }
1154
1154
  // rotate (one k1) or merge (several) - the same swap either way.
@@ -1165,7 +1165,7 @@ export const createMoneyer = async (config, deps = {}) => {
1165
1165
  // same oracle-free reason for a collision as for a dead k1
1166
1166
  return fail('Invalid or already spent k1.');
1167
1167
  }
1168
- return send({ status: 'OK', ...(signer ? { sig: certify(o1, mergedMsat) } : {}) });
1168
+ return send({ status: 'OK', ...certified('sig', o1, mergedMsat) });
1169
1169
  }
1170
1170
  return fail('Not found.', 404);
1171
1171
  };
package/dist/store.js CHANGED
@@ -531,10 +531,18 @@ export class NoteStore {
531
531
  // The operator's own names, re-applied at every startup. Idempotent, and
532
532
  // the environment wins: it is the operator's mint, and a name they put
533
533
  // in the environment is one they mean to have.
534
+ //
535
+ // A branch the owner set survives a restart, but not a change of owner:
536
+ // it is the previous owner's keys, and keeping it would go on paying the
537
+ // name to someone the operator just took it from.
534
538
  putOperatorZapName(name, pubkey) {
535
539
  this.db
536
540
  .prepare(`INSERT INTO zap_names (name, pubkey, created_at, paid_msat, source) VALUES (?, ?, ?, 0, 'env')
537
- ON CONFLICT(name) DO UPDATE SET pubkey = excluded.pubkey, source = 'env'`)
541
+ ON CONFLICT(name) DO UPDATE SET
542
+ cx1 = CASE WHEN zap_names.pubkey = excluded.pubkey THEN zap_names.cx1 ELSE NULL END,
543
+ next_index = CASE WHEN zap_names.pubkey = excluded.pubkey THEN zap_names.next_index ELSE 0 END,
544
+ pubkey = excluded.pubkey,
545
+ source = 'env'`)
538
546
  .run(name.toLowerCase(), pubkey, Date.now());
539
547
  }
540
548
  // A self-service registration: burn the note that paid for the name and
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forgesworn/moneyer",
3
- "version": "0.13.1",
3
+ "version": "0.14.0",
4
4
  "description": "An LNURLcash (LUD-25) mint - strikes Lightning bearer notes. Independent implementation, cln/lnd funding sources, SQLite, zero HTTP framework.",
5
5
  "author": "TheCryptoDonkey",
6
6
  "license": "MIT",
@@ -60,7 +60,7 @@
60
60
  "@noble/hashes": "^2.3.0",
61
61
  "@scure/base": "^1.2.4",
62
62
  "farrier-kit": "^1.1.3",
63
- "lnurlcash-kit": "^0.10.0",
63
+ "lnurlcash-kit": "^0.13.0",
64
64
  "nostr-tools": "2.24.1"
65
65
  },
66
66
  "devDependencies": {
@@ -70,7 +70,7 @@
70
70
  "@types/node": "^24.0.0",
71
71
  "animejs": "^4.0.0",
72
72
  "happy-dom": "^20.0.0",
73
- "lnurlcash-conformance": "^0.8.0",
73
+ "lnurlcash-conformance": "^0.10.0",
74
74
  "playwright": "^1.62.1",
75
75
  "typescript": "^5.7.0",
76
76
  "uqr": "^0.1.2",