@getalby/lightning-tools 7.0.2 → 8.0.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.
Files changed (44) hide show
  1. package/README.md +108 -28
  2. package/dist/cjs/402/l402.cjs +55 -0
  3. package/dist/cjs/402/l402.cjs.map +1 -0
  4. package/dist/cjs/402/mpp.cjs +179 -0
  5. package/dist/cjs/402/mpp.cjs.map +1 -0
  6. package/dist/cjs/402/x402.cjs +1313 -0
  7. package/dist/cjs/402/x402.cjs.map +1 -0
  8. package/dist/cjs/402.cjs +1585 -0
  9. package/dist/cjs/402.cjs.map +1 -0
  10. package/dist/cjs/bolt11.cjs +8 -0
  11. package/dist/cjs/bolt11.cjs.map +1 -1
  12. package/dist/cjs/index.cjs +301 -53
  13. package/dist/cjs/index.cjs.map +1 -1
  14. package/dist/cjs/lnurl.cjs +8 -0
  15. package/dist/cjs/lnurl.cjs.map +1 -1
  16. package/dist/esm/402/l402.js +53 -0
  17. package/dist/esm/402/l402.js.map +1 -0
  18. package/dist/esm/402/mpp.js +177 -0
  19. package/dist/esm/402/mpp.js.map +1 -0
  20. package/dist/esm/402/x402.js +1311 -0
  21. package/dist/esm/402/x402.js.map +1 -0
  22. package/dist/esm/402.js +1579 -0
  23. package/dist/esm/402.js.map +1 -0
  24. package/dist/esm/bolt11.js +8 -0
  25. package/dist/esm/bolt11.js.map +1 -1
  26. package/dist/esm/index.js +298 -50
  27. package/dist/esm/index.js.map +1 -1
  28. package/dist/esm/lnurl.js +8 -0
  29. package/dist/esm/lnurl.js.map +1 -1
  30. package/dist/lightning-tools.umd.js +2 -2
  31. package/dist/lightning-tools.umd.js.map +1 -1
  32. package/dist/types/402/l402.d.ts +13 -0
  33. package/dist/types/402/mpp.d.ts +26 -0
  34. package/dist/types/402/x402.d.ts +13 -0
  35. package/dist/types/402.d.ts +41 -0
  36. package/dist/types/bolt11.d.ts +4 -0
  37. package/dist/types/index.d.ts +38 -28
  38. package/dist/types/lnurl.d.ts +2 -0
  39. package/package.json +20 -5
  40. package/dist/cjs/l402.cjs +0 -93
  41. package/dist/cjs/l402.cjs.map +0 -1
  42. package/dist/esm/l402.js +0 -87
  43. package/dist/esm/l402.js.map +0 -1
  44. package/dist/types/l402.d.ts +0 -35
