@forgesworn/moneyer 0.20.0 → 0.20.1

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,24 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.20.1 - 2026-10-13
4
+
5
+ - **A name with a cx1 issues invoices its payer's wallet will accept.** Its
6
+ payRequest metadata carries `text/cpub` with the next free index, but the
7
+ invoice's description hash was taken over the metadata without it, so it
8
+ never matched `sha256(metadata)` as LUD-06 requires. A wallet that checks
9
+ refused to pay the name by Lightning. The callback is now
10
+ `/z/cb/<name>/<index>`, the index the payRequest advertised, and the
11
+ invoice commits to exactly the metadata the payer read, even when a
12
+ settled payment has moved the next free index on in between. The index
13
+ feeds only the metadata: the note still lands on the next free key at
14
+ settlement. An index that is not a non-hardened one is refused. Zaps were
15
+ unaffected (their hash is over the zap request), and so is a name without
16
+ a cx1, whose callback stays `/z/cb/<name>`; that form still answers for
17
+ every name.
18
+ - The derivation, address proof and auto-mint text moved from LUD-25 to
19
+ LUD-26 in the draft (lnurl/luds 2f17578, editorial); the README and
20
+ comments now point there.
21
+
3
22
  ## 0.20.0 - 2026-10-07
4
23
 
5
24
  - **A zap paid while the mint was down is no longer lost.** The expiry
package/README.md CHANGED
@@ -584,7 +584,7 @@ A registered name resolves on both rails at once:
584
584
  The discovery endpoint advertises `namePriceMsat` while registration is
585
585
  open, so a wallet can offer the flow without asking.
586
586
 
587
- ### A name that pays to your own keys (LUD-25 auto-mint)
587
+ ### A name that pays to your own keys (LUD-26 auto-mint)
588
588
 
589
589
  Add `"cx1": "cx1..."` to the registration body, or send `{"name", "cx1"}`
590
590
  again later from the key that owns the name, and payments to it stop being
@@ -592,7 +592,7 @@ custodial. A `cx1` is a watch-only branch: from it the mint can work out each
592
592
  of the holder's note keys in turn, but never spend one.
593
593
 
594
594
  A `cx1` is public, so pointing a name at one has to be proven by the branch
595
- itself, as LUD-25 requires: the body also carries `"sig"`, a BIP-340
595
+ itself, as LUD-26 requires: the body also carries `"sig"`, a BIP-340
596
596
  signature by the branch's purpose-0 index-0 key over
597
597
  `sha256("LNURLcash:register:<domain>:<name>")`, `<domain>` being this mint's
598
598
  hostname. Changing the branch later is proven by the branch on file, not the
@@ -690,7 +690,7 @@ clearnet.
690
690
  | `/.well-known/lnurlp/<zap name>` | NIP-57 payRequest; paying mints a note *to the name's pubkey* |
691
691
  | `/.well-known/lnurlw/<user>` | LUD-25 mint address discovery (experimental) |
692
692
  | `/p/cb` | LUD-06 pay callback; issues the mint invoice for the note the `comment` names, a hash or a `cp1` key (`h` is an older spelling for a hash) |
693
- | `/z/cb/<zap name>` | the zap callback; validates the kind 9734 and issues the invoice |
693
+ | `/z/cb/<zap name>` | the zap callback; validates the kind 9734 and issues the invoice. A name with a `cx1` calls back at `/z/cb/<zap name>/<index>`, the index its payRequest advertised, so the invoice commits to exactly the metadata the payer was shown |
694
694
  | `/verify/<hash>` | LUD-21 verify, for mint invoices and melt payments |
