@getalby/lightning-tools 8.1.0 → 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/README.md +78 -13
- package/dist/cjs/402/l402.cjs +1304 -2
- package/dist/cjs/402/l402.cjs.map +1 -1
- package/dist/cjs/402/mpp.cjs +1306 -5
- package/dist/cjs/402/mpp.cjs.map +1 -1
- package/dist/cjs/402/x402.cjs +73 -19
- package/dist/cjs/402/x402.cjs.map +1 -1
- package/dist/cjs/402.cjs +142 -19
- package/dist/cjs/402.cjs.map +1 -1
- package/dist/cjs/bip21.cjs +115 -0
- package/dist/cjs/bip21.cjs.map +1 -0
- package/dist/cjs/index.cjs +254 -19
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/esm/402/l402.js +1304 -2
- package/dist/esm/402/l402.js.map +1 -1
- package/dist/esm/402/mpp.js +1306 -5
- package/dist/esm/402/mpp.js.map +1 -1
- package/dist/esm/402/x402.js +73 -20
- package/dist/esm/402/x402.js.map +1 -1
- package/dist/esm/402.js +138 -20
- package/dist/esm/402.js.map +1 -1
- package/dist/esm/bip21.js +112 -0
- package/dist/esm/bip21.js.map +1 -0
- package/dist/esm/index.js +248 -20
- package/dist/esm/index.js.map +1 -1
- package/dist/lightning-tools.umd.js +2 -2
- package/dist/lightning-tools.umd.js.map +1 -1
- package/dist/types/402/l402.d.ts +49 -3
- package/dist/types/402/mpp.d.ts +54 -6
- package/dist/types/402/x402.d.ts +69 -4
- package/dist/types/402.d.ts +87 -17
- package/dist/types/bip21.d.ts +46 -0
- package/dist/types/index.d.ts +131 -17
- package/package.json +6 -1
package/dist/esm/402.js
CHANGED
|
@@ -1257,6 +1257,28 @@ class Invoice {
|
|
|
1257
1257
|
}
|
|
1258
1258
|
}
|
|
1259
1259
|
|
|
1260
|
+
/** Apply a previously-obtained credential to the outgoing request headers. */
|
|
1261
|
+
const applyCredentials = (headers, credentials) => {
|
|
1262
|
+
headers.set(credentials.header, credentials.value);
|
|
1263
|
+
};
|
|
1264
|
+
/** Attach payment metadata to a response and return it (typed). */
|
|
1265
|
+
const attachPayment = (response, payment) => {
|
|
1266
|
+
if (payment) {
|
|
1267
|
+
response.payment = payment;
|
|
1268
|
+
}
|
|
1269
|
+
return response;
|
|
1270
|
+
};
|
|
1271
|
+
/** Payment metadata describing a request authorized with a reused credential. */
|
|
1272
|
+
const reusedCredentialPayment = (credentials) => credentials ? { paid: false, amount: 0, credentials } : undefined;
|
|
1273
|
+
/** Satoshi amount of a BOLT11 invoice (0 when it cannot be decoded). */
|
|
1274
|
+
const getInvoiceAmount = (invoice) => {
|
|
1275
|
+
try {
|
|
1276
|
+
return new Invoice({ pr: invoice }).satoshi;
|
|
1277
|
+
}
|
|
1278
|
+
catch (_) {
|
|
1279
|
+
return 0;
|
|
1280
|
+
}
|
|
1281
|
+
};
|
|
1260
1282
|
function createGuardedWallet(wallet, maxAmountSats) {
|
|
1261
1283
|
return {
|
|
1262
1284
|
payInvoice: async (args) => {
|
|
@@ -1308,6 +1330,9 @@ const handleL402Payment = async (l402Header, url, fetchArgs, headers, wallet) =>
|
|
|
1308
1330
|
const details = parseL402(l402Header);
|
|
1309
1331
|
const token = details.token || details.macaroon;
|
|
1310
1332
|
const invoice = details.invoice;
|
|
1333
|
+
// Preserve the scheme the server challenged with (L402 or LSAT) so the
|
|
1334
|
+
// retry's Authorization header matches what the server expects.
|
|
1335
|
+
const scheme = /^\s*LSAT\b/i.test(l402Header) ? "LSAT" : "L402";
|
|
1311
1336
|
if (!token) {
|
|
1312
1337
|
throw new Error("L402: missing token/macaroon in WWW-Authenticate header");
|
|
1313
1338
|
}
|
|
@@ -1315,8 +1340,16 @@ const handleL402Payment = async (l402Header, url, fetchArgs, headers, wallet) =>
|
|
|
1315
1340
|
throw new Error("L402: missing invoice in WWW-Authenticate header");
|
|
1316
1341
|
}
|
|
1317
1342
|
const invResp = await wallet.payInvoice({ invoice });
|
|
1318
|
-
|
|
1319
|
-
|
|
1343
|
+
const value = `${scheme} ${token}:${invResp.preimage}`;
|
|
1344
|
+
headers.set("Authorization", value);
|
|
1345
|
+
const response = await fetch(url, fetchArgs);
|
|
1346
|
+
return attachPayment(response, {
|
|
1347
|
+
paid: true,
|
|
1348
|
+
amount: getInvoiceAmount(invoice),
|
|
1349
|
+
feesPaid: invResp.fees_paid,
|
|
1350
|
+
preimage: invResp.preimage,
|
|
1351
|
+
credentials: { header: "Authorization", value },
|
|
1352
|
+
});
|
|
1320
1353
|
};
|
|
1321
1354
|
const fetchWithL402 = async (url, fetchArgs, options) => {
|
|
1322
1355
|
const wallet = options.wallet;
|
|
@@ -1330,6 +1363,15 @@ const fetchWithL402 = async (url, fetchArgs, options) => {
|
|
|
1330
1363
|
fetchArgs.mode = "cors";
|
|
1331
1364
|
const headers = new Headers(fetchArgs.headers ?? undefined);
|
|
1332
1365
|
fetchArgs.headers = headers;
|
|
1366
|
+
// If the caller supplied a credential, we MUST use it and never pay again —
|
|
1367
|
+
// even if the server still responds with a 402. Re-paying here is the exact
|
|
1368
|
+
// double-charge this API exists to prevent; the caller decides what to do
|
|
1369
|
+
// with a rejected credential (retry after settlement, top up, etc.).
|
|
1370
|
+
if (options.credentials) {
|
|
1371
|
+
applyCredentials(headers, options.credentials);
|
|
1372
|
+
const reusedResp = await fetch(url, fetchArgs);
|
|
1373
|
+
return attachPayment(reusedResp, reusedCredentialPayment(options.credentials));
|
|
1374
|
+
}
|
|
1333
1375
|
const initResp = await fetch(url, fetchArgs);
|
|
1334
1376
|
const header = initResp.headers.get("www-authenticate");
|
|
1335
1377
|
if (!header) {
|
|
@@ -1350,7 +1392,7 @@ const buildX402PaymentSignature = (scheme, network, invoice, requirements) => {
|
|
|
1350
1392
|
return btoa(unescape(encodeURIComponent(json)));
|
|
1351
1393
|
};
|
|
1352
1394
|
|
|
1353
|
-
const
|
|
1395
|
+
const decodeX402Header = (x402Header) => {
|
|
1354
1396
|
let parsed;
|
|
1355
1397
|
try {
|
|
1356
1398
|
parsed = JSON.parse(decodeURIComponent(escape(atob(x402Header))));
|
|
@@ -1361,9 +1403,31 @@ const handleX402Payment = async (x402Header, url, fetchArgs, headers, wallet) =>
|
|
|
1361
1403
|
if (!Array.isArray(parsed.accepts) || parsed.accepts.length === 0) {
|
|
1362
1404
|
throw new Error("x402: PAYMENT-REQUIRED header contains no payment options");
|
|
1363
1405
|
}
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1406
|
+
return { accepts: parsed.accepts };
|
|
1407
|
+
};
|
|
1408
|
+
/**
|
|
1409
|
+
* Probe a PAYMENT-REQUIRED header for a lightning-payable offer without
|
|
1410
|
+
* throwing. Returns the matching requirements, or null if the header has no
|
|
1411
|
+
* lightning entry (e.g. USDC-only endpoints) or is malformed. Used by the
|
|
1412
|
+
* top-level fetch402 dispatcher to decide whether to attempt payment or hand
|
|
1413
|
+
* the 402 back to the caller.
|
|
1414
|
+
*/
|
|
1415
|
+
const findX402LightningRequirements = (x402Header) => {
|
|
1416
|
+
let accepts;
|
|
1417
|
+
try {
|
|
1418
|
+
({ accepts } = decodeX402Header(x402Header));
|
|
1419
|
+
}
|
|
1420
|
+
catch (_) {
|
|
1421
|
+
return null;
|
|
1422
|
+
}
|
|
1423
|
+
const requirements = accepts.find((e) => e?.extra?.paymentMethod === "lightning");
|
|
1424
|
+
if (!requirements?.extra?.invoice)
|
|
1425
|
+
return null;
|
|
1426
|
+
return requirements;
|
|
1427
|
+
};
|
|
1428
|
+
const handleX402Payment = async (x402Header, url, fetchArgs, headers, wallet) => {
|
|
1429
|
+
const { accepts } = decodeX402Header(x402Header);
|
|
1430
|
+
const requirements = accepts.find((e) => e?.extra?.paymentMethod === "lightning");
|
|
1367
1431
|
if (!requirements) {
|
|
1368
1432
|
throw new Error("x402: unsupported x402 network, only Bitcoin lightning network is supported.");
|
|
1369
1433
|
}
|
|
@@ -1374,9 +1438,17 @@ const handleX402Payment = async (x402Header, url, fetchArgs, headers, wallet) =>
|
|
|
1374
1438
|
if (invoice.amountRaw != requirements.amount) {
|
|
1375
1439
|
throw new Error(`Invalid invoice amount: ${invoice.amountRaw}. expected ${requirements.amount}`);
|
|
1376
1440
|
}
|
|
1377
|
-
await wallet.payInvoice({ invoice: invoice.paymentRequest });
|
|
1378
|
-
|
|
1379
|
-
|
|
1441
|
+
const invResp = await wallet.payInvoice({ invoice: invoice.paymentRequest });
|
|
1442
|
+
const value = buildX402PaymentSignature(requirements.scheme, requirements.network, invoice.paymentRequest, requirements);
|
|
1443
|
+
headers.set("payment-signature", value);
|
|
1444
|
+
const response = await fetch(url, fetchArgs);
|
|
1445
|
+
return attachPayment(response, {
|
|
1446
|
+
paid: true,
|
|
1447
|
+
amount: invoice.satoshi,
|
|
1448
|
+
feesPaid: invResp.fees_paid,
|
|
1449
|
+
preimage: invResp.preimage,
|
|
1450
|
+
credentials: { header: "payment-signature", value },
|
|
1451
|
+
});
|
|
1380
1452
|
};
|
|
1381
1453
|
const fetchWithX402 = async (url, fetchArgs, options) => {
|
|
1382
1454
|
const wallet = options.wallet;
|
|
@@ -1387,6 +1459,15 @@ const fetchWithX402 = async (url, fetchArgs, options) => {
|
|
|
1387
1459
|
fetchArgs.mode = "cors";
|
|
1388
1460
|
const headers = new Headers(fetchArgs.headers ?? undefined);
|
|
1389
1461
|
fetchArgs.headers = headers;
|
|
1462
|
+
// If the caller supplied a credential, we MUST use it and never pay again —
|
|
1463
|
+
// even if the server still responds with a 402. Re-paying here is the exact
|
|
1464
|
+
// double-charge this API exists to prevent; the caller decides what to do
|
|
1465
|
+
// with a rejected credential (retry after settlement, top up, etc.).
|
|
1466
|
+
if (options.credentials) {
|
|
1467
|
+
applyCredentials(headers, options.credentials);
|
|
1468
|
+
const reusedResp = await fetch(url, fetchArgs);
|
|
1469
|
+
return attachPayment(reusedResp, reusedCredentialPayment(options.credentials));
|
|
1470
|
+
}
|
|
1390
1471
|
const initResp = await fetch(url, fetchArgs);
|
|
1391
1472
|
const header = initResp.headers.get("PAYMENT-REQUIRED");
|
|
1392
1473
|
if (!header) {
|
|
@@ -1533,8 +1614,16 @@ const handleMppChargePayment = async (wwwAuthHeader, url, fetchArgs, headers, wa
|
|
|
1533
1614
|
const invResp = await wallet.payInvoice({ invoice });
|
|
1534
1615
|
// Per spec: Authorization: Payment <base64url-token> (single token, no wrapper)
|
|
1535
1616
|
const credential = buildMppCredential(challenge, invResp.preimage);
|
|
1536
|
-
|
|
1537
|
-
|
|
1617
|
+
const value = `Payment ${credential}`;
|
|
1618
|
+
headers.set("Authorization", value);
|
|
1619
|
+
const response = await fetch(url, fetchArgs);
|
|
1620
|
+
return attachPayment(response, {
|
|
1621
|
+
paid: true,
|
|
1622
|
+
amount: getInvoiceAmount(invoice),
|
|
1623
|
+
feesPaid: invResp.fees_paid,
|
|
1624
|
+
preimage: invResp.preimage,
|
|
1625
|
+
credentials: { header: "Authorization", value },
|
|
1626
|
+
});
|
|
1538
1627
|
};
|
|
1539
1628
|
/**
|
|
1540
1629
|
* Fetch a resource protected by the draft-lightning-charge-00 payment
|
|
@@ -1545,9 +1634,11 @@ const handleMppChargePayment = async (wwwAuthHeader, url, fetchArgs, headers, wa
|
|
|
1545
1634
|
* the function pays the embedded BOLT11 invoice and retries with the
|
|
1546
1635
|
* resulting preimage as the credential.
|
|
1547
1636
|
*
|
|
1548
|
-
*
|
|
1549
|
-
*
|
|
1550
|
-
*
|
|
1637
|
+
* Pass a previous credential via `options.credentials` to reuse it (e.g. when
|
|
1638
|
+
* polling); the credential is applied and the function NEVER pays again, even
|
|
1639
|
+
* if the server still responds with a 402 (that response is returned as-is).
|
|
1640
|
+
* Note: lightning-charge typically uses consume-once challenge semantics, so a
|
|
1641
|
+
* reused credential is only accepted by servers that explicitly support it.
|
|
1551
1642
|
*/
|
|
1552
1643
|
const fetchWithMpp = async (url, fetchArgs, options) => {
|
|
1553
1644
|
const wallet = options.wallet;
|
|
@@ -1561,6 +1652,15 @@ const fetchWithMpp = async (url, fetchArgs, options) => {
|
|
|
1561
1652
|
fetchArgs.mode = "cors";
|
|
1562
1653
|
const headers = new Headers(fetchArgs.headers ?? undefined);
|
|
1563
1654
|
fetchArgs.headers = headers;
|
|
1655
|
+
// If the caller supplied a credential, we MUST use it and never pay again —
|
|
1656
|
+
// even if the server still responds with a 402. Re-paying here is the exact
|
|
1657
|
+
// double-charge this API exists to prevent; the caller decides what to do
|
|
1658
|
+
// with a rejected credential (retry after settlement, top up, etc.).
|
|
1659
|
+
if (options.credentials) {
|
|
1660
|
+
applyCredentials(headers, options.credentials);
|
|
1661
|
+
const reusedResp = await fetch(url, fetchArgs);
|
|
1662
|
+
return attachPayment(reusedResp, reusedCredentialPayment(options.credentials));
|
|
1663
|
+
}
|
|
1564
1664
|
const initResp = await fetch(url, fetchArgs);
|
|
1565
1665
|
const wwwAuthHeader = initResp.headers.get("www-authenticate");
|
|
1566
1666
|
if (!wwwAuthHeader ||
|
|
@@ -1581,20 +1681,38 @@ const fetch402 = async (url, fetchArgs, options) => {
|
|
|
1581
1681
|
fetchArgs.mode = "cors";
|
|
1582
1682
|
const headers = new Headers(fetchArgs.headers ?? undefined);
|
|
1583
1683
|
fetchArgs.headers = headers;
|
|
1684
|
+
// If the caller supplied a credential, we MUST use it and never pay again —
|
|
1685
|
+
// even if the server still responds with a 402. Re-paying here is the exact
|
|
1686
|
+
// double-charge this API exists to prevent; the caller decides what to do
|
|
1687
|
+
// with a rejected credential (retry after settlement, top up, etc.).
|
|
1688
|
+
if (options.credentials) {
|
|
1689
|
+
applyCredentials(headers, options.credentials);
|
|
1690
|
+
const reusedResp = await fetch(url, fetchArgs);
|
|
1691
|
+
return attachPayment(reusedResp, reusedCredentialPayment(options.credentials));
|
|
1692
|
+
}
|
|
1584
1693
|
const initResp = await fetch(url, fetchArgs);
|
|
1694
|
+
// L402 / LSAT: dedicated scheme, dispatch directly.
|
|
1585
1695
|
const wwwAuthHeader = initResp.headers.get("www-authenticate");
|
|
1586
1696
|
if (wwwAuthHeader) {
|
|
1587
1697
|
const trimmed = wwwAuthHeader.trimStart().toLowerCase();
|
|
1588
|
-
if (trimmed.startsWith("payment")) {
|
|
1589
|
-
return handleMppChargePayment(wwwAuthHeader, url, fetchArgs, headers, wallet);
|
|
1590
|
-
}
|
|
1591
1698
|
if (trimmed.startsWith("l402") || trimmed.startsWith("lsat")) {
|
|
1592
1699
|
return handleL402Payment(wwwAuthHeader, url, fetchArgs, headers, wallet);
|
|
1593
1700
|
}
|
|
1594
|
-
throw new Error(`fetch402: unsupported WWW-Authenticate scheme: ${wwwAuthHeader}`);
|
|
1595
1701
|
}
|
|
1702
|
+
// A server may advertise multiple payment options at once (e.g. an MPP
|
|
1703
|
+
// USDC challenge in WWW-Authenticate alongside an x402 PAYMENT-REQUIRED
|
|
1704
|
+
// header that lists both USDC and lightning). Try each lightning-payable
|
|
1705
|
+
// handler in turn; only if none matches do we hand the original 402 back
|
|
1706
|
+
// to the caller so they can decide what to do with non-lightning offers.
|
|
1707
|
+
// 1. MPP-lightning challenge (Payment method="lightning" intent="charge").
|
|
1708
|
+
// parseMppChallenge returns null for any other method, which lets us
|
|
1709
|
+
// fall through to x402 instead of throwing.
|
|
1710
|
+
if (wwwAuthHeader && parseMppChallenge(wwwAuthHeader)) {
|
|
1711
|
+
return handleMppChargePayment(wwwAuthHeader, url, fetchArgs, headers, wallet);
|
|
1712
|
+
}
|
|
1713
|
+
// 2. x402 PAYMENT-REQUIRED with a lightning entry in `accepts`.
|
|
1596
1714
|
const x402Header = initResp.headers.get("PAYMENT-REQUIRED");
|
|
1597
|
-
if (x402Header) {
|
|
1715
|
+
if (x402Header && findX402LightningRequirements(x402Header)) {
|
|
1598
1716
|
return handleX402Payment(x402Header, url, fetchArgs, headers, wallet);
|
|
1599
1717
|
}
|
|
1600
1718
|
return initResp;
|
|
@@ -1679,5 +1797,5 @@ function parseL402Authorization(input) {
|
|
|
1679
1797
|
};
|
|
1680
1798
|
}
|
|
1681
1799
|
|
|
1682
|
-
export { createGuardedWallet, fetch402, fetchWithL402, fetchWithMpp, fetchWithX402, issueL402Macaroon, makeL402AuthenticateHeader, parseL402, parseL402Authorization, verifyL402Macaroon };
|
|
1800
|
+
export { applyCredentials, attachPayment, createGuardedWallet, fetch402, fetchWithL402, fetchWithMpp, fetchWithX402, findX402LightningRequirements, getInvoiceAmount, issueL402Macaroon, makeL402AuthenticateHeader, parseL402, parseL402Authorization, reusedCredentialPayment, verifyL402Macaroon };
|
|
1683
1801
|
//# sourceMappingURL=402.js.map
|