@getalby/lightning-tools 8.1.1 → 8.2.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/dist/cjs/402.cjs CHANGED
@@ -1259,6 +1259,28 @@ class Invoice {
1259
1259
  }
1260
1260
  }
1261
1261
 
1262
+ /** Apply a previously-obtained credential to the outgoing request headers. */
1263
+ const applyCredentials = (headers, credentials) => {
1264
+ headers.set(credentials.header, credentials.value);
1265
+ };
1266
+ /** Attach payment metadata to a response and return it (typed). */
1267
+ const attachPayment = (response, payment) => {
1268
+ if (payment) {
1269
+ response.payment = payment;
1270
+ }
1271
+ return response;
1272
+ };
1273
+ /** Payment metadata describing a request authorized with a reused credential. */
1274
+ const reusedCredentialPayment = (credentials) => credentials ? { paid: false, amount: 0, credentials } : undefined;
1275
+ /** Satoshi amount of a BOLT11 invoice (0 when it cannot be decoded). */
1276
+ const getInvoiceAmount = (invoice) => {
1277
+ try {
1278
+ return new Invoice({ pr: invoice }).satoshi;
1279
+ }
1280
+ catch (_) {
1281
+ return 0;
1282
+ }
1283
+ };
1262
1284
  function createGuardedWallet(wallet, maxAmountSats) {
1263
1285
  return {
1264
1286
  payInvoice: async (args) => {
@@ -1310,6 +1332,9 @@ const handleL402Payment = async (l402Header, url, fetchArgs, headers, wallet) =>
1310
1332
  const details = parseL402(l402Header);
1311
1333
  const token = details.token || details.macaroon;
1312
1334
  const invoice = details.invoice;
1335
+ // Preserve the scheme the server challenged with (L402 or LSAT) so the
1336
+ // retry's Authorization header matches what the server expects.
1337
+ const scheme = /^\s*LSAT\b/i.test(l402Header) ? "LSAT" : "L402";
1313
1338
  if (!token) {
1314
1339
  throw new Error("L402: missing token/macaroon in WWW-Authenticate header");
1315
1340
  }
@@ -1317,8 +1342,16 @@ const handleL402Payment = async (l402Header, url, fetchArgs, headers, wallet) =>
1317
1342
  throw new Error("L402: missing invoice in WWW-Authenticate header");
1318
1343
  }
1319
1344
  const invResp = await wallet.payInvoice({ invoice });
1320
- headers.set("Authorization", `L402 ${token}:${invResp.preimage}`);
1321
- return fetch(url, fetchArgs);
1345
+ const value = `${scheme} ${token}:${invResp.preimage}`;
1346
+ headers.set("Authorization", value);
1347
+ const response = await fetch(url, fetchArgs);
1348
+ return attachPayment(response, {
1349
+ paid: true,
1350
+ amount: getInvoiceAmount(invoice),
1351
+ feesPaid: invResp.fees_paid,
1352
+ preimage: invResp.preimage,
1353
+ credentials: { header: "Authorization", value },
1354
+ });
1322
1355
  };
1323
1356
  const fetchWithL402 = async (url, fetchArgs, options) => {
1324
1357
  const wallet = options.wallet;
@@ -1332,6 +1365,15 @@ const fetchWithL402 = async (url, fetchArgs, options) => {
1332
1365
  fetchArgs.mode = "cors";
1333
1366
  const headers = new Headers(fetchArgs.headers ?? undefined);
1334
1367
  fetchArgs.headers = headers;
1368
+ // If the caller supplied a credential, we MUST use it and never pay again —
1369
+ // even if the server still responds with a 402. Re-paying here is the exact
1370
+ // double-charge this API exists to prevent; the caller decides what to do
1371
+ // with a rejected credential (retry after settlement, top up, etc.).
1372
+ if (options.credentials) {
1373
+ applyCredentials(headers, options.credentials);
1374
+ const reusedResp = await fetch(url, fetchArgs);
1375
+ return attachPayment(reusedResp, reusedCredentialPayment(options.credentials));
1376
+ }
1335
1377
  const initResp = await fetch(url, fetchArgs);
1336
1378
  const header = initResp.headers.get("www-authenticate");
1337
1379
  if (!header) {
@@ -1398,9 +1440,17 @@ const handleX402Payment = async (x402Header, url, fetchArgs, headers, wallet) =>
1398
1440
  if (invoice.amountRaw != requirements.amount) {
1399
1441
  throw new Error(`Invalid invoice amount: ${invoice.amountRaw}. expected ${requirements.amount}`);
1400
1442
  }
1401
- await wallet.payInvoice({ invoice: invoice.paymentRequest });
1402
- headers.set("payment-signature", buildX402PaymentSignature(requirements.scheme, requirements.network, invoice.paymentRequest, requirements));
1403
- return fetch(url, fetchArgs);
1443
+ const invResp = await wallet.payInvoice({ invoice: invoice.paymentRequest });
1444
+ const value = buildX402PaymentSignature(requirements.scheme, requirements.network, invoice.paymentRequest, requirements);
1445
+ headers.set("payment-signature", value);
1446
+ const response = await fetch(url, fetchArgs);
1447
+ return attachPayment(response, {
1448
+ paid: true,
1449
+ amount: invoice.satoshi,
1450
+ feesPaid: invResp.fees_paid,
1451
+ preimage: invResp.preimage,
1452
+ credentials: { header: "payment-signature", value },
1453
+ });
1404
1454
  };
1405
1455
  const fetchWithX402 = async (url, fetchArgs, options) => {
1406
1456
  const wallet = options.wallet;
@@ -1411,6 +1461,15 @@ const fetchWithX402 = async (url, fetchArgs, options) => {
1411
1461
  fetchArgs.mode = "cors";
1412
1462
  const headers = new Headers(fetchArgs.headers ?? undefined);
1413
1463
  fetchArgs.headers = headers;
1464
+ // If the caller supplied a credential, we MUST use it and never pay again —
1465
+ // even if the server still responds with a 402. Re-paying here is the exact
1466
+ // double-charge this API exists to prevent; the caller decides what to do
1467
+ // with a rejected credential (retry after settlement, top up, etc.).
1468
+ if (options.credentials) {
1469
+ applyCredentials(headers, options.credentials);
1470
+ const reusedResp = await fetch(url, fetchArgs);
1471
+ return attachPayment(reusedResp, reusedCredentialPayment(options.credentials));
1472
+ }
1414
1473
  const initResp = await fetch(url, fetchArgs);
1415
1474
  const header = initResp.headers.get("PAYMENT-REQUIRED");
1416
1475
  if (!header) {
@@ -1557,8 +1616,16 @@ const handleMppChargePayment = async (wwwAuthHeader, url, fetchArgs, headers, wa
1557
1616
  const invResp = await wallet.payInvoice({ invoice });
1558
1617
  // Per spec: Authorization: Payment <base64url-token> (single token, no wrapper)
1559
1618
  const credential = buildMppCredential(challenge, invResp.preimage);
1560
- headers.set("Authorization", `Payment ${credential}`);
1561
- return fetch(url, fetchArgs);
1619
+ const value = `Payment ${credential}`;
1620
+ headers.set("Authorization", value);
1621
+ const response = await fetch(url, fetchArgs);
1622
+ return attachPayment(response, {
1623
+ paid: true,
1624
+ amount: getInvoiceAmount(invoice),
1625
+ feesPaid: invResp.fees_paid,
1626
+ preimage: invResp.preimage,
1627
+ credentials: { header: "Authorization", value },
1628
+ });
1562
1629
  };
1563
1630
  /**
1564
1631
  * Fetch a resource protected by the draft-lightning-charge-00 payment
@@ -1569,9 +1636,11 @@ const handleMppChargePayment = async (wwwAuthHeader, url, fetchArgs, headers, wa
1569
1636
  * the function pays the embedded BOLT11 invoice and retries with the
1570
1637
  * resulting preimage as the credential.
1571
1638
  *
1572
- * Note: lightning-charge uses consume-once challenge semantics each
1573
- * challenge embeds a fresh invoice, so paid credentials cannot be reused.
1574
- * The `store` option is accepted for API consistency but is not used.
1639
+ * Pass a previous credential via `options.credentials` to reuse it (e.g. when
1640
+ * polling); the credential is applied and the function NEVER pays again, even
1641
+ * if the server still responds with a 402 (that response is returned as-is).
1642
+ * Note: lightning-charge typically uses consume-once challenge semantics, so a
1643
+ * reused credential is only accepted by servers that explicitly support it.
1575
1644
  */
1576
1645
  const fetchWithMpp = async (url, fetchArgs, options) => {
1577
1646
  const wallet = options.wallet;
@@ -1585,6 +1654,15 @@ const fetchWithMpp = async (url, fetchArgs, options) => {
1585
1654
  fetchArgs.mode = "cors";
1586
1655
  const headers = new Headers(fetchArgs.headers ?? undefined);
1587
1656
  fetchArgs.headers = headers;
1657
+ // If the caller supplied a credential, we MUST use it and never pay again —
1658
+ // even if the server still responds with a 402. Re-paying here is the exact
1659
+ // double-charge this API exists to prevent; the caller decides what to do
1660
+ // with a rejected credential (retry after settlement, top up, etc.).
1661
+ if (options.credentials) {
1662
+ applyCredentials(headers, options.credentials);
1663
+ const reusedResp = await fetch(url, fetchArgs);
1664
+ return attachPayment(reusedResp, reusedCredentialPayment(options.credentials));
1665
+ }
1588
1666
  const initResp = await fetch(url, fetchArgs);
1589
1667
  const wwwAuthHeader = initResp.headers.get("www-authenticate");
1590
1668
  if (!wwwAuthHeader ||
@@ -1605,6 +1683,15 @@ const fetch402 = async (url, fetchArgs, options) => {
1605
1683
  fetchArgs.mode = "cors";
1606
1684
  const headers = new Headers(fetchArgs.headers ?? undefined);
1607
1685
  fetchArgs.headers = headers;
1686
+ // If the caller supplied a credential, we MUST use it and never pay again —
1687
+ // even if the server still responds with a 402. Re-paying here is the exact
1688
+ // double-charge this API exists to prevent; the caller decides what to do
1689
+ // with a rejected credential (retry after settlement, top up, etc.).
1690
+ if (options.credentials) {
1691
+ applyCredentials(headers, options.credentials);
1692
+ const reusedResp = await fetch(url, fetchArgs);
1693
+ return attachPayment(reusedResp, reusedCredentialPayment(options.credentials));
1694
+ }
1608
1695
  const initResp = await fetch(url, fetchArgs);
1609
1696
  // L402 / LSAT: dedicated scheme, dispatch directly.
1610
1697
  const wwwAuthHeader = initResp.headers.get("www-authenticate");
@@ -1712,15 +1799,19 @@ function parseL402Authorization(input) {
1712
1799
  };
1713
1800
  }
1714
1801
 
1802
+ exports.applyCredentials = applyCredentials;
1803
+ exports.attachPayment = attachPayment;
1715
1804
  exports.createGuardedWallet = createGuardedWallet;
1716
1805
  exports.fetch402 = fetch402;
1717
1806
  exports.fetchWithL402 = fetchWithL402;
1718
1807
  exports.fetchWithMpp = fetchWithMpp;
1719
1808
  exports.fetchWithX402 = fetchWithX402;
1720
1809
  exports.findX402LightningRequirements = findX402LightningRequirements;
1810
+ exports.getInvoiceAmount = getInvoiceAmount;
1721
1811
  exports.issueL402Macaroon = issueL402Macaroon;
1722
1812
  exports.makeL402AuthenticateHeader = makeL402AuthenticateHeader;
1723
1813
  exports.parseL402 = parseL402;
1724
1814
  exports.parseL402Authorization = parseL402Authorization;
1815
+ exports.reusedCredentialPayment = reusedCredentialPayment;
1725
1816
  exports.verifyL402Macaroon = verifyL402Macaroon;
1726
1817
  //# sourceMappingURL=402.cjs.map