@getalby/lightning-tools 6.1.0 → 8.0.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.
Files changed (44) hide show
  1. package/README.md +111 -29
  2. package/dist/cjs/402/l402.cjs +55 -0
  3. package/dist/cjs/402/l402.cjs.map +1 -0
  4. package/dist/cjs/402/mpp.cjs +179 -0
  5. package/dist/cjs/402/mpp.cjs.map +1 -0
  6. package/dist/cjs/402/x402.cjs +1313 -0
  7. package/dist/cjs/402/x402.cjs.map +1 -0
  8. package/dist/cjs/402.cjs +1585 -0
  9. package/dist/cjs/402.cjs.map +1 -0
  10. package/dist/cjs/bolt11.cjs +8 -0
  11. package/dist/cjs/bolt11.cjs.map +1 -1
  12. package/dist/cjs/index.cjs +304 -52
  13. package/dist/cjs/index.cjs.map +1 -1
  14. package/dist/cjs/lnurl.cjs +8 -0
  15. package/dist/cjs/lnurl.cjs.map +1 -1
  16. package/dist/esm/402/l402.js +53 -0
  17. package/dist/esm/402/l402.js.map +1 -0
  18. package/dist/esm/402/mpp.js +177 -0
  19. package/dist/esm/402/mpp.js.map +1 -0
  20. package/dist/esm/402/x402.js +1311 -0
  21. package/dist/esm/402/x402.js.map +1 -0
  22. package/dist/esm/402.js +1579 -0
  23. package/dist/esm/402.js.map +1 -0
  24. package/dist/esm/bolt11.js +8 -0
  25. package/dist/esm/bolt11.js.map +1 -1
  26. package/dist/esm/index.js +301 -50
  27. package/dist/esm/index.js.map +1 -1
  28. package/dist/esm/lnurl.js +8 -0
  29. package/dist/esm/lnurl.js.map +1 -1
  30. package/dist/lightning-tools.umd.js +2 -2
  31. package/dist/lightning-tools.umd.js.map +1 -1
  32. package/dist/types/402/l402.d.ts +13 -0
  33. package/dist/types/402/mpp.d.ts +26 -0
  34. package/dist/types/402/x402.d.ts +13 -0
  35. package/dist/types/402.d.ts +41 -0
  36. package/dist/types/bolt11.d.ts +4 -0
  37. package/dist/types/index.d.ts +40 -20
  38. package/dist/types/lnurl.d.ts +2 -0
  39. package/package.json +20 -5
  40. package/dist/cjs/l402.cjs +0 -89
  41. package/dist/cjs/l402.cjs.map +0 -1
  42. package/dist/esm/l402.js +0 -84
  43. package/dist/esm/l402.js.map +0 -1
  44. package/dist/types/l402.d.ts +0 -27
