lnurlcash-kit 0.12.0 → 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 +33 -0
- package/README.md +18 -14
- package/dist/index.d.ts +1 -0
- package/dist/index.js +16 -9
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,38 @@
|
|
|
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
|
+
|
|
27
|
+
## 0.12.1 - 2026-09-11
|
|
28
|
+
|
|
29
|
+
- `fetchNoteInfo` compares the mint's echoed `k1` with the one it asked
|
|
30
|
+
about as the note each names (`noteIdOf`), not as strings. One Part 2 note
|
|
31
|
+
has many valid `ck1`s: anyone can turn one into its high-S twin, and its
|
|
32
|
+
holder can re-sign, so a mint echoing another spelling of the same note
|
|
33
|
+
was reported as having handed back a different note. An echo that names no
|
|
34
|
+
note at all is still refused, and a Part 1 secret compares as it always did.
|
|
35
|
+
|
|
3
36
|
## 0.12.0 - 2026-09-11
|
|
4
37
|
|
|
5
38
|
**A Part 2 branch rooted in a Nostr key.**
|
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
|
-
//
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
|
|
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
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
|
@@ -422,8 +425,9 @@ Three things worth knowing:
|
|
|
422
425
|
- **`i` is any uint32**, serialised as 4 bytes big-endian, never hardened.
|
|
423
426
|
lnurl-wallet and lnurl-mint agree on that; the spec does not say.
|
|
424
427
|
|
|
425
|
-
`
|
|
426
|
-
|
|
428
|
+
The tests grade against lnurlcash-conformance's `vectors/part2.json`, built
|
|
429
|
+
from the primitives there and identical to vectors generated from lnurl-wallet
|
|
430
|
+
and checked against lnurl-mint.
|
|
427
431
|
|
|
428
432
|
**A branch rooted in a Nostr key.** A holder with no BIP-39 words, such as a
|
|
429
433
|
hardware signer that keeps only its identity key, or a wallet that never made
|
|
@@ -431,7 +435,7 @@ any, can still be paid to keys of its own. `deriveNostrAddressNode(secretKey,
|
|
|
431
435
|
host)` takes the branch from the key that owns the lightning address:
|
|
432
436
|
`HMAC-SHA256(key = secret key, msg = "LNURLcash/nostr-seed")`, then the path
|
|
433
437
|
above unchanged. heartwood-esp32 derives exactly this on the device, graded
|
|
434
|
-
against the same `
|
|
438
|
+
against the same values as conformance's `vectors/nostr-seed.json`, so its notes come back from
|
|
435
439
|
its nsec (or the phrase the nsec came from) without the device. This is ours,
|
|
436
440
|
not LUD-25's; a mint sees an ordinary `cx1` either way.
|
|
437
441
|
|
package/dist/index.d.ts
CHANGED
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 ??
|
|
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,10 +965,11 @@ var fetchNoteInfo = async (url, options = {}) => {
|
|
|
964
965
|
}
|
|
965
966
|
assertWithdrawRequestShape(body, {
|
|
966
967
|
requireK1: true,
|
|
967
|
-
requireMintPubkey: opts.
|
|
968
|
+
requireMintPubkey: opts.requireMintPubkey
|
|
968
969
|
});
|
|
969
970
|
const queried = noteK1(url);
|
|
970
|
-
|
|
971
|
+
const echoed = noteIdOf(body.k1);
|
|
972
|
+
if (queried && (echoed === null || echoed !== noteIdOf(queried))) {
|
|
971
973
|
throw new ProtocolError(
|
|
972
974
|
"The service echoed back a different k1 than was queried - the note may have been redeemed elsewhere, or the service isn't spec-compliant."
|
|
973
975
|
);
|
|
@@ -990,7 +992,7 @@ var fetchNoteInfoByHash = async (withdrawLink, h, options = {}) => {
|
|
|
990
992
|
}
|
|
991
993
|
assertWithdrawRequestShape(body, {
|
|
992
994
|
requireK1: false,
|
|
993
|
-
requireMintPubkey: opts.
|
|
995
|
+
requireMintPubkey: opts.requireMintPubkey
|
|
994
996
|
});
|
|
995
997
|
const info = body;
|
|
996
998
|
const payLink = sameOriginPayLink(body.payLink, reqUrl);
|
|
@@ -1138,8 +1140,13 @@ var replayableCallbackRequest = async (callback, params, options) => {
|
|
|
1138
1140
|
}
|
|
1139
1141
|
throw lastError;
|
|
1140
1142
|
};
|
|
1141
|
-
var requireSignature = (value, options, what) => {
|
|
1143
|
+
var requireSignature = (value, options, what, output) => {
|
|
1142
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
|
+
}
|
|
1143
1150
|
if (!resolveOptions(options).requireSignatures) return void 0;
|
|
1144
1151
|
throw new UnverifiableNoteError(
|
|
1145
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.`
|
|
@@ -1172,7 +1179,7 @@ var rotateNoteWithHash = async (callback, k1, h, options = {}) => {
|
|
|
1172
1179
|
],
|
|
1173
1180
|
options
|
|
1174
1181
|
);
|
|
1175
|
-
return { signature: requireSignature(body.sig, options, "rotate") };
|
|
1182
|
+
return { signature: requireSignature(body.sig, options, "rotate", h) };
|
|
1176
1183
|
};
|
|
1177
1184
|
var splitNoteWithHash = async (callback, k1s, amountMsat, h, h2, options = {}) => {
|
|
1178
1185
|
const body = await replayableCallbackRequest(
|
|
@@ -1186,8 +1193,8 @@ var splitNoteWithHash = async (callback, k1s, amountMsat, h, h2, options = {}) =
|
|
|
1186
1193
|
options
|
|
1187
1194
|
);
|
|
1188
1195
|
return {
|
|
1189
|
-
signature: requireSignature(body.sig, options, "split"),
|
|
1190
|
-
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)
|
|
1191
1198
|
};
|
|
1192
1199
|
};
|
|
1193
1200
|
var mergeNotesWithHash = async (callback, k1s, h, options = {}) => {
|
|
@@ -1196,7 +1203,7 @@ var mergeNotesWithHash = async (callback, k1s, h, options = {}) => {
|
|
|
1196
1203
|
[...k1s.map((k1) => ["k1", k1]), outputParam(h, 1)],
|
|
1197
1204
|
options
|
|
1198
1205
|
);
|
|
1199
|
-
return { signature: requireSignature(body.sig, options, "merge") };
|
|
1206
|
+
return { signature: requireSignature(body.sig, options, "merge", h) };
|
|
1200
1207
|
};
|
|
1201
1208
|
var keepingOutputs = (err, newSecrets) => {
|
|
1202
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.
|
|
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.
|
|
63
|
+
"lnurlcash-conformance": "^0.10.0",
|
|
64
64
|
"tsup": "^8.5.0",
|
|
65
65
|
"typescript": "^5.7.0",
|
|
66
66
|
"vitest": "^3.0.0"
|