402-mcp 4.1.2 → 4.2.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.
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Payment requests are prefixed like a bech32 human-readable part so a bare
3
+ * string is self-describing, the way NUT-18 uses `creqA`.
4
+ */
5
+ const PREFIX = 'lnurlcashreq1';
6
+ /** Detects whether a 402 response carries an lnurlcash challenge. */
7
+ export function isLnurlcashChallenge(headers) {
8
+ const value = headers.get('x-lnurlcash');
9
+ return !!value && value.startsWith(PREFIX);
10
+ }
11
+ /** A whole number of sats that survives the conversion to msat everything downstream works in. */
12
+ function satsFrom(value) {
13
+ const n = typeof value === 'number'
14
+ ? value
15
+ : typeof value === 'string' && /^[0-9]+$/.test(value)
16
+ ? Number(value)
17
+ : NaN;
18
+ if (!Number.isSafeInteger(n) || n <= 0)
19
+ return null;
20
+ return Number.isSafeInteger(n * 1000) ? n : null;
21
+ }
22
+ function stringsFrom(value) {
23
+ if (!Array.isArray(value))
24
+ return null;
25
+ const out = value.filter((m) => typeof m === 'string' && m.length > 0);
26
+ return out.length > 0 ? out : null;
27
+ }
28
+ /**
29
+ * Parses an lnurlcash payment request from the X-LNURLcash header value:
30
+ * `lnurlcashreq1` followed by unpadded base64url of the request's RFC 8785
31
+ * canonical JSON.
32
+ *
33
+ * The request is `{ v, id, amount, currency, methodDetails: { mints } }`,
34
+ * with the amount a decimal STRING of whole sats. That is the shape the
35
+ * conformance vectors pin and `lnurlcash-kit` encodes.
36
+ *
37
+ * A shorter object, `{ a: number, u, m }`, briefly went out under the same
38
+ * prefix before the format settled. It is still read here, because a decoder
39
+ * that returns nothing on a string it can plainly understand helps no one,
40
+ * and because the kit's own decoder accepts it too. Nothing emits it.
41
+ *
42
+ * Only `sat` exists today; anything else returns null rather than guessing at
43
+ * a denomination.
44
+ */
45
+ export function parseLnurlcashChallenge(header) {
46
+ if (!header.startsWith(PREFIX))
47
+ return null;
48
+ try {
49
+ const b64 = header.slice(PREFIX.length);
50
+ const json = Buffer.from(b64, 'base64url').toString('utf-8');
51
+ const data = JSON.parse(json);
52
+ if (typeof data !== 'object' || data === null)
53
+ return null;
54
+ const details = data.methodDetails;
55
+ const longForm = data.amount !== undefined || data.v !== undefined;
56
+ const amount = satsFrom(longForm ? data.amount : data.a);
57
+ const currency = longForm ? data.currency : data.u;
58
+ const mints = stringsFrom(longForm
59
+ ? typeof details === 'object' && details !== null
60
+ ? details.mints
61
+ : undefined
62
+ : data.m);
63
+ if (amount === null || currency !== 'sat' || !mints)
64
+ return null;
65
+ return { amount, unit: 'sat', mints };
66
+ }
67
+ catch {
68
+ return null;
69
+ }
70
+ }
71
+ //# sourceMappingURL=parse.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse.js","sourceRoot":"","sources":["../../src/xlnurlcash/parse.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,37 @@
1
+ import type { LnurlcashNoteStore } from '../store/lnurlcash-notes.js';
2
+ import { type NoteOps, type NoteOpsOptions } from '../wallet/lnurlcash-ops.js';
3
+ import type { LnurlcashChallenge } from './parse.js';
4
+ export interface LnurlcashPaymentResult {
5
+ /** The bearer note URL to send in the X-LNURLcash header. */
6
+ header: string;
7
+ /** Amount in sats the note covers. */
8
+ amountSats: number;
9
+ /**
10
+ * Records what the paywall did with the note. `true` means it granted
11
+ * access, so it rotated the note and our secret is dead. `false` leaves the
12
+ * note held pending: only the mint can say whether it was rotated, and the
13
+ * next lnurlcash operation asks.
14
+ */
15
+ settle: (accepted: boolean) => void;
16
+ }
17
+ /**
18
+ * Mints arrive as bare hosts (`mint.example.com`), origins, or full discovery
19
+ * URLs. Compared on host so all three forms match a stored note's origin.
20
+ */
21
+ export declare function mintHost(value: string): string | null;
22
+ /**
23
+ * Builds a bearer-note payment for an lnurlcash challenge: an exact-value note
24
+ * from an accepted mint, or one split at the mint to the exact price so the
25
+ * paywall is never overpaid and the change stays in the store. Returns null
26
+ * when no note can cover it, so the caller falls through to another rail.
27
+ *
28
+ * The chosen note is held pending before it is returned. Handing a bearer note
29
+ * to a server puts it beyond our control: the server settles by rotating it,
30
+ * and only the mint knows whether that happened.
31
+ */
32
+ export declare function attemptLnurlcashPayment(opts: {
33
+ challenge: LnurlcashChallenge;
34
+ noteStore: LnurlcashNoteStore;
35
+ ops?: NoteOps;
36
+ options?: NoteOpsOptions;
37
+ }): Promise<LnurlcashPaymentResult | null>;
@@ -0,0 +1,102 @@
1
+ import { createNoteOps, noteUrl, NoteGone } from '../wallet/lnurlcash-ops.js';
2
+ /**
3
+ * Mints arrive as bare hosts (`mint.example.com`), origins, or full discovery
4
+ * URLs. Compared on host so all three forms match a stored note's origin.
5
+ */
6
+ export function mintHost(value) {
7
+ const trimmed = value.trim();
8
+ if (trimmed.length === 0)
9
+ return null;
10
+ const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed)
11
+ ? trimmed.replace(/^lnurlw:\/\//i, 'https://')
12
+ : `https://${trimmed}`;
13
+ try {
14
+ const host = new URL(withScheme).host;
15
+ return host.length > 0 ? host.toLowerCase() : null;
16
+ }
17
+ catch {
18
+ return null;
19
+ }
20
+ }
21
+ /**
22
+ * Builds a bearer-note payment for an lnurlcash challenge: an exact-value note
23
+ * from an accepted mint, or one split at the mint to the exact price so the
24
+ * paywall is never overpaid and the change stays in the store. Returns null
25
+ * when no note can cover it, so the caller falls through to another rail.
26
+ *
27
+ * The chosen note is held pending before it is returned. Handing a bearer note
28
+ * to a server puts it beyond our control: the server settles by rotating it,
29
+ * and only the mint knows whether that happened.
30
+ */
31
+ export async function attemptLnurlcashPayment(opts) {
32
+ const { challenge, noteStore } = opts;
33
+ const ops = opts.ops ?? createNoteOps(noteStore, opts.options ?? {});
34
+ const accepted = new Set(challenge.mints.map(mintHost).filter((h) => h !== null));
35
+ if (accepted.size === 0)
36
+ return null;
37
+ const needMsat = challenge.amount * 1000;
38
+ try {
39
+ await ops.reconcile();
40
+ }
41
+ catch { /* best effort; selection below simply sees fewer notes */ }
42
+ const live = noteStore.live().filter(n => {
43
+ const host = mintHost(n.mint);
44
+ return host !== null && accepted.has(host);
45
+ });
46
+ // Exact-value notes first: handing one over avoids a split round trip
47
+ // entirely. Then the smallest note that covers the price, so large notes
48
+ // stay whole.
49
+ const candidates = [
50
+ ...live.filter(n => n.amountMsat === needMsat),
51
+ ...live.filter(n => n.amountMsat > needMsat).sort((a, b) => a.amountMsat - b.amountMsat),
52
+ ];
53
+ for (const candidate of candidates) {
54
+ // The stored value is a cache. Only the mint knows what a note is worth
55
+ // now, and a bearer note anyone else holds a copy of may already be spent,
56
+ // so it is confirmed before it is handed over.
57
+ const probe = await ops.probeNote(candidate.mint, candidate.secret);
58
+ if (!probe.ok) {
59
+ // A mint that refuses the note has retired it: drop it and try the next.
60
+ // A mint that cannot be reached has said nothing, so nothing is dropped
61
+ // and no further note is risked against it.
62
+ if (probe.reason === 'unreachable')
63
+ return null;
64
+ noteStore.remove(candidate.secret);
65
+ continue;
66
+ }
67
+ if (probe.note.amountMsat !== candidate.amountMsat) {
68
+ noteStore.setAmount(candidate.secret, probe.note.amountMsat);
69
+ }
70
+ if (probe.note.amountMsat < needMsat)
71
+ continue;
72
+ const current = noteStore.find(candidate.secret);
73
+ if (!current)
74
+ continue;
75
+ let exact;
76
+ try {
77
+ exact = probe.note.amountMsat === needMsat
78
+ ? current
79
+ : await ops.split(current, needMsat);
80
+ }
81
+ catch (error) {
82
+ // A note the mint no longer recognises is normal for a bearer asset:
83
+ // drop it and try the next. Anything else means the mint is refusing or
84
+ // unreachable, so stop rather than burn more notes against it.
85
+ if (error instanceof NoteGone)
86
+ continue;
87
+ return null;
88
+ }
89
+ noteStore.setState(exact.secret, 'melting');
90
+ const secret = exact.secret;
91
+ return {
92
+ header: noteUrl(exact),
93
+ amountSats: challenge.amount,
94
+ settle(granted) {
95
+ if (granted)
96
+ noteStore.remove(secret);
97
+ },
98
+ };
99
+ }
100
+ return null;
101
+ }
102
+ //# sourceMappingURL=payment.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"payment.js","sourceRoot":"","sources":["../../src/xlnurlcash/payment.ts"],"names":[],"mappings":"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"}
package/llms-full.txt CHANGED
@@ -85,6 +85,11 @@ src/
85
85
  xcashu/