@@ -834,8 +834,12 @@ const decodeInvoice = (paymentRequest) => {
834
834
  return null;
835
835
  const paymentHash = hashTag.value;
836
836
  let satoshi = 0;
837
+ let millisatoshi = 0;
838
+ let amountRaw = "0";
837
839
  const amountTag = decoded.sections.find((value) => value.name === "amount");
838
840
  if (amountTag?.name === "amount" && amountTag.value) {
841
+ amountRaw = amountTag.value;
842
+ millisatoshi = parseInt(amountTag.value);
839
843
  satoshi = parseInt(amountTag.value) / 1000; // millisats
840
844
  }
841
845
  const timestampTag = decoded.sections.find((value) => value.name === "timestamp");
@@ -854,6 +858,8 @@ const decodeInvoice = (paymentRequest) => {
854
858
  return {
855
859
  paymentHash,
856
860
  satoshi,
861
+ millisatoshi,
862
+ amountRaw,
857
863
  timestamp,
858
864
  expiry,
859
865
  description,
@@ -1183,6 +1189,8 @@ class Invoice {
1183
1189
  }
1184
1190
  this.paymentHash = decodedInvoice.paymentHash;
1185
1191
  this.satoshi = decodedInvoice.satoshi;
1192
+ this.millisatoshi = decodedInvoice.millisatoshi;
1193
+ this.amountRaw = decodedInvoice.amountRaw;
1186
1194
  this.timestamp = decodedInvoice.timestamp;
1187
1195
  this.expiry = decodedInvoice.expiry;
1188
1196
  this.createdDate = new Date(this.timestamp * 1000);
@@ -1659,24 +1667,18 @@ class LightningAddress {
1659
1667
  }
1660
1668
  }
1661
1669
 
1662
- class MemoryStorage {
1663
- constructor(initial) {
1664
- this.storage = initial || {};
1665
- }
1666
- getItem(key) {
1667
- return this.storage[key];
1668
- }
1669
- setItem(key, value) {
1670
- this.storage[key] = value;
1671
- }
1672
- }
1673
- class NoStorage {
1674
- constructor(initial) { }
1675
- getItem(key) {
1676
- return null;
1677
- }
1678
- setItem(key, value) { }
1670
+ function createGuardedWallet(wallet, maxAmountSats) {
1671
+ return {
1672
+ payInvoice: async (args) => {
1673
+ const invoice = new Invoice({ pr: args.invoice });
1674
+ if (invoice.satoshi > maxAmountSats) {
1675
+ throw new Error(`Invoice amount (${invoice.satoshi} sats) exceeds maxAmount (${maxAmountSats} sats)`);
1676
+ }
1677
+ return wallet.payInvoice(args);
1678
+ },
1679
+ };
1679
1680
  }
1681
+
1680
1682
  const parseL402 = (input) => {
1681
1683
  // Remove the L402 and LSAT identifiers
1682
1684
  const string = input.replace("L402", "").replace("LSAT", "").trim();
@@ -1694,51 +1696,300 @@ const parseL402 = (input) => {
1694
1696
  return keyValuePairs;
1695
1697
  };
1696
1698
 
1697
- const memoryStorage = new MemoryStorage();
1698
- const HEADER_KEY = "L402"; // we have to update this to L402 at some point
1699
- const fetchWithL402 = async (url, fetchArgs, options) => {
1700
- if (!options) {
1701
- options = {};
1699
+ const handleL402Payment = async (l402Header, url, fetchArgs, headers, wallet) => {
1700
+ const details = parseL402(l402Header);
1701
+ const token = details.token || details.macaroon;
1702
+ const invoice = details.invoice;
1703
+ if (!token) {
1704
+ throw new Error("L402: missing token/macaroon in WWW-Authenticate header");
1702
1705
  }
1703
- const headerKey = options.headerKey || HEADER_KEY;
1704
- const webln = options.webln || globalThis.webln;
1705
- if (!webln) {
1706
- throw new Error("WebLN is missing");
1706
+ if (!invoice) {
1707
+ throw new Error("L402: missing invoice in WWW-Authenticate header");
1708
+ }
1709
+ const invResp = await wallet.payInvoice({ invoice });
1710
+ headers.set("Authorization", `L402 ${token}:${invResp.preimage}`);
1711
+ return fetch(url, fetchArgs);
1712
+ };
1713
+ const fetchWithL402 = async (url, fetchArgs, options) => {
1714
+ const wallet = options.wallet;
1715
+ if (!wallet) {
1716
+ throw new Error("wallet is missing");
1707
1717
  }
1708
- const store = options.store || memoryStorage;
1709
1718
  if (!fetchArgs) {
1710
1719
  fetchArgs = {};
1711
1720
  }
1712
1721
  fetchArgs.cache = "no-store";
1713
1722
  fetchArgs.mode = "cors";
1714
- if (!fetchArgs.headers) {
1715
- fetchArgs.headers = {};
1723
+ const headers = new Headers(fetchArgs.headers ?? undefined);
1724
+ fetchArgs.headers = headers;
1725
+ const initResp = await fetch(url, fetchArgs);
1726
+ const header = initResp.headers.get("www-authenticate");
1727
+ if (!header) {
1728
+ return initResp;
1716
1729
  }
1717
- const cachedL402Data = store.getItem(url);
1718
- if (cachedL402Data) {
1719
- const data = JSON.parse(cachedL402Data);
1720
- fetchArgs.headers["Authorization"] =
1721
- `${headerKey} ${data.token}:${data.preimage}`;
1722
- return await fetch(url, fetchArgs);
1730
+ return handleL402Payment(header, url, fetchArgs, headers, wallet);
1731
+ };
1732
+
1733
+ const buildX402PaymentSignature = (scheme, network, invoice, requirements) => {
1734
+ const json = JSON.stringify({
1735
+ x402Version: 2,
1736
+ scheme,
1737
+ network,
1738
+ payload: { invoice },
1739
+ accepted: requirements,
1740
+ });
1741
+ // btoa only handles latin1; encode via UTF-8 to be safe
1742
+ return btoa(unescape(encodeURIComponent(json)));
1743
+ };
1744
+
1745
+ const handleX402Payment = async (x402Header, url, fetchArgs, headers, wallet) => {
1746
+ let parsed;
1747
+ try {
1748
+ parsed = JSON.parse(decodeURIComponent(escape(atob(x402Header))));
1723
1749
  }
1724
- fetchArgs.headers["Accept-Authenticate"] = headerKey;
1750
+ catch (_) {
1751
+ throw new Error("x402: invalid PAYMENT-REQUIRED header (not valid base64-encoded JSON)");
1752
+ }
1753
+ if (!Array.isArray(parsed.accepts) || parsed.accepts.length === 0) {
1754
+ throw new Error("x402: PAYMENT-REQUIRED header contains no payment options");
1755
+ }
1756
+ const requirements = parsed.accepts.find((e) => {
1757
+ return e.extra?.paymentMethod === "lightning";
1758
+ });
1759
+ if (!requirements) {
1760
+ throw new Error("x402: unsupported x402 network, only Bitcoin lightning network is supported.");
1761
+ }
1762
+ if (!requirements.extra?.invoice) {
1763
+ throw new Error("x402: payment requirements missing lightning invoice");
1764
+ }
1765
+ const invoice = new Invoice({ pr: requirements.extra.invoice });
1766
+ if (invoice.amountRaw != requirements.amount) {
1767
+ throw new Error(`Invalid invoice amount: ${invoice.amountRaw}. expected ${requirements.amount}`);
1768
+ }
1769
+ await wallet.payInvoice({ invoice: invoice.paymentRequest });
1770
+ headers.set("payment-signature", buildX402PaymentSignature(requirements.scheme, requirements.network, invoice.paymentRequest, requirements));
1771
+ return fetch(url, fetchArgs);
1772
+ };
1773
+ const fetchWithX402 = async (url, fetchArgs, options) => {
1774
+ const wallet = options.wallet;
1775
+ if (!fetchArgs) {
1776
+ fetchArgs = {};
1777
+ }
1778
+ fetchArgs.cache = "no-store";
1779
+ fetchArgs.mode = "cors";
1780
+ const headers = new Headers(fetchArgs.headers ?? undefined);
1781
+ fetchArgs.headers = headers;
1725
1782
  const initResp = await fetch(url, fetchArgs);
1726
- const header = initResp.headers.get("www-authenticate");
1783
+ const header = initResp.headers.get("PAYMENT-REQUIRED");
1727
1784
  if (!header) {
1728
1785
  return initResp;
1729
1786
  }
1730
- const details = parseL402(header);
1731
- const token = details.token || details.macaroon;
1732
- const inv = details.invoice;
1733
- await webln.enable();
1734
- const invResp = await webln.sendPayment(inv);
1735
- store.setItem(url, JSON.stringify({
1736
- token: token,
1737
- preimage: invResp.preimage,
1738
- }));
1739
- fetchArgs.headers["Authorization"] =
1740
- `${headerKey} ${token}:${invResp.preimage}`;
1741
- return await fetch(url, fetchArgs);
1787
+ return handleX402Payment(header, url, fetchArgs, headers, wallet);
1788
+ };
1789
+
1790
+ /**
1791
+ * Parse a `WWW-Authenticate: Payment …` header produced by a
1792
+ * draft-lightning-charge-00 server. Expected format:
1793
+ *
1794
+ * Payment id="<id>", realm="<realm>", method="lightning",
1795
+ * intent="charge", request="<base64url>" [, expires="<rfc3339>"]
1796
+ *
1797
+ * Returns null when the header is not a Payment lightning/charge challenge.
1798
+ */
1799
+ const parseMppChallenge = (header) => {
1800
+ if (!header.trimStart().toLowerCase().startsWith("payment")) {
1801
+ return null;
1802
+ }
1803
+ const rest = header
1804
+ .slice(header.toLowerCase().indexOf("payment") + "payment".length)
1805
+ .trim();
1806
+ const result = {};
1807
+ const regex = /(\w+)=("([^"]*)"|'([^']*)'|([^,\s]*))/g;
1808
+ let match;
1809
+ while ((match = regex.exec(rest)) !== null) {
1810
+ result[match[1]] = match[3] ?? match[4] ?? match[5] ?? "";
1811
+ }
1812
+ if (result.method !== "lightning" ||
1813
+ result.intent !== "charge" ||
1814
+ !result.id ||
1815
+ !result.realm ||
1816
+ !result.request) {
1817
+ return null;
1818
+ }
1819
+ return {
1820
+ id: result.id,
1821
+ realm: result.realm,
1822
+ method: result.method,
1823
+ intent: result.intent,
1824
+ request: result.request,
1825
+ ...(result.expires ? { expires: result.expires } : {}),
1826
+ };
1827
+ };
1828
+ /** Decode a base64url string (no padding required) to a UTF-8 string. */
1829
+ const decodeBase64url = (input) => {
1830
+ const base64 = input.replace(/-/g, "+").replace(/_/g, "/");
1831
+ const binary = atob(base64);
1832
+ const bytes = new Uint8Array(binary.length);
1833
+ for (let i = 0; i < binary.length; i++) {
1834
+ bytes[i] = binary.charCodeAt(i);
1835
+ }
1836
+ return new TextDecoder("utf-8").decode(bytes);
1837
+ };
1838
+ /** Encode a UTF-8 string to base64url without padding. */
1839
+ const encodeBase64url = (input) => {
1840
+ const bytes = new TextEncoder().encode(input);
1841
+ let binary = "";
1842
+ for (let i = 0; i < bytes.length; i++) {
1843
+ binary += String.fromCharCode(bytes[i]);
1844
+ }
1845
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
1846
+ };
1847
+ /**
1848
+ * JSON Canonicalization Scheme (RFC 8785).
1849
+ * Produces compact JSON with object keys sorted lexicographically.
1850
+ */
1851
+ const jcs = (value) => {
1852
+ if (value === null || typeof value !== "object") {
1853
+ return JSON.stringify(value);
1854
+ }
1855
+ if (Array.isArray(value)) {
1856
+ return "[" + value.map(jcs).join(",") + "]";
1857
+ }
1858
+ const keys = Object.keys(value).sort();
1859
+ return ("{" +
1860
+ keys
1861
+ .map((k) => JSON.stringify(k) + ":" + jcs(value[k]))
1862
+ .join(",") +
1863
+ "}");
1864
+ };
1865
+ /**
1866
+ * Build the base64url-encoded credential token for the `Authorization` header.
1867
+ *
1868
+ * Per the spec the credential is a JCS-serialised JSON object that echoes all
1869
+ * challenge auth-params (id, realm, method, intent, request, expires) and
1870
+ * carries the HTLC preimage that proves payment:
1871
+ *
1872
+ * {
1873
+ * "challenge": { "id": "…", "intent": "charge",
1874
+ * "method": "lightning", "realm": "…", "request": "…" },
1875
+ * "payload": { "preimage": "<64-char lowercase hex>" }
1876
+ * }
1877
+ *
1878
+ * Keys are sorted lexicographically at every level per JCS.
1879
+ */
1880
+ const buildMppCredential = (challenge, preimage, source) => {
1881
+ const challengeEcho = {
1882
+ id: challenge.id,
1883
+ intent: challenge.intent,
1884
+ method: challenge.method,
1885
+ realm: challenge.realm,
1886
+ request: challenge.request,
1887
+ };
1888
+ if (challenge.expires) {
1889
+ challengeEcho.expires = challenge.expires;
1890
+ }
1891
+ const credential = {
1892
+ challenge: challengeEcho,
1893
+ payload: { preimage },
1894
+ };
1895
+ return encodeBase64url(jcs(credential));
1896
+ };
1897
+
1898
+ /**
1899
+ * Handle a `WWW-Authenticate: Payment …` challenge produced by a
1900
+ * draft-lightning-charge-00 server.
1901
+ *
1902
+ * Flow:
1903
+ * 1. Parse the challenge from the header.
1904
+ * 2. Decode the `request` auth-param to find the BOLT11 invoice.
1905
+ * 3. Pay the invoice via the wallet; receive the HTLC preimage.
1906
+ * 4. Build the `Authorization: Payment <credential>` header.
1907
+ * 5. Retry the original request with the credential.
1908
+ */
1909
+ const handleMppChargePayment = async (wwwAuthHeader, url, fetchArgs, headers, wallet) => {
1910
+ const challenge = parseMppChallenge(wwwAuthHeader);
1911
+ if (!challenge) {
1912
+ throw new Error("mpp: invalid or unsupported WWW-Authenticate challenge (expected Payment method=lightning intent=charge)");
1913
+ }
1914
+ let request;
1915
+ try {
1916
+ request = JSON.parse(decodeBase64url(challenge.request));
1917
+ }
1918
+ catch (_) {
1919
+ throw new Error("mpp: invalid request auth-param (not valid base64url-encoded JSON)");
1920
+ }
1921
+ const invoice = request.methodDetails?.invoice;
1922
+ if (!invoice) {
1923
+ throw new Error("mpp: missing invoice in charge request");
1924
+ }
1925
+ const invResp = await wallet.payInvoice({ invoice });
1926
+ // Per spec: Authorization: Payment <base64url-token> (single token, no wrapper)
1927
+ const credential = buildMppCredential(challenge, invResp.preimage);
1928
+ headers.set("Authorization", `Payment ${credential}`);
1929
+ return fetch(url, fetchArgs);
1930
+ };
1931
+ /**
1932
+ * Fetch a resource protected by the draft-lightning-charge-00 payment
1933
+ * authentication protocol.
1934
+ *
1935
+ * On a `402 Payment Required` response that carries a
1936
+ * `WWW-Authenticate: Payment method="lightning" intent="charge" …` header
1937
+ * the function pays the embedded BOLT11 invoice and retries with the
1938
+ * resulting preimage as the credential.
1939
+ *
1940
+ * Note: lightning-charge uses consume-once challenge semantics – each
1941
+ * challenge embeds a fresh invoice, so paid credentials cannot be reused.
1942
+ * The `store` option is accepted for API consistency but is not used.
1943
+ */
1944
+ const fetchWithMpp = async (url, fetchArgs, options) => {
1945
+ const wallet = options.wallet;
1946
+ if (!wallet) {
1947
+ throw new Error("wallet is missing");
1948
+ }
1949
+ if (!fetchArgs) {
1950
+ fetchArgs = {};
1951
+ }
1952
+ fetchArgs.cache = "no-store";
1953
+ fetchArgs.mode = "cors";
1954
+ const headers = new Headers(fetchArgs.headers ?? undefined);
1955
+ fetchArgs.headers = headers;
1956
+ const initResp = await fetch(url, fetchArgs);
1957
+ const wwwAuthHeader = initResp.headers.get("www-authenticate");
1958
+ if (!wwwAuthHeader ||
1959
+ !wwwAuthHeader.trimStart().toLowerCase().startsWith("payment")) {
1960
+ return initResp;
1961
+ }
1962
+ return handleMppChargePayment(wwwAuthHeader, url, fetchArgs, headers, wallet);
1963
+ };
1964
+
1965
+ const fetch402 = async (url, fetchArgs, options) => {
1966
+ const wallet = options.maxAmount
1967
+ ? createGuardedWallet(options.wallet, options.maxAmount)
1968
+ : options.wallet;
1969
+ if (!fetchArgs) {
1970
+ fetchArgs = {};
1971
+ }
1972
+ fetchArgs.cache = "no-store";
1973
+ fetchArgs.mode = "cors";
1974
+ const headers = new Headers(fetchArgs.headers ?? undefined);
1975
+ fetchArgs.headers = headers;
1976
+ const initResp = await fetch(url, fetchArgs);
1977
+ const wwwAuthHeader = initResp.headers.get("www-authenticate");
1978
+ if (wwwAuthHeader) {
1979
+ const trimmed = wwwAuthHeader.trimStart().toLowerCase();
1980
+ if (trimmed.startsWith("payment")) {
1981
+ return handleMppChargePayment(wwwAuthHeader, url, fetchArgs, headers, wallet);
1982
+ }
1983
+ if (trimmed.startsWith("l402") || trimmed.startsWith("lsat")) {
1984
+ return handleL402Payment(wwwAuthHeader, url, fetchArgs, headers, wallet);
1985
+ }
1986
+ throw new Error(`fetch402: unsupported WWW-Authenticate scheme: ${wwwAuthHeader}`);
1987
+ }
1988
+ const x402Header = initResp.headers.get("PAYMENT-REQUIRED");
1989
+ if (x402Header) {
1990
+ return handleX402Payment(x402Header, url, fetchArgs, headers, wallet);
1991
+ }
1992
+ return initResp;
1742
1993
  };
1743
1994
 
1744
1995
  const numSatsInBtc = 100000000;
@@ -1794,10 +2045,12 @@ exports.DEFAULT_PROXY = DEFAULT_PROXY;
1794
2045
  exports.Invoice = Invoice;
1795
2046
  exports.LN_ADDRESS_REGEX = LN_ADDRESS_REGEX;
1796
2047
  exports.LightningAddress = LightningAddress;
1797
- exports.MemoryStorage = MemoryStorage;
1798
- exports.NoStorage = NoStorage;
2048
+ exports.createGuardedWallet = createGuardedWallet;
1799
2049
  exports.decodeInvoice = decodeInvoice;
2050
+ exports.fetch402 = fetch402;
1800
2051
  exports.fetchWithL402 = fetchWithL402;
2052
+ exports.fetchWithMpp = fetchWithMpp;
2053
+ exports.fetchWithX402 = fetchWithX402;
1801
2054
  exports.fromHexString = fromHexString;
1802
2055
  exports.generateZapEvent = generateZapEvent;
1803
2056
  exports.getEventHash = getEventHash;
@@ -1809,7 +2062,6 @@ exports.getSatoshiValue = getSatoshiValue;
1809
2062
  exports.isUrl = isUrl;
1810
2063
  exports.isValidAmount = isValidAmount;
1811
2064
  exports.parseKeysendResponse = parseKeysendResponse;
1812
- exports.parseL402 = parseL402;
1813
2065
  exports.parseLnUrlPayResponse = parseLnUrlPayResponse;
1814
2066
  exports.parseNostrResponse = parseNostrResponse;
1815
2067
  exports.sendBoostagram = sendBoostagram;