lnurlcash-conformance 0.2.1 → 0.2.3
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 +22 -0
- package/mock-mint/index.d.ts +12 -0
- package/mock-mint/index.mjs +20 -0
- package/package.json +1 -1
- package/runner/index.mjs +16 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,28 @@ Semantic versioning. While the LUD-25 draft is unmerged, `0.x` minor bumps
|
|
|
4
4
|
may add or tighten checks that a previously-passing mint now fails; pin an
|
|
5
5
|
exact version if you gate CI on the grade.
|
|
6
6
|
|
|
7
|
+
## 0.2.3 - 2026-08-22
|
|
8
|
+
|
|
9
|
+
- **New check: `keeps signatures off the informational endpoint`.** LUD-25
|
|
10
|
+
says signatures are only ever delivered in the `withdrawSuccessResponse`
|
|
11
|
+
of a rotate, split or merge, and that the informational endpoint never
|
|
12
|
+
returns one. Nothing graded it. A mint that hands one out there lets
|
|
13
|
+
anyone holding a note's public URL mint an offline certificate for it,
|
|
14
|
+
and invites a wallet to treat an online answer as offline proof. Both
|
|
15
|
+
known mints pass; the check exists so a third cannot quietly not.
|
|
16
|
+
|
|
17
|
+
## 0.2.2 - 2026-08-22
|
|
18
|
+
|
|
19
|
+
- The mock mint publishes `payLink` on a note's informational GET, as the
|
|
20
|
+
reference mint does. That is the way home for a holder who has nothing
|
|
21
|
+
but a note: without it a wallet that only ever received notes cannot
|
|
22
|
+
reach the document carrying the mint's retired signing keys, so a
|
|
23
|
+
correctly announced key rotation is indistinguishable from a substituted
|
|
24
|
+
key. `noteInfoPayLink: false` models a mint that does not publish it, and
|
|
25
|
+
`payLinkOffOrigin: true` is the misbehaviour where a mint points its
|
|
26
|
+
`payLink` at a different origin, nominating a third party to vouch for
|
|
27
|
+
its own key history - a client must ignore that.
|
|
28
|
+
|
|
7
29
|
## 0.2.1 - 2026-08-22
|
|
8
30
|
|
|
9
31
|
- The grader ships its own TypeScript declarations. The mock mint had them
|
package/mock-mint/index.d.ts
CHANGED
|
@@ -38,7 +38,19 @@ export interface MockMintOptions {
|
|
|
38
38
|
verify?: boolean
|
|
39
39
|
privateKey?: string
|
|
40
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Publish `payLink` on a note's informational GET - the way home for a
|
|
43
|
+
* holder who has nothing but the note. On by default, as the reference
|
|
44
|
+
* mint does it.
|
|
45
|
+
*/
|
|
46
|
+
noteInfoPayLink?: boolean
|
|
47
|
+
|
|
41
48
|
// ---- misbehaviour ----
|
|
49
|
+
/**
|
|
50
|
+
* Point `payLink` at a different origin, nominating a third party to
|
|
51
|
+
* vouch for this mint's key history. A client must ignore it.
|
|
52
|
+
*/
|
|
53
|
+
payLinkOffOrigin?: boolean
|
|
42
54
|
/** answer the informational GET with a k1 other than the one queried */
|
|
43
55
|
echoWrongK1?: boolean
|
|
44
56
|
/** report a maxWithdrawable that is not what the note is worth */
|
package/mock-mint/index.mjs
CHANGED
|
@@ -70,7 +70,14 @@ const DEFAULTS = {
|
|
|
70
70
|
// preimage it serves IS a bearer secret, so an operator needs a real
|
|
71
71
|
// off switch.
|
|
72
72
|
verify: true,
|
|
73
|
+
// Publish `payLink` on a note's informational GET, the way home for a
|
|
74
|
+
// holder who has nothing but the note. On by default because the
|
|
75
|
+
// reference mint does it; turn it off for a mint that does not.
|
|
76
|
+
noteInfoPayLink: true,
|
|
73
77
|
// ---- misbehaviour ----
|
|
78
|
+
// point `payLink` at a DIFFERENT origin, nominating a third party to
|
|
79
|
+
// vouch for this mint's key history. A client must ignore it.
|
|
80
|
+
payLinkOffOrigin: false,
|
|
74
81
|
// answer the informational GET with a k1 other than the one queried
|
|
75
82
|
echoWrongK1: false,
|
|
76
83
|
// report a maxWithdrawable that is not what the note is worth
|
|
@@ -673,6 +680,19 @@ export const createMockMint = async (options = {}) => {
|
|
|
673
680
|
minWithdrawable: 0,
|
|
674
681
|
maxWithdrawable: note.amountMsat + opts.lieAboutValue,
|
|
675
682
|
defaultDescription: 'an LNURLcash note',
|
|
683
|
+
// The way home, as the reference mint publishes it. A holder with
|
|
684
|
+
// nothing but a note can reach the document carrying this mint's
|
|
685
|
+
// terms and its retired signing keys; without it a wallet that only
|
|
686
|
+
// ever received notes cannot tell an announced key rotation from a
|
|
687
|
+
// substituted key, because the document lives under a username the
|
|
688
|
+
// note never mentions.
|
|
689
|
+
...(opts.noteInfoPayLink
|
|
690
|
+
? {
|
|
691
|
+
payLink: opts.payLinkOffOrigin
|
|
692
|
+
? 'https://elsewhere.example/.well-known/lnurlp/mint'
|
|
693
|
+
: `${origin}/.well-known/lnurlp/${opts.username}`
|
|
694
|
+
}
|
|
695
|
+
: {}),
|
|
676
696
|
mintPubkey: pubkey
|
|
677
697
|
})
|
|
678
698
|
}
|
package/package.json
CHANGED
package/runner/index.mjs
CHANGED
|
@@ -848,6 +848,22 @@ export const gradeNote = async (noteUrl, report, options = {}) => {
|
|
|
848
848
|
: `verified offline against a previous signing key (${signedBy.slice(0, 16)}...)`
|
|
849
849
|
})
|
|
850
850
|
|
|
851
|
+
await report.check('keeps signatures off the informational endpoint', async () => {
|
|
852
|
+
// LUD-25: "Signatures are only ever delivered in the
|
|
853
|
+
// withdrawSuccessResponse of a rotate, split or merge, the
|
|
854
|
+
// informational endpoint never returns one." A mint that hands one out
|
|
855
|
+
// here lets anyone holding only a note's PUBLIC url mint a certificate
|
|
856
|
+
// for it, and invites a wallet to treat the informational answer as an
|
|
857
|
+
// offline proof when it is an online one.
|
|
858
|
+
const here = new URL(url)
|
|
859
|
+
here.searchParams.set('k1', current)
|
|
860
|
+
const body = await get(here)
|
|
861
|
+
assert(body.status !== 'ERROR', `informational GET refused: ${body.reason}`)
|
|
862
|
+
assert(body.sig === undefined, 'the informational GET returned a sig')
|
|
863
|
+
assert(body.sig2 === undefined, 'the informational GET returned a sig2')
|
|
864
|
+
return 'no signature where the spec forbids one'
|
|
865
|
+
})
|
|
866
|
+
|
|
851
867
|
// The still-alive probe the three adversarial checks below share: a
|
|
852
868
|
// compliant refusal is ATOMIC, so the note it refused to touch must
|
|
853
869
|
// still be spendable afterwards.
|