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