lnurlcash-kit 0.12.1 → 0.13.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,29 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.13.0 - 2026-09-11
4
+
5
+ **A plain note is unsigned.** LUD-25 Part 2 certifies `cp1` notes only, and
6
+ the reference mint and moneyer now answer a rotate, split or merge to a hash
7
+ output with a bare `{"status":"OK"}`. This library follows.
8
+
9
+ - `requireSignatures` now defaults to **false**. A hash output that comes
10
+ back unsigned is the spec, not a fault: `signature` (and `changeSignature`)
11
+ is undefined, and no `UnverifiableNoteError` is raised. Set it true to
12
+ keep demanding the old Part 1 signature over the hash.
13
+ - A `cp1` output is owed its `cs1` certificate regardless of the option: a
14
+ rotate, split or merge naming one that comes back without `sig` (or
15
+ `sig2` for a `cp1` change) raises `UnverifiableNoteError`, carrying the
16
+ fresh secrets as before.
17
+ - New option `requireMintPubkey`, default true, takes over the
18
+ `withdrawRequest` check that `requireSignatures` used to carry. A Part
19
+ 1-only mint that publishes no `mintPubkey` is admitted with it set false.
20
+ - Graded by lnurlcash-conformance 0.10.0's response vectors, which now
21
+ carry `output`/`change: "cp1"` on the cases that mint a `cp1` note.
22
+
23
+ If you relied on the default to refuse unsigned plain notes, set
24
+ `requireSignatures: true`; if you only ever wanted verifiable notes, hold
25
+ `cp1` notes, which are the only kind the spec makes verifiable.
26
+
3
27
  ## 0.12.1 - 2026-09-11
4
28
 
5
29
  - `fetchNoteInfo` compares the mint's echoed `k1` with the one it asked
package/README.md CHANGED
@@ -58,14 +58,14 @@ console.log(info.maxWithdrawable, 'msat')
58
58
  // that GET put the secret on the wire, so rotate it
59
59
  const fresh = await rotateNote(info.callback, info.k1)
60
60
 
61
- // and check the mint really issued it, without asking anyone. Both fields
62
- // are guaranteed here: LUD-25 requires the mint to publish mintPubkey and
63
- // to sign what it mints, and this library refuses a mint that does neither.
64
- verifyNoteSignature(fresh.k1, info.maxWithdrawable, fresh.signature!, info.mintPubkey)
61
+ // A plain note comes back unsigned: LUD-25 Part 2 certifies cp1 notes
62
+ // only, and a hash has nothing to attest to without disclosing the secret.
63
+ // To hold something a recipient can check offline, rotate into a cp1 key
64
+ // (see Part 2 below) and verify its cs1 against info.mintPubkey.
65
65
  ```
66
66
 
67
67
  Every request function takes options last — `fetch`, `timeoutMs`, `offline`,
68
- `randomSecret`, `requireSignatures`, `mutationRetries`.
68
+ `randomSecret`, `requireSignatures`, `requireMintPubkey`, `mutationRetries`.
69
69
  `createClient(options)` binds one set once:
70
70
 
71
71
  ```ts
