@getalby/lightning-tools 8.0.0 → 8.1.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.
- package/dist/cjs/402/l402.cjs +102 -0
- package/dist/cjs/402/l402.cjs.map +1 -1
- package/dist/cjs/402/x402.cjs +539 -509
- package/dist/cjs/402/x402.cjs.map +1 -1
- package/dist/cjs/402.cjs +157 -16
- package/dist/cjs/402.cjs.map +1 -1
- package/dist/cjs/bolt11.cjs +526 -518
- package/dist/cjs/bolt11.cjs.map +1 -1
- package/dist/cjs/index.cjs +624 -482
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/lnurl.cjs +14 -7
- package/dist/cjs/lnurl.cjs.map +1 -1
- package/dist/esm/402/l402.js +98 -1
- package/dist/esm/402/l402.js.map +1 -1
- package/dist/esm/402/x402.js +539 -510
- package/dist/esm/402/x402.js.map +1 -1
- package/dist/esm/402.js +152 -17
- package/dist/esm/402.js.map +1 -1
- package/dist/esm/bolt11.js +526 -519
- package/dist/esm/bolt11.js.map +1 -1
- package/dist/esm/index.js +618 -483
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/lnurl.js +14 -7
- package/dist/esm/lnurl.js.map +1 -1
- package/dist/lightning-tools.umd.js +3 -3
- package/dist/lightning-tools.umd.js.map +1 -1
- package/dist/types/402/l402.d.ts +39 -1
- package/dist/types/402/x402.d.ts +20 -1
- package/dist/types/402.d.ts +58 -2
- package/dist/types/bolt11.d.ts +2 -1
- package/dist/types/index.d.ts +59 -2
- package/package.json +1 -1
package/dist/cjs/402.cjs
CHANGED
|
@@ -1176,6 +1176,19 @@ const decodeInvoice = (paymentRequest) => {
|
|
|
1176
1176
|
return null;
|
|
1177
1177
|
}
|
|
1178
1178
|
};
|
|
1179
|
+
function validatePreimage(preimage, paymentHash) {
|
|
1180
|
+
try {
|
|
1181
|
+
if (!/^[0-9a-fA-F]{64}$/.test(preimage))
|
|
1182
|
+
return false;
|
|
1183
|
+
if (!/^[0-9a-fA-F]{64}$/.test(paymentHash))
|
|
1184
|
+
return false;
|
|
1185
|
+
const preimageHash = bytesToHex(sha256(fromHexString(preimage)));
|
|
1186
|
+
return paymentHash === preimageHash;
|
|
1187
|
+
}
|
|
1188
|
+
catch {
|
|
1189
|
+
return false;
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1179
1192
|
|
|
1180
1193
|
class Invoice {
|
|
1181
1194
|
constructor(args) {
|
|
@@ -1215,13 +1228,7 @@ class Invoice {
|
|
|
1215
1228
|
validatePreimage(preimage) {
|
|
1216
1229
|
if (!preimage || !this.paymentHash)
|
|
1217
1230
|
return false;
|
|
1218
|
-
|
|
1219
|
-
const preimageHash = bytesToHex(sha256(fromHexString(preimage)));
|
|
1220
|
-
return this.paymentHash === preimageHash;
|
|
1221
|
-
}
|
|
1222
|
-
catch {
|
|
1223
|
-
return false;
|
|
1224
|
-
}
|
|
1231
|
+
return validatePreimage(preimage, this.paymentHash);
|
|
1225
1232
|
}
|
|
1226
1233
|
async verifyPayment() {
|
|
1227
1234
|
try {
|
|
@@ -1264,6 +1271,11 @@ function createGuardedWallet(wallet, maxAmountSats) {
|
|
|
1264
1271
|
};
|
|
1265
1272
|
}
|
|
1266
1273
|
|
|
1274
|
+
/**
|
|
1275
|
+
* Client: parse "www-authenticate" header from server response
|
|
1276
|
+
* @param input
|
|
1277
|
+
* @returns details from the header value (token or macaroon, invoice)
|
|
1278
|
+
*/
|
|
1267
1279
|
const parseL402 = (input) => {
|
|
1268
1280
|
// Remove the L402 and LSAT identifiers
|
|
1269
1281
|
const string = input.replace("L402", "").replace("LSAT", "").trim();
|
|
@@ -1278,6 +1290,19 @@ const parseL402 = (input) => {
|
|
|
1278
1290
|
// Value is either match[3] (double-quoted), match[4] (single-quoted), or match[5] (unquoted)
|
|
1279
1291
|
keyValuePairs[match[1]] = match[3] || match[4] || match[5];
|
|
1280
1292
|
}
|
|
1293
|
+
if (!keyValuePairs["token"] && keyValuePairs["macaroon"]) {
|
|
1294
|
+
// fallback to old naming
|
|
1295
|
+
keyValuePairs["token"] = keyValuePairs["macaroon"];
|
|
1296
|
+
delete keyValuePairs["macaroon"];
|
|
1297
|
+
}
|
|
1298
|
+
if (!("token" in keyValuePairs) ||
|
|
1299
|
+
typeof keyValuePairs["token"] !== "string") {
|
|
1300
|
+
throw new Error("No macaroon or token found in www-authenticate header");
|
|
1301
|
+
}
|
|
1302
|
+
if (!("invoice" in keyValuePairs) ||
|
|
1303
|
+
typeof keyValuePairs["invoice"] !== "string") {
|
|
1304
|
+
throw new Error("No invoice found in www-authenticate header");
|
|
1305
|
+
}
|
|
1281
1306
|
return keyValuePairs;
|
|
1282
1307
|
};
|
|
1283
1308
|
|
|
@@ -1327,7 +1352,7 @@ const buildX402PaymentSignature = (scheme, network, invoice, requirements) => {
|
|
|
1327
1352
|
return btoa(unescape(encodeURIComponent(json)));
|
|
1328
1353
|
};
|
|
1329
1354
|
|
|
1330
|
-
const
|
|
1355
|
+
const decodeX402Header = (x402Header) => {
|
|
1331
1356
|
let parsed;
|
|
1332
1357
|
try {
|
|
1333
1358
|
parsed = JSON.parse(decodeURIComponent(escape(atob(x402Header))));
|
|
@@ -1338,9 +1363,31 @@ const handleX402Payment = async (x402Header, url, fetchArgs, headers, wallet) =>
|
|
|
1338
1363
|
if (!Array.isArray(parsed.accepts) || parsed.accepts.length === 0) {
|
|
1339
1364
|
throw new Error("x402: PAYMENT-REQUIRED header contains no payment options");
|
|
1340
1365
|
}
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1366
|
+
return { accepts: parsed.accepts };
|
|
1367
|
+
};
|
|
1368
|
+
/**
|
|
1369
|
+
* Probe a PAYMENT-REQUIRED header for a lightning-payable offer without
|
|
1370
|
+
* throwing. Returns the matching requirements, or null if the header has no
|
|
1371
|
+
* lightning entry (e.g. USDC-only endpoints) or is malformed. Used by the
|
|
1372
|
+
* top-level fetch402 dispatcher to decide whether to attempt payment or hand
|
|
1373
|
+
* the 402 back to the caller.
|
|
1374
|
+
*/
|
|
1375
|
+
const findX402LightningRequirements = (x402Header) => {
|
|
1376
|
+
let accepts;
|
|
1377
|
+
try {
|
|
1378
|
+
({ accepts } = decodeX402Header(x402Header));
|
|
1379
|
+
}
|
|
1380
|
+
catch (_) {
|
|
1381
|
+
return null;
|
|
1382
|
+
}
|
|
1383
|
+
const requirements = accepts.find((e) => e?.extra?.paymentMethod === "lightning");
|
|
1384
|
+
if (!requirements?.extra?.invoice)
|
|
1385
|
+
return null;
|
|
1386
|
+
return requirements;
|
|
1387
|
+
};
|
|
1388
|
+
const handleX402Payment = async (x402Header, url, fetchArgs, headers, wallet) => {
|
|
1389
|
+
const { accepts } = decodeX402Header(x402Header);
|
|
1390
|
+
const requirements = accepts.find((e) => e?.extra?.paymentMethod === "lightning");
|
|
1344
1391
|
if (!requirements) {
|
|
1345
1392
|
throw new Error("x402: unsupported x402 network, only Bitcoin lightning network is supported.");
|
|
1346
1393
|
}
|
|
@@ -1559,27 +1606,121 @@ const fetch402 = async (url, fetchArgs, options) => {
|
|
|
1559
1606
|
const headers = new Headers(fetchArgs.headers ?? undefined);
|
|
1560
1607
|
fetchArgs.headers = headers;
|
|
1561
1608
|
const initResp = await fetch(url, fetchArgs);
|
|
1609
|
+
// L402 / LSAT: dedicated scheme, dispatch directly.
|
|
1562
1610
|
const wwwAuthHeader = initResp.headers.get("www-authenticate");
|
|
1563
1611
|
if (wwwAuthHeader) {
|
|
1564
1612
|
const trimmed = wwwAuthHeader.trimStart().toLowerCase();
|
|
1565
|
-
if (trimmed.startsWith("payment")) {
|
|
1566
|
-
return handleMppChargePayment(wwwAuthHeader, url, fetchArgs, headers, wallet);
|
|
1567
|
-
}
|
|
1568
1613
|
if (trimmed.startsWith("l402") || trimmed.startsWith("lsat")) {
|
|
1569
1614
|
return handleL402Payment(wwwAuthHeader, url, fetchArgs, headers, wallet);
|
|
1570
1615
|
}
|
|
1571
|
-
throw new Error(`fetch402: unsupported WWW-Authenticate scheme: ${wwwAuthHeader}`);
|
|
1572
1616
|
}
|
|
1617
|
+
// A server may advertise multiple payment options at once (e.g. an MPP
|
|
1618
|
+
// USDC challenge in WWW-Authenticate alongside an x402 PAYMENT-REQUIRED
|
|
1619
|
+
// header that lists both USDC and lightning). Try each lightning-payable
|
|
1620
|
+
// handler in turn; only if none matches do we hand the original 402 back
|
|
1621
|
+
// to the caller so they can decide what to do with non-lightning offers.
|
|
1622
|
+
// 1. MPP-lightning challenge (Payment method="lightning" intent="charge").
|
|
1623
|
+
// parseMppChallenge returns null for any other method, which lets us
|
|
1624
|
+
// fall through to x402 instead of throwing.
|
|
1625
|
+
if (wwwAuthHeader && parseMppChallenge(wwwAuthHeader)) {
|
|
1626
|
+
return handleMppChargePayment(wwwAuthHeader, url, fetchArgs, headers, wallet);
|
|
1627
|
+
}
|
|
1628
|
+
// 2. x402 PAYMENT-REQUIRED with a lightning entry in `accepts`.
|
|
1573
1629
|
const x402Header = initResp.headers.get("PAYMENT-REQUIRED");
|
|
1574
|
-
if (x402Header) {
|
|
1630
|
+
if (x402Header && findX402LightningRequirements(x402Header)) {
|
|
1575
1631
|
return handleX402Payment(x402Header, url, fetchArgs, headers, wallet);
|
|
1576
1632
|
}
|
|
1577
1633
|
return initResp;
|
|
1578
1634
|
};
|
|
1579
1635
|
|
|
1636
|
+
async function issueL402Macaroon(secret, paymentHash, params) {
|
|
1637
|
+
if (params !== undefined &&
|
|
1638
|
+
Object.prototype.hasOwnProperty.call(params, "paymentHash")) {
|
|
1639
|
+
throw new Error("paymentHash is reserved");
|
|
1640
|
+
}
|
|
1641
|
+
const payload = { ...params, paymentHash };
|
|
1642
|
+
const encoded = Buffer.from(JSON.stringify(payload)).toString("base64url");
|
|
1643
|
+
const mac = await sign(secret, encoded);
|
|
1644
|
+
return `${encoded}.${mac}`;
|
|
1645
|
+
}
|
|
1646
|
+
async function verifyL402Macaroon(secret, token) {
|
|
1647
|
+
const { timingSafeEqual } = await import('crypto');
|
|
1648
|
+
const dotIndex = token.lastIndexOf(".");
|
|
1649
|
+
if (dotIndex === -1)
|
|
1650
|
+
throw new Error("Invalid macaroon token");
|
|
1651
|
+
const encoded = token.slice(0, dotIndex);
|
|
1652
|
+
const mac = token.slice(dotIndex + 1);
|
|
1653
|
+
// Constant-time comparison to prevent timing attacks
|
|
1654
|
+
const expectedMac = await sign(secret, encoded);
|
|
1655
|
+
try {
|
|
1656
|
+
if (!timingSafeEqual(Buffer.from(mac, "hex"), Buffer.from(expectedMac, "hex"))) {
|
|
1657
|
+
throw new Error("Invalid macaroon token");
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
catch (e) {
|
|
1661
|
+
throw new Error("Invalid macaroon token");
|
|
1662
|
+
}
|
|
1663
|
+
try {
|
|
1664
|
+
const parsed = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
|
|
1665
|
+
if (parsed === null ||
|
|
1666
|
+
typeof parsed !== "object" ||
|
|
1667
|
+
Array.isArray(parsed) ||
|
|
1668
|
+
typeof parsed.paymentHash !== "string") {
|
|
1669
|
+
throw new Error("Invalid macaroon payload");
|
|
1670
|
+
}
|
|
1671
|
+
return parsed;
|
|
1672
|
+
}
|
|
1673
|
+
catch {
|
|
1674
|
+
throw new Error("Invalid macaroon token");
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
async function sign(secret, payload) {
|
|
1678
|
+
const { createHmac } = await import('crypto');
|
|
1679
|
+
return createHmac("sha256", secret).update(payload).digest("hex");
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
/**
|
|
1683
|
+
* Server: create a WWW-Authenticate header for a given macaroon and invoice
|
|
1684
|
+
* @param args the macaroon/token and invoice generated for the client's request
|
|
1685
|
+
* @returns the header value
|
|
1686
|
+
*/
|
|
1687
|
+
const makeL402AuthenticateHeader = (args) => {
|
|
1688
|
+
if (!args.token) {
|
|
1689
|
+
throw new Error("token must be provided");
|
|
1690
|
+
}
|
|
1691
|
+
return `L402 version="0" token="${args.token}", invoice="${args.invoice}"`;
|
|
1692
|
+
};
|
|
1693
|
+
/**
|
|
1694
|
+
* Server: parse "authorization" header sent from client
|
|
1695
|
+
* @param input value from authorization header
|
|
1696
|
+
* @returns the macaroon and preimage
|
|
1697
|
+
*/
|
|
1698
|
+
function parseL402Authorization(input) {
|
|
1699
|
+
// Backwards compat: LSAT was the former name of L402
|
|
1700
|
+
const normalized = input.replace(/^LSAT /, "L402 ");
|
|
1701
|
+
const prefix = "L402 ";
|
|
1702
|
+
if (!normalized.startsWith(prefix))
|
|
1703
|
+
return null;
|
|
1704
|
+
const credentials = normalized.slice(prefix.length);
|
|
1705
|
+
const colonIndex = credentials.indexOf(":");
|
|
1706
|
+
if (colonIndex === -1) {
|
|
1707
|
+
throw new Error("Invalid authorization header value");
|
|
1708
|
+
}
|
|
1709
|
+
return {
|
|
1710
|
+
token: credentials.slice(0, colonIndex),
|
|
1711
|
+
preimage: credentials.slice(colonIndex + 1),
|
|
1712
|
+
};
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1580
1715
|
exports.createGuardedWallet = createGuardedWallet;
|
|
1581
1716
|
exports.fetch402 = fetch402;
|
|
1582
1717
|
exports.fetchWithL402 = fetchWithL402;
|
|
1583
1718
|
exports.fetchWithMpp = fetchWithMpp;
|
|
1584
1719
|
exports.fetchWithX402 = fetchWithX402;
|
|
1720
|
+
exports.findX402LightningRequirements = findX402LightningRequirements;
|
|
1721
|
+
exports.issueL402Macaroon = issueL402Macaroon;
|
|
1722
|
+
exports.makeL402AuthenticateHeader = makeL402AuthenticateHeader;
|
|
1723
|
+
exports.parseL402 = parseL402;
|
|
1724
|
+
exports.parseL402Authorization = parseL402Authorization;
|
|
1725
|
+
exports.verifyL402Macaroon = verifyL402Macaroon;
|
|
1585
1726
|
//# sourceMappingURL=402.cjs.map
|