86
86
  parse.ts # Detect X-Cashu header; parse NUT-18 payment request
87
87
  payment.ts # Autonomous ecash payment: coin-select, send exact token amount
88
+ xlnurlcash/
89
+ parse.ts # Detect X-LNURLcash header; parse the lnurlcash payment request
90
+ # (v/id/amount-as-string/currency/methodDetails.mints, per the
91
+ # conformance vectors; the older short form is still read)
92
+ payment.ts # Autonomous bearer-note payment: pick or split a note to the exact price
88
93
  ietf-payment/
89
94
  parse.ts # Detect/parse WWW-Authenticate: Payment challenges (draft-ryan-httpauth-payment-01)
90
95
  credential.ts # Build Authorization: Payment base64url credential after Lightning payment
@@ -277,25 +282,27 @@ The L402 protocol is an extension of HTTP 402 (Payment Required):
277
282
 
278
283
  ## Supported 402 Protocol Variants
279
284
 
280
- `l402-fetch` detects and handles four HTTP 402 challenge variants. Detection order in source: L402, x402, xCashu, IETF Payment.
285
+ `l402-fetch` detects and handles five HTTP 402 challenge variants. Detection order in source: LNURLcash, x402, IETF Payment, L402, with xCashu tried immediately after LNURLcash.
281
286
 
