@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/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
|
-
|
|
1321
|
-
|
|
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) {
|
|
@@ -1352,7 +1394,7 @@ const buildX402PaymentSignature = (scheme, network, invoice, requirements) => {
|
|
|
1352
1394
|
return btoa(unescape(encodeURIComponent(json)));
|
|
1353
1395
|
};
|
|
1354
1396
|
|
|
1355
|
-
const
|
|
1397
|
+
const decodeX402Header = (x402Header) => {
|
|
1356
1398
|
let parsed;
|
|
1357
1399
|
try {
|
|
1358
1400
|
parsed = JSON.parse(decodeURIComponent(escape(atob(x402Header))));
|
|
@@ -1363,9 +1405,31 @@ const handleX402Payment = async (x402Header, url, fetchArgs, headers, wallet) =>
|
|
|
1363
1405
|
if (!Array.isArray(parsed.accepts) || parsed.accepts.length === 0) {
|
|
1364
1406
|
throw new Error("x402: PAYMENT-REQUIRED header contains no payment options");
|
|
1365
1407
|
}
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1408
|
+
return { accepts: parsed.accepts };
|
|
1409
|
+
};
|
|
1410
|
+
/**
|
|
1411
|
+
* Probe a PAYMENT-REQUIRED header for a lightning-payable offer without
|
|
1412
|
+
* throwing. Returns the matching requirements, or null if the header has no
|
|
1413
|
+
* lightning entry (e.g. USDC-only endpoints) or is malformed. Used by the
|
|
1414
|
+
* top-level fetch402 dispatcher to decide whether to attempt payment or hand
|
|
1415
|
+
* the 402 back to the caller.
|
|
1416
|
+
*/
|
|
1417
|
+
const findX402LightningRequirements = (x402Header) => {
|
|
1418
|
+
let accepts;
|
|
1419
|
+
try {
|
|
1420
|
+
({ accepts } = decodeX402Header(x402Header));
|
|
1421
|
+
}
|
|
1422
|
+
catch (_) {
|
|
1423
|
+
return null;
|
|
1424
|
+
}
|
|
1425
|
+
const requirements = accepts.find((e) => e?.extra?.paymentMethod === "lightning");
|
|
1426
|
+
if (!requirements?.extra?.invoice)
|
|
1427
|
+
return null;
|
|
1428
|
+
return requirements;
|
|
1429
|
+
};
|
|
1430
|
+
const handleX402Payment = async (x402Header, url, fetchArgs, headers, wallet) => {
|
|
1431
|
+
const { accepts } = decodeX402Header(x402Header);
|
|
1432
|
+
const requirements = accepts.find((e) => e?.extra?.paymentMethod === "lightning");
|
|
1369
1433
|
if (!requirements) {
|
|
1370
1434
|
throw new Error("x402: unsupported x402 network, only Bitcoin lightning network is supported.");
|
|
1371
1435
|
}
|
|
@@ -1376,9 +1440,17 @@ const handleX402Payment = async (x402Header, url, fetchArgs, headers, wallet) =>
|
|
|
1376
1440
|
if (invoice.amountRaw != requirements.amount) {
|
|
1377
1441
|
throw new Error(`Invalid invoice amount: ${invoice.amountRaw}. expected ${requirements.amount}`);
|
|
1378
1442
|
}
|
|
1379
|
-
await wallet.payInvoice({ invoice: invoice.paymentRequest });
|
|
1380
|
-
|
|
1381
|
-
|
|
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
|
+
});
|
|
1382
1454
|
};
|
|
1383
1455
|
const fetchWithX402 = async (url, fetchArgs, options) => {
|
|
1384
1456
|
const wallet = options.wallet;
|
|
@@ -1389,6 +1461,15 @@ const fetchWithX402 = async (url, fetchArgs, options) => {
|
|
|
1389
1461
|
fetchArgs.mode = "cors";
|
|
1390
1462
|
const headers = new Headers(fetchArgs.headers ?? undefined);
|
|
1391
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
|
+
}
|
|
1392
1473
|
const initResp = await fetch(url, fetchArgs);
|
|
1393
1474
|
const header = initResp.headers.get("PAYMENT-REQUIRED");
|
|
1394
1475
|
if (!header) {
|
|
@@ -1535,8 +1616,16 @@ const handleMppChargePayment = async (wwwAuthHeader, url, fetchArgs, headers, wa
|
|
|
1535
1616
|
const invResp = await wallet.payInvoice({ invoice });
|
|
1536
1617
|
// Per spec: Authorization: Payment <base64url-token> (single token, no wrapper)
|
|
1537
1618
|
const credential = buildMppCredential(challenge, invResp.preimage);
|
|
1538
|
-
|
|
1539
|
-
|
|
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
|
+
});
|
|
1540
1629
|
};
|
|
1541
1630
|
/**
|
|
1542
1631
|
* Fetch a resource protected by the draft-lightning-charge-00 payment
|
|
@@ -1547,9 +1636,11 @@ const handleMppChargePayment = async (wwwAuthHeader, url, fetchArgs, headers, wa
|
|
|
1547
1636
|
* the function pays the embedded BOLT11 invoice and retries with the
|
|
1548
1637
|
* resulting preimage as the credential.
|
|
1549
1638
|
*
|
|
1550
|
-
*
|
|
1551
|
-
*
|
|
1552
|
-
*
|
|
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.
|
|
1553
1644
|
*/
|
|
1554
1645
|
const fetchWithMpp = async (url, fetchArgs, options) => {
|
|
1555
1646
|
const wallet = options.wallet;
|
|
@@ -1563,6 +1654,15 @@ const fetchWithMpp = async (url, fetchArgs, options) => {
|
|
|
1563
1654
|
fetchArgs.mode = "cors";
|
|
1564
1655
|
const headers = new Headers(fetchArgs.headers ?? undefined);
|
|
1565
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
|
+
}
|
|
1566
1666
|
const initResp = await fetch(url, fetchArgs);
|
|
1567
1667
|
const wwwAuthHeader = initResp.headers.get("www-authenticate");
|
|
1568
1668
|
if (!wwwAuthHeader ||
|
|
@@ -1583,20 +1683,38 @@ const fetch402 = async (url, fetchArgs, options) => {
|
|
|
1583
1683
|
fetchArgs.mode = "cors";
|
|
1584
1684
|
const headers = new Headers(fetchArgs.headers ?? undefined);
|
|
1585
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
|
+
}
|
|
1586
1695
|
const initResp = await fetch(url, fetchArgs);
|
|
1696
|
+
// L402 / LSAT: dedicated scheme, dispatch directly.
|
|
1587
1697
|
const wwwAuthHeader = initResp.headers.get("www-authenticate");
|
|
1588
1698
|
if (wwwAuthHeader) {
|
|
1589
1699
|
const trimmed = wwwAuthHeader.trimStart().toLowerCase();
|
|
1590
|
-
if (trimmed.startsWith("payment")) {
|
|
1591
|
-
return handleMppChargePayment(wwwAuthHeader, url, fetchArgs, headers, wallet);
|
|
1592
|
-
}
|
|
1593
1700
|
if (trimmed.startsWith("l402") || trimmed.startsWith("lsat")) {
|
|
1594
1701
|
return handleL402Payment(wwwAuthHeader, url, fetchArgs, headers, wallet);
|
|
1595
1702
|
}
|
|
1596
|
-
throw new Error(`fetch402: unsupported WWW-Authenticate scheme: ${wwwAuthHeader}`);
|
|
1597
1703
|
}
|
|
1704
|
+
// A server may advertise multiple payment options at once (e.g. an MPP
|
|
1705
|
+
// USDC challenge in WWW-Authenticate alongside an x402 PAYMENT-REQUIRED
|
|
1706
|
+
// header that lists both USDC and lightning). Try each lightning-payable
|
|
1707
|
+
// handler in turn; only if none matches do we hand the original 402 back
|
|
1708
|
+
// to the caller so they can decide what to do with non-lightning offers.
|
|
1709
|
+
// 1. MPP-lightning challenge (Payment method="lightning" intent="charge").
|
|
1710
|
+
// parseMppChallenge returns null for any other method, which lets us
|
|
1711
|
+
// fall through to x402 instead of throwing.
|
|
1712
|
+
if (wwwAuthHeader && parseMppChallenge(wwwAuthHeader)) {
|
|
1713
|
+
return handleMppChargePayment(wwwAuthHeader, url, fetchArgs, headers, wallet);
|
|
1714
|
+
}
|
|
1715
|
+
// 2. x402 PAYMENT-REQUIRED with a lightning entry in `accepts`.
|
|
1598
1716
|
const x402Header = initResp.headers.get("PAYMENT-REQUIRED");
|
|
1599
|
-
if (x402Header) {
|
|
1717
|
+
if (x402Header && findX402LightningRequirements(x402Header)) {
|
|
1600
1718
|
return handleX402Payment(x402Header, url, fetchArgs, headers, wallet);
|
|
1601
1719
|
}
|
|
1602
1720
|
return initResp;
|
|
@@ -1681,14 +1799,19 @@ function parseL402Authorization(input) {
|
|
|
1681
1799
|
};
|
|
1682
1800
|
}
|
|
1683
1801
|
|
|
1802
|
+
exports.applyCredentials = applyCredentials;
|
|
1803
|
+
exports.attachPayment = attachPayment;
|
|
1684
1804
|
exports.createGuardedWallet = createGuardedWallet;
|
|
1685
1805
|
exports.fetch402 = fetch402;
|
|
1686
1806
|
exports.fetchWithL402 = fetchWithL402;
|
|
1687
1807
|
exports.fetchWithMpp = fetchWithMpp;
|
|
1688
1808
|
exports.fetchWithX402 = fetchWithX402;
|
|
1809
|
+
exports.findX402LightningRequirements = findX402LightningRequirements;
|
|
1810
|
+
exports.getInvoiceAmount = getInvoiceAmount;
|
|
1689
1811
|
exports.issueL402Macaroon = issueL402Macaroon;
|
|
1690
1812
|
exports.makeL402AuthenticateHeader = makeL402AuthenticateHeader;
|
|
1691
1813
|
exports.parseL402 = parseL402;
|
|
1692
1814
|
exports.parseL402Authorization = parseL402Authorization;
|
|
1815
|
+
exports.reusedCredentialPayment = reusedCredentialPayment;
|
|
1693
1816
|
exports.verifyL402Macaroon = verifyL402Macaroon;
|
|
1694
1817
|
//# sourceMappingURL=402.cjs.map
|