695
695
  | `/w` | LUD-03 informational GET; accepts `k1` (a preimage, `ck1` or `cw1`, verified in full) or the non-disclosing LUD-25 `p` check (a bearer note's h or any `cp1`; `h` is its older name, still accepted); a live note carries `payLink` and its `cs1` as `sig` |
696
696
  | `/w/cb` | the mutating callback: melt, rotate, split, merge; inputs as preimages, `ck1`s or `cw1`s, outputs as `p1`/`p2` (a bearer note's h or a `cp1`), or their older names `h`/`h2` |
package/dist/names.js CHANGED
@@ -87,7 +87,7 @@ export const validateNip98 = (authorization, request) => {
87
87
  }
88
88
  return { pubkey: event.pubkey };
89
89
  };
90
- // LUD-25's address proof. A cx1 is public, so pointing a name at one, or
90
+ // LUD-26's address proof. A cx1 is public, so pointing a name at one, or
91
91
  // away from one, has to be proven by the branch itself: a BIP-340
92
92
  // signature by its index-0 key over sha256("LNURLcash:<action>:<domain>:
93
93
  // <name>"). The domain is this mint's own, so a proof one mint has seen
@@ -132,7 +132,7 @@ export const registerName = async (args) => {
132
132
  if ([...ALWAYS_RESERVED, ...args.reserved.map(name => name.toLowerCase())].includes(raw)) {
133
133
  return refuse('That name is reserved.', 403);
134
134
  }
135
- // LUD-25 Part 2: a watch-only branch, so payments to the name are minted
135
+ // LUD-26 auto-mint: a watch-only branch, so payments to the name are minted
136
136
  // to the holder's own keys. Absent leaves it as it is; null clears it.
137
137
  let cx1;
138
138
  if (args.body.cx1 === null) {
package/dist/server.js CHANGED
@@ -615,13 +615,14 @@ export const createMoneyer = async (config, deps = {}) => {
615
615
  if (lnurlpMatch && zap && zap.isZapName(lnurlpMatch[1])) {
616
616
  return send(zap.payRequest(lnurlpMatch[1]));
617
617
  }
618
- const zapCbMatch = requestUrl.pathname.match(/^\/z\/cb\/(.+)$/);
618
+ // `/z/cb/<name>`, or `/z/cb/<name>/<index>` for a name advertising a cx1.
619
+ const zapCbMatch = requestUrl.pathname.match(/^\/z\/cb\/([^/]+)(?:\/([^/]+))?$/);
619
620
  if (zapCbMatch) {
620
621
  if (!zap || !zap.isZapName(zapCbMatch[1]))
621
622
  return fail('Unknown user.', 404);
622
623
  if (config.sunset)
623
624
  return fail('This mint is sunsetting - minting is disabled.');
624
- const result = await zap.callback(zapCbMatch[1], Number(q.get('amount')), q.get('nostr'));
625
+ const result = await zap.callback(zapCbMatch[1], Number(q.get('amount')), q.get('nostr'), zapCbMatch[2] ?? null);
625
626
  if ('reason' in result)
626
627
  return fail(result.reason);
627
628
  return send({ ...result, disposable: false });
package/dist/zap.d.ts CHANGED
@@ -45,7 +45,7 @@ export type ZapBridge = {
45
45
  pubkey: string;
46
46
  isZapName(name: string): boolean;
47
47
  payRequest(name: string): Record<string, unknown> | null;
48
- callback(name: string, amountMsat: number, nostrParam: string | null): Promise<ZapCallbackResult>;
48
+ callback(name: string, amountMsat: number, nostrParam: string | null, advertisedIndex?: string | null): Promise<ZapCallbackResult>;
49
49
  settle(): Promise<number>;
50
50
  publish(): Promise<number>;
51
51
  sweep(nowMs?: number): Promise<number>;
package/dist/zap.js CHANGED
@@ -111,7 +111,7 @@ export const createZapBridge = (deps) => {
111
111
  const registered = store.zapName(lowered);
112
112
  if (!registered)
113
113
  return null;
114
- let internalTransfer;
114
+ let advertisedIndex;
115
115
  const branch = decodeCx1(registered.cx1 ?? '');
116
116
  if (branch) {
117
117
  let index = registered.nextIndex;
@@ -119,7 +119,7 @@ export const createZapBridge = (deps) => {
119
119
  try {
120
120
  const noteId = bytesToHex(deriveNotePubkey(branch.pubkeyXOnly, branch.chainCode, NOTE_PURPOSE_LIGHTNING_ADDRESS, index));
121
121
  if (!store.outputIdInUse(noteId)) {
122
- internalTransfer = `${registered.cx1}:${index}`;
122
+ advertisedIndex = index;
123
123
  break;
124
124
  }
125
125
  }
@@ -128,9 +128,15 @@ export const createZapBridge = (deps) => {
128
128
  }
129
129
  }
130
130
  }
131
+ const internalTransfer = advertisedIndex === undefined ? undefined : `${registered.cx1}:${advertisedIndex}`;
131
132
  return {
132
133
  tag: 'payRequest',
133
- callback: `${origin}/z/cb/${lowered}`,
134
+ // The index rides in the callback path so the invoice can commit to
135
+ // this exact metadata (LUD-06): by the time the payer calls back, a
136
+ // settled payment may have moved the next unused index on. A path
137
+ // segment rather than a query parameter, because some zap clients
138
+ // append `?amount=` without looking for an existing query.
139
+ callback: `${origin}/z/cb/${lowered}${advertisedIndex === undefined ? '' : `/${advertisedIndex}`}`,
134
140
  minSendable: deps.minSendableMsat,
135
141
  maxSendable: deps.maxSendableMsat,
136
142
  metadata: metadataFor(lowered, host, deps.mintFeeLine, deps.feeInWords, internalTransfer),
@@ -141,11 +147,16 @@ export const createZapBridge = (deps) => {
141
147
  // is one.
142
148
  };
143
149
  };
144
- const callback = async (name, amountMsat, nostrParam) => {
150
+ const callback = async (name, amountMsat, nostrParam, advertisedIndex = null) => {
145
151
  const lowered = name.toLowerCase();
146
- const recipient = store.zapName(lowered)?.pubkey;
147
- if (!recipient)
152
+ const registered = store.zapName(lowered);
153
+ const recipient = registered?.pubkey;
154
+ if (!registered || !recipient)
148
155
  return { reason: 'Unknown user.' };
156
+ // Below 2^31: a non-hardened index, as the derivation's ser32 needs.
157
+ if (advertisedIndex !== null && (!/^\d{1,10}$/.test(advertisedIndex) || Number(advertisedIndex) > 0x7fffffff)) {
158
+ return { reason: 'Invalid callback.' };
159
+ }
149
160
  if (!Number.isSafeInteger(amountMsat) || amountMsat <= 0)
150
161
  return { reason: 'Invalid amount.' };
151
162
  if (amountMsat < deps.minSendableMsat || amountMsat > deps.maxSendableMsat)
@@ -174,13 +185,21 @@ export const createZapBridge = (deps) => {
174
185
  // nothing to them. The note's secret is minted at settlement.
175
186
  const preimage = bytesToHex(randomBytes(32));
176
187
  const paymentHash = hashK1(preimage);
188
+ // A zap commits to its zap request (NIP-57); any other payment to the
189
+ // metadata the payer was shown (LUD-06), rebuilt from the index the
190
+ // payRequest advertised. That index is only a hint and feeds nothing
191
+ // but this string: settlement derives the note's key itself, so a
192
+ // forged one changes no more than the forger's own invoice. A cx1
193
+ // changed or cleared between the two requests still mismatches, and
194
+ // the payer's wallet fetches the payRequest again.
195
+ const internalTransfer = advertisedIndex !== null && registered.cx1 ? `${registered.cx1}:${Number(advertisedIndex)}` : undefined;
177
196
  let pr;
178
197
  try {
179
198
  pr = (await backend.createInvoice({
180
199
  amountMsat,
181
200
  preimageHex: preimage,
182
201
  memo: `Zap ${lowered}@${host}`,
183
- descriptionForHash: zapRequest ?? metadataFor(lowered, host, deps.mintFeeLine, deps.feeInWords)
202
+ descriptionForHash: zapRequest ?? metadataFor(lowered, host, deps.mintFeeLine, deps.feeInWords, internalTransfer)
184
203
  })).pr;
185
204
  }
186
205
  catch (err) {
package/llms.txt CHANGED
@@ -131,4 +131,7 @@ POST /names claim a name (NIP-98; the signer owns it); body {nam
131
131
  /w?p=<cp1>&c=<amount-bearing-cs1>&i=<index> plus an `i` tag. The index is
132
132
  taken at settlement, never at the invoice, and occupied keys are skipped
133
133
  Its payRequest metadata publishes text/cpub = cx1:<next-free-purpose-2-index> for
134
- same-mint rotate/split/merge transfers; a raced key returns "already in use"
134
+ same-mint rotate/split/merge transfers; a raced key returns "already in use".
135
+ Such a name's callback is /z/cb/{name}/{index}: the index it advertised, so a
136
+ non-zap invoice's description hash is sha256 of that exact metadata (a zap's
137
+ is of its kind 9734). The index feeds only the metadata, never the note's key
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forgesworn/moneyer",
3
- "version": "0.20.0",
3
+ "version": "0.20.1",
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",