282
287
  | Protocol | Detection | Credential sent | Autonomous? |
283
288
  |----------|-----------|----------------|-------------|
284
289
  | **L402** | `WWW-Authenticate: L402 macaroon=…, invoice=…` | `Authorization: L402 <macaroon>:<preimage>` | Yes (NWC or Cashu) |
285
290
  | **IETF Payment** (`draft-ryan-httpauth-payment-01`) | `WWW-Authenticate: Payment id=… method=lightning …` | `Authorization: Payment <base64url-json>` | Yes (NWC or Cashu) |
291
+ | **LNURLcash** (LUD-25) | `X-LNURLcash: lnurlcashreq1<base64url>` | `X-LNURLcash: <bearer note URL>` | Yes (note store only) |
286
292
  | **xCashu** (NUT-18) | `X-Cashu: creqA<base64url>` | `X-Cashu: <cashuB-token>` | Yes (Cashu wallet only) |
287
293
  | **x402** | `X-Payment-Required: x402` + JSON body | Human presents EIP-681 deeplink | No — on-chain EVM; surfaced to human |
288
294
 
289
- xCashu uses coin selection via `wallet.send(amount, proofs, { includeFees: true })` and restores change proofs to the token store. IETF Payment decodes the base64url charge request to extract the BOLT11 invoice, then pays it through the normal wallet stack and wraps the preimage in a JSON credential.
295
+ LNURLcash is tried before the others because a bearer note is already money in hand: the note URL goes straight into the retry header and the server settles it by rotating the note. A note worth exactly the price is used as it is; otherwise one is split at the mint and the change stays in the store. The chosen note is held pending until the outcome is known, so nothing else can spend it in the meantime. xCashu uses coin selection via `wallet.send(amount, proofs, { includeFees: true })` and restores change proofs to the token store. IETF Payment decodes the base64url charge request to extract the BOLT11 invoice, then pays it through the normal wallet stack and wraps the preimage in a JSON credential.
290
296
 