package/README.md CHANGED
@@ -10,7 +10,7 @@ Before you start coding, look at example scenarios in our **[Developer Sandbox](
10
10
 
11
11
  ## 🤖 🚀 ⚡ For Developers using Agents / LLMs / Vibe Coding
12
12
 
13
- Skip the rest of this README and use the [Alby Bitcoin Payments Agent Skill](https://github.com/getAlby/alby-agent-skill) instead. It will handle the rest!
13
+ Skip the rest of this README and use the [Alby Bitcoin Builder Skill](https://github.com/getAlby/builder-skill) instead. It will handle the rest!
14
14
 
15
15
  ## Manual Installation
16
16
 
@@ -28,7 +28,7 @@ or for use without any build tools:
28
28
 
29
29
  ```html
30
30
  <script type="module">
31
- import { LightningAddress } from "https://esm.sh/@getalby/lightning-tools@5.0.0"; // jsdelivr.net, skypack.dev also work
31
+ import { LightningAddress } from "https://esm.sh/@getalby/lightning-tools@7"; // jsdelivr.net, skypack.dev also work
32
32
 
33
33
  // use LightningAddress normally...
34
34
  (async () => {
@@ -168,7 +168,45 @@ Native zaps without a browser extension are possible by using a Nostr Wallet Con
168
168
 
169
169
  See [examples/zaps-nwc](examples/zaps-nwc.js)
170
170
 
171
- ### L402
171
+ > All examples in the [examples/](examples/) directory are runnable. See [examples/README.md](examples/README.md) for setup instructions.
172
+
173
+ ### HTTP 402 - requesting HTTP resources that require a payment
174
+
175
+ L402, X402, MPP are protocol standards based on the HTTP 402 `Payment Required` code
176
+ for machine-to-machine payments. It is used to charge for HTTP API requests, tool calls, or agentic payments.
177
+
178
+ This library includes functions to consome those resources.
179
+
180
+ #### fetch402(url: string, fetchArgs, options)
181
+
182
+ `fetch402` is a single function that transparently handles L402 and X402 and MPP protected resources. Use it when you don't know or don't care which protocol the server uses — it will detect the protocol from the response headers and pay accordingly.
183
+
184
+ - url: the protected URL
185
+ - fetchArgs: arguments are passed to the underlying `fetch()` function used to do the HTTP request
186
+ - options:
187
+ - wallet: any object that implements `payInvoice({ invoice })` and returns `{ preimage }`. Used to pay L402, X402 and MPP invoices.
188
+
189
+ ##### Examples
190
+
191
+ ```js
192
+ import { fetch402 } from "@getalby/lightning-tools/402";
193
+ import { NWCClient } from "@getalby/sdk";
194
+
195
+ const nwc = new NWCClient({
196
+ nostrWalletConnectUrl: "nostr+walletconnect://...",
197
+ });
198
+
199
+ await fetch402(
200
+ "https://example.com/protected-resource",
201
+ {},
202
+ { wallet: nwc },
203
+ )
204
+ .then((res) => res.json())
205
+ .then(console.log)
206
+ .finally(() => nwc.close());
207
+ ```
208
+
209
+ #### L402
172
210
 
173
211
  L402 is a protocol standard based on the HTTP 402 Payment Required error code
174
212
  designed to support the use case of charging for services and
@@ -176,59 +214,101 @@ authenticating users in distributed networks.
176
214
 
177
215
  This library includes a `fetchWithL402` function to consume L402 protected resources.
178
216
 
179
- #### fetchWithL402(url: string, fetchArgs, options)
217
+ ##### fetchWithL402(url: string, fetchArgs, options)
180
218
 
181
219
  - url: the L402 protected URL
182
220
  - fetchArgs: arguments are passed to the underlying `fetch()` function used to do the HTTP request
183
221
  - options:
184
- - wallet: any object that implements `sendPayment(paymentRequest)` and returns `{ preimage }`. Used to pay the L402 invoice.
185
- - store: a key/value store object to persiste the l402 for each URL. The store must implement a `getItem()`/`setItem()` function as the browser's localStorage. By default a memory storage is used.
186
- - headerKey: defaults to L402 but if you need to consume an old LSAT API set this to LSAT
222
+ - wallet: any object (e.g. a NWC client) that implements `payInvoice({ invoice })` and returns `{ preimage }`. Used to pay the L402 invoice.
187
223
 
188
224
  ##### Examples
189
225
 
190
226
  ```js
191
- import { fetchWithL402 } from "@getalby/lightning-tools/l402";
227
+ import { fetchWithL402 } from "@getalby/lightning-tools/402/l402";
228
+ import { NWCClient } from "@getalby/sdk";
229
+
230
+ const nwc = new NWCClient({
231
+ nostrWalletConnectUrl: "nostr+walletconnect://...",
232
+ });
192
233
 
193
- // pass a wallet that implements sendPayment()
194
- // the tokens/preimage data will be stored in the browser's localStorage and used for any following request
195
234
  await fetchWithL402(
196
- "https://lsat-weather-api.getalby.repl.co/kigali",
235
+ "https://l402.example.com/protected-resource",
197
236
  {},
198
- { wallet: myWallet, store: window.localStorage },
237
+ { wallet: nwc },
199
238
  )
200
239
  .then((res) => res.json())
201
- .then(console.log);
240
+ .then(console.log)
241
+ .finally(() => nwc.close());
202
242
  ```
203
243
 
244
+ #### X402
245
+
246
+ Similar to L402 X402 is an open protocol for machine-to-machine payments built on the HTTP 402 Payment Required status code.
247
+ It enables APIs and resources to request payments inline, without prior registration or authentication.
248
+
249
+ This library includes a `fetchWithX402` function to consume X402-protected resources that support the lightning network.
250
+ (Note: X402 works also with other coins and network. This library supports X402 resources that accept Bitcoin on the lightning network)
251
+
252
+ ##### fetchWithX402(url: string, fetchArgs, options)
253
+
254
+ - url: the X402 protected URL
255
+ - fetchArgs: arguments are passed to the underlying `fetch()` function used to do the HTTP request
256
+ - options:
257
+ - wallet: any object (e.g. a NWC client) that implements `payInvoice({ invoice })` and returns `{ preimage }`. Used to pay the X402 invoice.
258
+
259
+ ##### Examples
260
+
204
261
  ```js
205
- import { fetchWithL402 } from "@getalby/lightning-tools/l402";
206
- import { NostrWebLNProvider } from "@getalby/sdk";
262
+ import { fetchWithX402 } from "@getalby/lightning-tools/402/x402";
263
+ import { NWCClient } from "@getalby/sdk";
207
264
 
208
- // use a NWC provider as the wallet to do the payments
209
- const nwc = new NostrWebLNProvider({
210
- nostrWalletConnectUrl: loadNWCUrl(),
265
+ const nwc = new NWCClient({
266
+ nostrWalletConnectUrl: "nostr+walletconnect://...",
211
267
  });
212
268
 
213
- // this will fetch the resource and pay the invoice using the NWC wallet
214
- await fetchWithL402(
215
- "https://lsat-weather-api.getalby.repl.co/kigali",
269
+ await fetchWithX402(
270
+ "https://x402.example.com/protected-resource",
216
271
  {},
217
272
  { wallet: nwc },
218
273
  )
219
274
  .then((res) => res.json())
220
- .then(console.log);
275
+ .then(console.log)
276
+ .finally(() => nwc.close());
221
277
  ```
222
278
 
279
+ #### MPP
280
+
281
+ MPP is an open protocol for machine-to-machine payments built on the HTTP 402 Payment Required status code.
282
+ Charge for API requests, tool calls, or content—agents and apps pay per request in the same HTTP call.
283
+
284
+ This library includes a `fetchWithMpp` function to consume MPP-protected resources that support the lightning network.
285
+ (Note: MPP works also with other payment methods. This library supports resources that accept Bitcoin on the lightning network)
286
+
287
+ ##### fetchWithMpp(url: string, fetchArgs, options)
288
+
289
+ - url: the MPP protected URL
290
+ - fetchArgs: arguments are passed to the underlying `fetch()` function used to do the HTTP request
291
+ - options:
292
+ - wallet: any object (e.g. a NWC client) that implements `payInvoice({ invoice })` and returns `{ preimage }`. Used to pay the X402 invoice.
293
+
294
+ ##### Examples
295
+
223
296
  ```js
224
- import { fetchWithL402, NoStorage } from "@getalby/lightning-tools/l402";
297
+ import { fetchWithMpp } from "@getalby/lightning-tools/402/mpp";
298
+ import { NWCClient } from "@getalby/sdk";
225
299
 
226
- // do not store the tokens
227
- await fetchWithL402(
228
- "https://lsat-weather-api.getalby.repl.co/kigali",
300
+ const nwc = new NWCClient({
301
+ nostrWalletConnectUrl: "nostr+walletconnect://...",
302
+ });
303
+
304
+ await fetchWithMpp(
305
+ "https://mpp.example.com/protected-resource",
229
306
  {},
230
- { store: new NoStorage() },
231
- );
307
+ { wallet: nwc },
308
+ )
309
+ .then((res) => res.json())
310
+ .then(console.log)
311
+ .finally(() => nwc.close());
232
312
  ```
233
313
 
234
314
  ### Basic invoice decoding
@@ -0,0 +1,55 @@
1
+ 'use strict';
2
+
3
+ const parseL402 = (input) => {
4
+ // Remove the L402 and LSAT identifiers
5
+ const string = input.replace("L402", "").replace("LSAT", "").trim();
6
+ // Initialize an object to store the key-value pairs
7
+ const keyValuePairs = {};
8
+ // Regular expression to match key and (quoted or unquoted) value
9
+ const regex = /(\w+)=("([^"]*)"|'([^']*)'|([^,]*))/g;
10
+ let match;
11
+ // Use regex to find all key-value pairs
12
+ while ((match = regex.exec(string)) !== null) {
13
+ // Key is always match[1]
14
+ // Value is either match[3] (double-quoted), match[4] (single-quoted), or match[5] (unquoted)
15
+ keyValuePairs[match[1]] = match[3] || match[4] || match[5];
16
+ }
17
+ return keyValuePairs;
18
+ };
19
+
20
+ const handleL402Payment = async (l402Header, url, fetchArgs, headers, wallet) => {
21
+ const details = parseL402(l402Header);
22
+ const token = details.token || details.macaroon;
23
+ const invoice = details.invoice;
24
+ if (!token) {
25
+ throw new Error("L402: missing token/macaroon in WWW-Authenticate header");
26
+ }
27
+ if (!invoice) {
28
+ throw new Error("L402: missing invoice in WWW-Authenticate header");
29
+ }
30
+ const invResp = await wallet.payInvoice({ invoice });
31
+ headers.set("Authorization", `L402 ${token}:${invResp.preimage}`);
32
+ return fetch(url, fetchArgs);
33
+ };
34
+ const fetchWithL402 = async (url, fetchArgs, options) => {
35
+ const wallet = options.wallet;
36
+ if (!wallet) {
37
+ throw new Error("wallet is missing");
38
+ }
39
+ if (!fetchArgs) {
40
+ fetchArgs = {};
41
+ }
42
+ fetchArgs.cache = "no-store";
43
+ fetchArgs.mode = "cors";
44
+ const headers = new Headers(fetchArgs.headers ?? undefined);
45
+ fetchArgs.headers = headers;
46
+ const initResp = await fetch(url, fetchArgs);
47
+ const header = initResp.headers.get("www-authenticate");
48
+ if (!header) {
49
+ return initResp;
50
+ }
51
+ return handleL402Payment(header, url, fetchArgs, headers, wallet);
52
+ };
53
+
54
+ exports.fetchWithL402 = fetchWithL402;
55
+ //# sourceMappingURL=l402.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"l402.cjs","sources":["../../../src/402/l402/utils.ts","../../../src/402/l402/l402.ts"],"sourcesContent":["export const parseL402 = (input: string): Record<string, string> => {\n // Remove the L402 and LSAT identifiers\n const string = input.replace(\"L402\", \"\").replace(\"LSAT\", \"\").trim();\n\n // Initialize an object to store the key-value pairs\n const keyValuePairs = {};\n\n // Regular expression to match key and (quoted or unquoted) value\n const regex = /(\\w+)=(\"([^\"]*)\"|'([^']*)'|([^,]*))/g;\n let match;\n\n // Use regex to find all key-value pairs\n while ((match = regex.exec(string)) !== null) {\n // Key is always match[1]\n // Value is either match[3] (double-quoted), match[4] (single-quoted), or match[5] (unquoted)\n keyValuePairs[match[1]] = match[3] || match[4] || match[5];\n }\n\n return keyValuePairs;\n};\n\nexport const makeL402AuthenticateHeader = (args: {\n macaroon?: string;\n token?: string;\n invoice: string;\n}) => {\n if (args.macaroon) {\n return `L402 version=\"0\" macaroon=\"${args.macaroon}\", invoice=\"${args.invoice}\"`;\n } else {\n return `L402 version=\"0\" token=\"${args.token}\", invoice=\"${args.invoice}\"`;\n }\n};\n","import { Wallet } from \"../utils\";\nimport { parseL402 } from \"./utils\";\n\nexport const handleL402Payment = async (\n l402Header: string,\n url: string,\n fetchArgs: RequestInit,\n headers: Headers,\n wallet: Wallet,\n): Promise<Response> => {\n const details = parseL402(l402Header);\n const token = details.token || details.macaroon;\n const invoice = details.invoice;\n\n if (!token) {\n throw new Error(\"L402: missing token/macaroon in WWW-Authenticate header\");\n }\n if (!invoice) {\n throw new Error(\"L402: missing invoice in WWW-Authenticate header\");\n }\n\n const invResp = await wallet.payInvoice({ invoice });\n headers.set(\"Authorization\", `L402 ${token}:${invResp.preimage}`);\n return fetch(url, fetchArgs);\n};\n\nexport const fetchWithL402 = async (\n url: string,\n fetchArgs: RequestInit,\n options: {\n wallet: Wallet;\n },\n) => {\n const wallet = options.wallet;\n if (!wallet) {\n throw new Error(\"wallet is missing\");\n }\n if (!fetchArgs) {\n fetchArgs = {};\n }\n fetchArgs.cache = \"no-store\";\n fetchArgs.mode = \"cors\";\n const headers = new Headers(fetchArgs.headers ?? undefined);\n fetchArgs.headers = headers;\n\n const initResp = await fetch(url, fetchArgs);\n const header = initResp.headers.get(\"www-authenticate\");\n if (!header) {\n return initResp;\n }\n\n return handleL402Payment(header, url, fetchArgs, headers, wallet);\n};\n"],"names":[],"mappings":";;AAAO,MAAM,SAAS,GAAG,CAAC,KAAa,KAA4B;;IAEjE,MAAM,MAAM,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE;;IAGnE,MAAM,aAAa,GAAG,EAAE;;IAGxB,MAAM,KAAK,GAAG,sCAAsC;AACpD,IAAA,IAAI,KAAK;;AAGT,IAAA,OAAO,CAAC,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,IAAI,EAAE;;;QAG5C,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC;IAC5D;AAEA,IAAA,OAAO,aAAa;AACtB,CAAC;;AChBM,MAAM,iBAAiB,GAAG,OAC/B,UAAkB,EAClB,GAAW,EACX,SAAsB,EACtB,OAAgB,EAChB,MAAc,KACO;AACrB,IAAA,MAAM,OAAO,GAAG,SAAS,CAAC,UAAU,CAAC;IACrC,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,OAAO,CAAC,QAAQ;AAC/C,IAAA,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO;IAE/B,IAAI,CAAC,KAAK,EAAE;AACV,QAAA,MAAM,IAAI,KAAK,CAAC,yDAAyD,CAAC;IAC5E;IACA,IAAI,CAAC,OAAO,EAAE;AACZ,QAAA,MAAM,IAAI,KAAK,CAAC,kDAAkD,CAAC;IACrE;IAEA,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,UAAU,CAAC,EAAE,OAAO,EAAE,CAAC;AACpD,IAAA,OAAO,CAAC,GAAG,CAAC,eAAe,EAAE,CAAA,KAAA,EAAQ,KAAK,CAAA,CAAA,EAAI,OAAO,CAAC,QAAQ,CAAA,CAAE,CAAC;AACjE,IAAA,OAAO,KAAK,CAAC,GAAG,EAAE,SAAS,CAAC;AAC9B,CAAC;AAEM,MAAM,aAAa,GAAG,OAC3B,GAAW,EACX,SAAsB,EACtB,OAEC,KACC;AACF,IAAA,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM;IAC7B,IAAI,CAAC,MAAM,EAAE;AACX,QAAA,MAAM,IAAI,KAAK,CAAC,mBAAmB,CAAC;IACtC;IACA,IAAI,CAAC,SAAS,EAAE;QACd,SAAS,GAAG,EAAE;IAChB;AACA,IAAA,SAAS,CAAC,KAAK,GAAG,UAAU;AAC5B,IAAA,SAAS,CAAC,IAAI,GAAG,MAAM;IACvB,MAAM,OAAO,GAAG,IAAI,OAAO,CAAC,SAAS,CAAC,OAAO,IAAI,SAAS,CAAC;AAC3D,IAAA,SAAS,CAAC,OAAO,GAAG,OAAO;IAE3B,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE,SAAS,CAAC;IAC5C,MAAM,MAAM,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC;IACvD,IAAI,CAAC,MAAM,EAAE;AACX,QAAA,OAAO,QAAQ;IACjB;AAEA,IAAA,OAAO,iBAAiB,CAAC,MAAM,EAAE,GAAG,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,CAAC;AACnE;;;;"}
@@ -0,0 +1,179 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Parse a `WWW-Authenticate: Payment …` header produced by a
5
+ * draft-lightning-charge-00 server. Expected format:
6
+ *
7
+ * Payment id="<id>", realm="<realm>", method="lightning",
8
+ * intent="charge", request="<base64url>" [, expires="<rfc3339>"]
9
+ *
10
+ * Returns null when the header is not a Payment lightning/charge challenge.
11
+ */
12
+ const parseMppChallenge = (header) => {
13
+ if (!header.trimStart().toLowerCase().startsWith("payment")) {
14
+ return null;
15
+ }
16
+ const rest = header
17
+ .slice(header.toLowerCase().indexOf("payment") + "payment".length)
18
+ .trim();
19
+ const result = {};
20
+ const regex = /(\w+)=("([^"]*)"|'([^']*)'|([^,\s]*))/g;
21
+ let match;
22
+ while ((match = regex.exec(rest)) !== null) {
23
+ result[match[1]] = match[3] ?? match[4] ?? match[5] ?? "";
24
+ }
25
+ if (result.method !== "lightning" ||
26
+ result.intent !== "charge" ||
27
+ !result.id ||
28
+ !result.realm ||
29
+ !result.request) {
30
+ return null;
31
+ }
32
+ return {
33
+ id: result.id,
34
+ realm: result.realm,
35
+ method: result.method,
36
+ intent: result.intent,
37
+ request: result.request,
38
+ ...(result.expires ? { expires: result.expires } : {}),
39
+ };
40
+ };
41
+ /** Decode a base64url string (no padding required) to a UTF-8 string. */
42
+ const decodeBase64url = (input) => {
43
+ const base64 = input.replace(/-/g, "+").replace(/_/g, "/");
44
+ const binary = atob(base64);
45
+ const bytes = new Uint8Array(binary.length);
46
+ for (let i = 0; i < binary.length; i++) {
47
+ bytes[i] = binary.charCodeAt(i);
48
+ }
49
+ return new TextDecoder("utf-8").decode(bytes);
50
+ };
51
+ /** Encode a UTF-8 string to base64url without padding. */
52
+ const encodeBase64url = (input) => {
53
+ const bytes = new TextEncoder().encode(input);
54
+ let binary = "";
55
+ for (let i = 0; i < bytes.length; i++) {
56
+ binary += String.fromCharCode(bytes[i]);
57
+ }
58
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
59
+ };
60
+ /**
61
+ * JSON Canonicalization Scheme (RFC 8785).
62
+ * Produces compact JSON with object keys sorted lexicographically.
63
+ */
64
+ const jcs = (value) => {
65
+ if (value === null || typeof value !== "object") {
66
+ return JSON.stringify(value);
67
+ }
68
+ if (Array.isArray(value)) {
69
+ return "[" + value.map(jcs).join(",") + "]";
70
+ }
71
+ const keys = Object.keys(value).sort();
72
+ return ("{" +
73
+ keys
74
+ .map((k) => JSON.stringify(k) + ":" + jcs(value[k]))
75
+ .join(",") +
76
+ "}");
77
+ };
78
+ /**
79
+ * Build the base64url-encoded credential token for the `Authorization` header.
80
+ *
81
+ * Per the spec the credential is a JCS-serialised JSON object that echoes all
82
+ * challenge auth-params (id, realm, method, intent, request, expires) and
83
+ * carries the HTLC preimage that proves payment:
84
+ *
85
+ * {
86
+ * "challenge": { "id": "…", "intent": "charge",
87
+ * "method": "lightning", "realm": "…", "request": "…" },
88
+ * "payload": { "preimage": "<64-char lowercase hex>" }
89
+ * }
90
+ *
91
+ * Keys are sorted lexicographically at every level per JCS.
92
+ */
93
+ const buildMppCredential = (challenge, preimage, source) => {
94
+ const challengeEcho = {
95
+ id: challenge.id,
96
+ intent: challenge.intent,
97
+ method: challenge.method,
98
+ realm: challenge.realm,
99
+ request: challenge.request,
100
+ };
101
+ if (challenge.expires) {
102
+ challengeEcho.expires = challenge.expires;
103
+ }
104
+ const credential = {
105
+ challenge: challengeEcho,
106
+ payload: { preimage },
107
+ };
108
+ return encodeBase64url(jcs(credential));
109
+ };
110
+
111
+ /**
112
+ * Handle a `WWW-Authenticate: Payment …` challenge produced by a
113
+ * draft-lightning-charge-00 server.
114
+ *
115
+ * Flow:
116
+ * 1. Parse the challenge from the header.
117
+ * 2. Decode the `request` auth-param to find the BOLT11 invoice.
118
+ * 3. Pay the invoice via the wallet; receive the HTLC preimage.
119
+ * 4. Build the `Authorization: Payment <credential>` header.
120
+ * 5. Retry the original request with the credential.
121
+ */
122
+ const handleMppChargePayment = async (wwwAuthHeader, url, fetchArgs, headers, wallet) => {
123
+ const challenge = parseMppChallenge(wwwAuthHeader);
124
+ if (!challenge) {
125
+ throw new Error("mpp: invalid or unsupported WWW-Authenticate challenge (expected Payment method=lightning intent=charge)");
126
+ }
127
+ let request;
128
+ try {
129
+ request = JSON.parse(decodeBase64url(challenge.request));
130
+ }
131
+ catch (_) {
132
+ throw new Error("mpp: invalid request auth-param (not valid base64url-encoded JSON)");
133
+ }
134
+ const invoice = request.methodDetails?.invoice;
135
+ if (!invoice) {
136
+ throw new Error("mpp: missing invoice in charge request");
137
+ }
138
+ const invResp = await wallet.payInvoice({ invoice });
139
+ // Per spec: Authorization: Payment <base64url-token> (single token, no wrapper)
140
+ const credential = buildMppCredential(challenge, invResp.preimage);
141
+ headers.set("Authorization", `Payment ${credential}`);
142
+ return fetch(url, fetchArgs);
143
+ };
144
+ /**
145
+ * Fetch a resource protected by the draft-lightning-charge-00 payment
146
+ * authentication protocol.
147
+ *
148
+ * On a `402 Payment Required` response that carries a
149
+ * `WWW-Authenticate: Payment method="lightning" intent="charge" …` header
150
+ * the function pays the embedded BOLT11 invoice and retries with the
151
+ * resulting preimage as the credential.
152
+ *
153
+ * Note: lightning-charge uses consume-once challenge semantics – each
154
+ * challenge embeds a fresh invoice, so paid credentials cannot be reused.
155
+ * The `store` option is accepted for API consistency but is not used.
156
+ */
157
+ const fetchWithMpp = async (url, fetchArgs, options) => {
158
+ const wallet = options.wallet;
159
+ if (!wallet) {
160
+ throw new Error("wallet is missing");
161
+ }
162
+ if (!fetchArgs) {
163
+ fetchArgs = {};
164
+ }
165
+ fetchArgs.cache = "no-store";
166
+ fetchArgs.mode = "cors";
167
+ const headers = new Headers(fetchArgs.headers ?? undefined);
168
+ fetchArgs.headers = headers;
169
+ const initResp = await fetch(url, fetchArgs);
170
+ const wwwAuthHeader = initResp.headers.get("www-authenticate");
171
+ if (!wwwAuthHeader ||
172
+ !wwwAuthHeader.trimStart().toLowerCase().startsWith("payment")) {
173
+ return initResp;
174
+ }
175
+ return handleMppChargePayment(wwwAuthHeader, url, fetchArgs, headers, wallet);
176
+ };
177
+
178
+ exports.fetchWithMpp = fetchWithMpp;
179
+ //# sourceMappingURL=mpp.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mpp.cjs","sources":["../../../src/402/mpp/utils.ts","../../../src/402/mpp/mpp.ts"],"sourcesContent":["export interface MppChallenge {\n id: string;\n realm: string;\n method: string;\n intent: string;\n request: string;\n expires?: string;\n}\n\nexport interface MppChargeRequest {\n amount: string;\n currency: string;\n description?: string;\n recipient?: string;\n externalId?: string;\n methodDetails: {\n invoice: string;\n paymentHash?: string;\n network?: string;\n };\n}\n\n/**\n * Parse a `WWW-Authenticate: Payment …` header produced by a\n * draft-lightning-charge-00 server. Expected format:\n *\n * Payment id=\"<id>\", realm=\"<realm>\", method=\"lightning\",\n * intent=\"charge\", request=\"<base64url>\" [, expires=\"<rfc3339>\"]\n *\n * Returns null when the header is not a Payment lightning/charge challenge.\n */\nexport const parseMppChallenge = (header: string): MppChallenge | null => {\n if (!header.trimStart().toLowerCase().startsWith(\"payment\")) {\n return null;\n }\n const rest = header\n .slice(header.toLowerCase().indexOf(\"payment\") + \"payment\".length)\n .trim();\n const result: Record<string, string> = {};\n const regex = /(\\w+)=(\"([^\"]*)\"|'([^']*)'|([^,\\s]*))/g;\n let match;\n while ((match = regex.exec(rest)) !== null) {\n result[match[1]] = match[3] ?? match[4] ?? match[5] ?? \"\";\n }\n\n if (\n result.method !== \"lightning\" ||\n result.intent !== \"charge\" ||\n !result.id ||\n !result.realm ||\n !result.request\n ) {\n return null;\n }\n\n return {\n id: result.id,\n realm: result.realm,\n method: result.method,\n intent: result.intent,\n request: result.request,\n ...(result.expires ? { expires: result.expires } : {}),\n };\n};\n\n/** Decode a base64url string (no padding required) to a UTF-8 string. */\nexport const decodeBase64url = (input: string): string => {\n const base64 = input.replace(/-/g, \"+\").replace(/_/g, \"/\");\n const binary = atob(base64);\n const bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) {\n bytes[i] = binary.charCodeAt(i);\n }\n return new TextDecoder(\"utf-8\").decode(bytes);\n};\n\n/** Encode a UTF-8 string to base64url without padding. */\nconst encodeBase64url = (input: string): string => {\n const bytes = new TextEncoder().encode(input);\n let binary = \"\";\n for (let i = 0; i < bytes.length; i++) {\n binary += String.fromCharCode(bytes[i]);\n }\n return btoa(binary).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=/g, \"\");\n};\n\n/**\n * JSON Canonicalization Scheme (RFC 8785).\n * Produces compact JSON with object keys sorted lexicographically.\n */\nconst jcs = (value: unknown): string => {\n if (value === null || typeof value !== \"object\") {\n return JSON.stringify(value);\n }\n if (Array.isArray(value)) {\n return \"[\" + (value as unknown[]).map(jcs).join(\",\") + \"]\";\n }\n const keys = Object.keys(value as object).sort();\n return (\n \"{\" +\n keys\n .map(\n (k) =>\n JSON.stringify(k) + \":\" + jcs((value as Record<string, unknown>)[k]),\n )\n .join(\",\") +\n \"}\"\n );\n};\n\n/**\n * Build the base64url-encoded credential token for the `Authorization` header.\n *\n * Per the spec the credential is a JCS-serialised JSON object that echoes all\n * challenge auth-params (id, realm, method, intent, request, expires) and\n * carries the HTLC preimage that proves payment:\n *\n * {\n * \"challenge\": { \"id\": \"…\", \"intent\": \"charge\",\n * \"method\": \"lightning\", \"realm\": \"…\", \"request\": \"…\" },\n * \"payload\": { \"preimage\": \"<64-char lowercase hex>\" }\n * }\n *\n * Keys are sorted lexicographically at every level per JCS.\n */\nexport const buildMppCredential = (\n challenge: MppChallenge,\n preimage: string,\n source?: string,\n): string => {\n const challengeEcho: Record<string, string> = {\n id: challenge.id,\n intent: challenge.intent,\n method: challenge.method,\n realm: challenge.realm,\n request: challenge.request,\n };\n if (challenge.expires) {\n challengeEcho.expires = challenge.expires;\n }\n\n const credential: Record<string, unknown> = {\n challenge: challengeEcho,\n payload: { preimage },\n };\n if (source) {\n credential.source = source;\n }\n\n return encodeBase64url(jcs(credential));\n};\n\n/**\n * Construct a `WWW-Authenticate` header for testing / server implementations.\n *\n * The auth scheme is `Payment` per [I-D.httpauth-payment].\n */\nexport const makeMppWwwAuthenticateHeader = (args: {\n id: string;\n realm: string;\n request: string;\n expires?: string;\n}): string => {\n let header =\n `Payment id=\"${args.id}\", realm=\"${args.realm}\", method=\"lightning\",` +\n ` intent=\"charge\", request=\"${args.request}\"`;\n if (args.expires) {\n header += `, expires=\"${args.expires}\"`;\n }\n return header;\n};\n\n/** Encode an MppChargeRequest as a base64url string suitable for the `request` auth-param. */\nexport const encodeMppChargeRequest = (request: MppChargeRequest): string =>\n encodeBase64url(jcs(request));\n","import { Wallet } from \"../utils\";\nimport {\n buildMppCredential,\n decodeBase64url,\n MppChargeRequest,\n parseMppChallenge,\n} from \"./utils\";\n\n/**\n * Handle a `WWW-Authenticate: Payment …` challenge produced by a\n * draft-lightning-charge-00 server.\n *\n * Flow:\n * 1. Parse the challenge from the header.\n * 2. Decode the `request` auth-param to find the BOLT11 invoice.\n * 3. Pay the invoice via the wallet; receive the HTLC preimage.\n * 4. Build the `Authorization: Payment <credential>` header.\n * 5. Retry the original request with the credential.\n */\nexport const handleMppChargePayment = async (\n wwwAuthHeader: string,\n url: string,\n fetchArgs: RequestInit,\n headers: Headers,\n wallet: Wallet,\n): Promise<Response> => {\n const challenge = parseMppChallenge(wwwAuthHeader);\n if (!challenge) {\n throw new Error(\n \"mpp: invalid or unsupported WWW-Authenticate challenge (expected Payment method=lightning intent=charge)\",\n );\n }\n\n let request: MppChargeRequest;\n try {\n request = JSON.parse(decodeBase64url(challenge.request));\n } catch (_) {\n throw new Error(\n \"mpp: invalid request auth-param (not valid base64url-encoded JSON)\",\n );\n }\n\n const invoice = request.methodDetails?.invoice;\n if (!invoice) {\n throw new Error(\"mpp: missing invoice in charge request\");\n }\n\n const invResp = await wallet.payInvoice({ invoice });\n\n // Per spec: Authorization: Payment <base64url-token> (single token, no wrapper)\n const credential = buildMppCredential(challenge, invResp.preimage);\n headers.set(\"Authorization\", `Payment ${credential}`);\n\n return fetch(url, fetchArgs);\n};\n\n/**\n * Fetch a resource protected by the draft-lightning-charge-00 payment\n * authentication protocol.\n *\n * On a `402 Payment Required` response that carries a\n * `WWW-Authenticate: Payment method=\"lightning\" intent=\"charge\" …` header\n * the function pays the embedded BOLT11 invoice and retries with the\n * resulting preimage as the credential.\n *\n * Note: lightning-charge uses consume-once challenge semantics – each\n * challenge embeds a fresh invoice, so paid credentials cannot be reused.\n * The `store` option is accepted for API consistency but is not used.\n */\nexport const fetchWithMpp = async (\n url: string,\n fetchArgs: RequestInit,\n options: { wallet: Wallet },\n): Promise<Response> => {\n const wallet = options.wallet;\n if (!wallet) {\n throw new Error(\"wallet is missing\");\n }\n if (!fetchArgs) {\n fetchArgs = {};\n }\n fetchArgs.cache = \"no-store\";\n fetchArgs.mode = \"cors\";\n const headers = new Headers(fetchArgs.headers ?? undefined);\n fetchArgs.headers = headers;\n\n const initResp = await fetch(url, fetchArgs);\n const wwwAuthHeader = initResp.headers.get(\"www-authenticate\");\n if (\n !wwwAuthHeader ||\n !wwwAuthHeader.trimStart().toLowerCase().startsWith(\"payment\")\n ) {\n return initResp;\n }\n\n return handleMppChargePayment(wwwAuthHeader, url, fetchArgs, headers, wallet);\n};\n"],"names":[],"mappings":"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"}