@@ -189,12 +189,15 @@ sig = 65 bytes, r || s || recovery_id
189
189
  ```
190
190
 
191
191
  A `withdrawRequest` publishing no `mintPubkey`, or one that is not a 33-byte
192
- compressed secp256k1 key, is refused with a `ProtocolError`. A mutation the
193
- service confirms but does not sign raises `UnverifiableNoteError` — which
194
- **carries the fresh secrets**, because the mutation landed and the note it
195
- minted is real; read them with `newSecretsOf` and persist them before
196
- anything else. Pass `requireSignatures: false` to deal with a mint that
197
- predates the requirement.
192
+ compressed secp256k1 key, is refused with a `ProtocolError`; pass
193
+ `requireMintPubkey: false` for a Part 1-only mint that publishes none. A
194
+ mutation to a `cp1` output the service confirms but does not certify raises
195
+ `UnverifiableNoteError` — which **carries the fresh secrets**, because the
196
+ mutation landed and the note it minted is real; read them with
197
+ `newSecretsOf` and persist them before anything else. A mutation to a plain
198
+ hash output is unsigned by design and comes back with `signature`
199
+ undefined; pass `requireSignatures: true` to demand the old Part 1
200
+ signature over the hash instead, as mints issued before the Part 2 rewrite.
198
201
 
199
202
  `verifyNoteSignature` recovers the pubkey and compares it to `mintPubkey`.
200
203
  It accepts the recovery id at either end, because lnurl-mint once emitted
package/dist/index.d.ts CHANGED
@@ -14,6 +14,7 @@ type LnurlcashOptions = {
14
14
  offline?: boolean;
15
15
  randomSecret?: RandomSecret;
16
16
  requireSignatures?: boolean;
17
+ requireMintPubkey?: boolean;
17
18
  mutationRetries?: number;
18
19
  };
19
20
 
package/dist/index.js CHANGED
@@ -690,7 +690,8 @@ var resolveOptions = (options = {}) => ({
690
690
  timeoutMs: options.timeoutMs ?? 3e4,
691
691
  offline: options.offline ?? false,
692
692
  randomSecret: options.randomSecret ?? defaultRandomSecret,
693
- requireSignatures: options.requireSignatures ?? true,
693
+ requireSignatures: options.requireSignatures ?? false,
694
+ requireMintPubkey: options.requireMintPubkey ?? true,
694
695
  // A negative or non-finite count is read as none rather than thrown on:
695
696
  // this is a resilience knob, and refusing the whole operation over it
696
697
  // would be a worse answer than not retrying.
@@ -964,7 +965,7 @@ var fetchNoteInfo = async (url, options = {}) => {
964
965
  }
965
966
  assertWithdrawRequestShape(body, {
966
967
  requireK1: true,
967
- requireMintPubkey: opts.requireSignatures
968
+ requireMintPubkey: opts.requireMintPubkey
968
969
  });
969
970
  const queried = noteK1(url);
970
971
  const echoed = noteIdOf(body.k1);
@@ -991,7 +992,7 @@ var fetchNoteInfoByHash = async (withdrawLink, h, options = {}) => {
991
992
  }
992
993
  assertWithdrawRequestShape(body, {
993
994
  requireK1: false,
994
- requireMintPubkey: opts.requireSignatures
995
+ requireMintPubkey: opts.requireMintPubkey
995
996
  });
996
997
  const info = body;
997
998
  const payLink = sameOriginPayLink(body.payLink, reqUrl);
@@ -1139,8 +1140,13 @@ var replayableCallbackRequest = async (callback, params, options) => {
1139
1140
  }
1140
1141
  throw lastError;
1141
1142
  };
1142
- var requireSignature = (value, options, what) => {
1143
+ var requireSignature = (value, options, what, output) => {
1143
1144
  if (typeof value === "string" && value.length > 0) return value;
1145
+ if (isCp1(output)) {
1146
+ throw new UnverifiableNoteError(
1147
+ `The service confirmed the ${what} to a cp1 output but returned no cs1 certificate, which LUD-25 Part 2 requires, so the note it just minted cannot be verified offline. The note exists - keep the key.`
1148
+ );
1149
+ }
1144
1150
  if (!resolveOptions(options).requireSignatures) return void 0;
1145
1151
  throw new UnverifiableNoteError(
1146
1152
  `The service confirmed the ${what} but returned no signature, so the note it just minted cannot be verified offline. The note exists - keep the secret.`
@@ -1173,7 +1179,7 @@ var rotateNoteWithHash = async (callback, k1, h, options = {}) => {
1173
1179
  ],
1174
1180
  options
1175
1181
  );
1176
- return { signature: requireSignature(body.sig, options, "rotate") };
1182
+ return { signature: requireSignature(body.sig, options, "rotate", h) };
1177
1183
  };
1178
1184
  var splitNoteWithHash = async (callback, k1s, amountMsat, h, h2, options = {}) => {
1179
1185
  const body = await replayableCallbackRequest(
@@ -1187,8 +1193,8 @@ var splitNoteWithHash = async (callback, k1s, amountMsat, h, h2, options = {}) =
1187
1193
  options
1188
1194
  );
1189
1195
  return {
1190
- signature: requireSignature(body.sig, options, "split"),
1191
- changeSignature: requireSignature(body.sig2, options, "split's change")
1196
+ signature: requireSignature(body.sig, options, "split", h),
1197
+ changeSignature: requireSignature(body.sig2, options, "split's change", h2)
1192
1198
  };
1193
1199
  };
1194
1200
  var mergeNotesWithHash = async (callback, k1s, h, options = {}) => {
@@ -1197,7 +1203,7 @@ var mergeNotesWithHash = async (callback, k1s, h, options = {}) => {
1197
1203
  [...k1s.map((k1) => ["k1", k1]), outputParam(h, 1)],
1198
1204
  options
1199
1205
  );
1200
- return { signature: requireSignature(body.sig, options, "merge") };
1206
+ return { signature: requireSignature(body.sig, options, "merge", h) };
1201
1207
  };
1202
1208
  var keepingOutputs = (err, newSecrets) => {
1203
1209
  if (err instanceof NoteSpentError || err instanceof NoteUnknownError) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lnurlcash-kit",
3
- "version": "0.12.1",
3
+ "version": "0.13.0",
4
4
  "description": "LNURLcash (LUD-25) bearer note client for TypeScript - mint, rotate, split, merge, melt, and verify offline",
5
5
  "author": "TheCryptoDonkey",
6
6
  "license": "MIT",
@@ -60,7 +60,7 @@
60
60
  },
61
61
  "devDependencies": {
62
62
  "@types/node": "^26.2.0",
63
- "lnurlcash-conformance": "^0.9.0",
63
+ "lnurlcash-conformance": "^0.10.0",
64
64
  "tsup": "^8.5.0",
65
65
  "typescript": "^5.7.0",
66
66
  "vitest": "^3.0.0"