291
297
  ## Wallet Priority and Resolution
292
298
 
293
- Wallets are tried in order: NWC → Cashu → Human. The first wallet that succeeds is used.
299
+ Wallets are tried in order: NWC → Cashu → LNURLcash → Human. The first wallet that succeeds is used.
294
300
 
295
301
  | Wallet | Autonomy | Requirements | Speed |
296
302
  |--------|----------|-------------|-------|
297
303
  | NWC | Fully autonomous | `NWC_URI_FILE` pointing to a private 0600 bearer file | ~2-5 seconds |
298
304
  | Cashu | Fully autonomous | `CASHU_TOKENS` file with ecash tokens | ~3-8 seconds |
305
+ | LNURLcash | Fully autonomous | `LNURLCASH_NOTES` file with LUD-25 bearer notes | ~2-6 seconds |
299
306
  | Human | Requires human | None — always available as fallback | Minutes |
300
307
 
301
308
  The agent can force a specific method per-call using the `paymentMethod` parameter.
package/llms.txt CHANGED
@@ -120,10 +120,11 @@ Three payer methods are tried in priority order:
120
120
 
121
121
  The agent can override the payment method per-call, or you can configure only the methods you want available.
122
122
 
123
- `l402-fetch` handles four HTTP 402 challenge variants transparently:
123
+ `l402-fetch` handles five HTTP 402 challenge variants transparently:
124
124
 
125
125
  - **L402** (`WWW-Authenticate: L402`) — the primary protocol; Lightning invoice paid via the wallet stack.
126
126
  - **IETF Payment** (`WWW-Authenticate: Payment`, `draft-ryan-httpauth-payment-01`) — alternative Lightning path; builds a base64url credential after payment.
127
+ - **LNURLcash** (`X-LNURLcash: lnurlcashreq1…`, LUD-25): autonomous bearer-note payment; hands over a note worth exactly the price, splitting one at the mint if needed. Tried first: no Lightning hop and no swap at the mint.
127
128
  - **xCashu** (`X-Cashu: creqA…`, NUT-18) — autonomous ecash payment; selects and sends the exact token amount without a Lightning round-trip.
128
129
  - **x402** (`X-Payment-Required: x402`) — on-chain EVM payment; surfaces an EIP-681 deeplink to the human (no autonomous settlement).
129
130
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "402-mcp",
3
- "version": "4.1.2",
3
+ "version": "4.2.1",
4
4
  "mcpName": "dev.forgesworn/402-mcp",
5
5
  "description": "L402 + x402 client MCP - AI agents discover, pay for, and consume any payment-gated API",
6
6
  "type": "module",
@@ -14,6 +14,7 @@
14
14
  "./l402/*": "./build/l402/*.js",
15
15
  "./x402/*": "./build/x402/*.js",
16
16
  "./xcashu/*": "./build/xcashu/*.js",
17
+ "./xlnurlcash/*": "./build/xlnurlcash/*.js",
17
18
  "./ietf-payment/*": "./build/ietf-payment/*.js",
18
19
  "./store/*": "./build/store/*.js",
19
20
  "./fetch/*": "./build/fetch/*.js",
@@ -81,7 +82,10 @@
81
82
  "ai-agent",
82
83
  "autonomous-payments",
83
84
  "macaroon",
84
- "stablecoin"
85
+ "stablecoin",
86
+ "lnurlcash",
87
+ "lud-25",
88
+ "bearer-notes"
85
89
  ],
86
90
  "author": "forgesworn",
87
91
  "homepage": "https://github.com/forgesworn/402-mcp",