thunder-bridge 0.8.1 → 0.8.2

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/index.js CHANGED
@@ -34,10 +34,8 @@ var ProblemError = class extends Error {
34
34
  this.detail = problem.detail ?? null;
35
35
  }
36
36
  };
37
- var NAMESPACE = "urn:problem-type:thunder-bridge:";
38
- var NAMESPACE_BEFORE_THE_RENAME = "urn:problem-type:thunder-bridge-direct:";
39
37
  function isProblemType(problem, type) {
40
- return problem.type === type || problem.type === type.replace(NAMESPACE, NAMESPACE_BEFORE_THE_RENAME);
38
+ return problem.type === type;
41
39
  }
42
40
  var NO_WALLET_AVAILABLE = "urn:problem-type:thunder-bridge:no-wallet-available";
43
41
  var REQUEST_IN_FLIGHT = "urn:problem-type:thunder-bridge:request-in-flight";
@@ -73,8 +71,8 @@ function hexToBytes(hex) {
73
71
  }
74
72
  return bytes;
75
73
  }
76
- function isHex(text2) {
77
- return /^[0-9a-f]+$/i.test(text2) && text2.length % 2 === 0;
74
+ function isHex(text3) {
75
+ return /^[0-9a-f]+$/i.test(text3) && text3.length % 2 === 0;
78
76
  }
79
77
 
80
78
  // ../core/sha256.ts
@@ -168,8 +166,8 @@ function sha256(bytes) {
168
166
  }
169
167
  return asBytes(state);
170
168
  }
171
- function sha256Hex(text2) {
172
- return bytesToHex(sha256(new TextEncoder().encode(text2)));
169
+ function sha256Hex(text3) {
170
+ return bytesToHex(sha256(new TextEncoder().encode(text3)));
173
171
  }
174
172
  function withPadding(bytes) {
175
173
  const padded = new Uint8Array(
@@ -348,10 +346,13 @@ function sameOrigin(one, other) {
348
346
  return false;
349
347
  }
350
348
  }
349
+ function publicAddress(literal) {
350
+ return literal.includes(":") ? ipv6IsGlobal(literal) : ipv4IsGlobal(literal);
351
+ }
351
352
  function hostIsPublic(host) {
352
- if (host.startsWith("[")) return ipv6IsGlobal(host.slice(1, -1));
353
+ if (host.startsWith("[")) return publicAddress(host.slice(1, -1));
353
354
  const name = host.replace(/\.$/, "");
354
- if (/^\d+\.\d+\.\d+\.\d+$/.test(name)) return ipv4IsGlobal(name);
355
+ if (/^\d+\.\d+\.\d+\.\d+$/.test(name)) return publicAddress(name);
355
356
  const labels = name.split(".");
356
357
  if (labels.length < 2 || labels.some((label) => label === "")) return false;
357
358
  return !PRIVATE_SUFFIXES.includes(labels[labels.length - 1]);
@@ -616,6 +617,16 @@ var TERMINAL = /* @__PURE__ */ new Set(["paid", "expired"]);
616
617
  var RECONNECT_DELAY_MS = 3e3;
617
618
  var RECONNECT_CAP_MS = 3e4;
618
619
  var UNANSWERED_ATTEMPTS = 5;
620
+ var TRIGGER_MIN_CHARS = 16;
621
+ var STRANGER_PROBE = "is-this-gateway-yours";
622
+ function unguessable(trigger) {
623
+ if (trigger.length < TRIGGER_MIN_CHARS) {
624
+ throw new Error(
625
+ `a trigger secret is the only thing guarding its stream, and nothing rate limits a guess at it, so it has to be at least ${TRIGGER_MIN_CHARS} characters nobody can predict`
626
+ );
627
+ }
628
+ return trigger;
629
+ }
619
630
  function backoffMs(firstDelay, attempt) {
620
631
  const grown = Math.min(firstDelay * 2 ** (attempt - 1), RECONNECT_CAP_MS);
621
632
  return Math.round(grown * (0.5 + Math.random() / 2));
@@ -624,6 +635,7 @@ var ThunderBridge = class {
624
635
  baseUrl;
625
636
  verify;
626
637
  token;
638
+ strangers = null;
627
639
  constructor(baseUrl, options) {
628
640
  this.baseUrl = baseUrl.replace(/\/+$/, "");
629
641
  this.verify = options?.verify ?? true;
@@ -637,6 +649,21 @@ var ThunderBridge = class {
637
649
  get isPrivate() {
638
650
  return this.token !== null;
639
651
  }
652
+ /**
653
+ * Whether the gateway turns away a caller carrying no token, asked by making
654
+ * one unauthenticated read it would have to refuse. `isPrivate` answers only
655
+ * whether you configured a token, which is your side of the arrangement and
656
+ * says nothing about the gateway's, so a made-up token against a public
657
+ * instance reads as private and is not. Asked once and remembered, because an
658
+ * instance does not change its mind. Anything other than a refusal counts as
659
+ * open, so an unreachable gateway fails closed
660
+ */
661
+ async refusesStrangers() {
662
+ this.strangers ??= fetch(`${this.baseUrl}/incoming-payments/${STRANGER_PROBE}`, {
663
+ headers: { accept: "application/json" }
664
+ }).then((answer) => answer.status === 401).catch(() => false);
665
+ return await this.strangers;
666
+ }
640
667
  /**
641
668
  * Ask the gateway for an invoice payable to the first address on your list
642
669
  * that can issue a provable one, throws `NoWalletAvailableError` when none can
@@ -648,7 +675,10 @@ var ThunderBridge = class {
648
675
  const response = await fetch(`${this.baseUrl}/incoming-payments`, {
649
676
  method: "POST",
650
677
  headers,
651
- body: createRequestBody(params, options?.trigger ? sha256Hex(options.trigger) : null)
678
+ body: createRequestBody(
679
+ params,
680
+ options?.trigger ? sha256Hex(unguessable(options.trigger)) : null
681
+ )
652
682
  });
653
683
  if (!response.ok) throw await problemFrom(response);
654
684
  const payment = await paymentFrom(response);
@@ -890,7 +920,10 @@ var ThunderBridge = class {
890
920
  const response = await fetch(`${this.baseUrl}/watched-payments`, {
891
921
  method: "POST",
892
922
  headers: this.sending(),
893
- body: watchRequestBody(params, params.trigger ? sha256Hex(params.trigger) : null)
923
+ body: watchRequestBody(
924
+ params,
925
+ params.trigger ? sha256Hex(unguessable(params.trigger)) : null
926
+ )
894
927
  });
895
928
  if (!response.ok) throw await problemFrom(response);
896
929
  const watched = triggerEventFromWire(await response.json().catch(() => null));
@@ -909,7 +942,7 @@ var ThunderBridge = class {
909
942
  */
910
943
  followTrigger(secret, options) {
911
944
  const base = this.baseUrl.replace(/^http/, "ws");
912
- const direct = `${base}/ws/triggers/${encodeURIComponent(secret)}`;
945
+ const direct = `${base}/ws/triggers/${encodeURIComponent(unguessable(secret))}`;
913
946
  const firstDelay = options.reconnectDelayMs ?? RECONNECT_DELAY_MS;
914
947
  let socket = null;
915
948
  let retry;
@@ -1081,10 +1114,10 @@ function toBase64Url(bytes) {
1081
1114
  for (const byte of bytes) binary += String.fromCharCode(byte);
1082
1115
  return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1083
1116
  }
1084
- function fromBase64Url(text2) {
1085
- if (text2.length === 0 || !/^[A-Za-z0-9_-]+$/.test(text2)) return null;
1117
+ function fromBase64Url(text3) {
1118
+ if (text3.length === 0 || !/^[A-Za-z0-9_-]+$/.test(text3)) return null;
1086
1119
  try {
1087
- const binary = atob(text2.replace(/-/g, "+").replace(/_/g, "/"));
1120
+ const binary = atob(text3.replace(/-/g, "+").replace(/_/g, "/"));
1088
1121
  const bytes = new Uint8Array(binary.length);
1089
1122
  for (let at = 0; at < bytes.length; at++) bytes[at] = binary.charCodeAt(at);
1090
1123
  return bytes;
@@ -1116,6 +1149,87 @@ function asBuffer(bytes) {
1116
1149
  return bytes instanceof Uint8Array && bytes.buffer instanceof ArrayBuffer ? bytes : new Uint8Array(bytes);
1117
1150
  }
1118
1151
 
1152
+ // ../core/outbound.ts
1153
+ import { lookup } from "dns/promises";
1154
+ import { setTimeout as sleep } from "timers/promises";
1155
+ var HTTP_TIMEOUT_MS2 = 15e3;
1156
+ var LOOKUP_TIMEOUT_MS = 5e3;
1157
+ var HOPS_FOLLOWED = 2;
1158
+ var REDIRECTS = [301, 302, 303, 307, 308];
1159
+ var KEEPS_THE_METHOD = [307, 308];
1160
+ var CREDENTIALS = ["authorization", "cookie", "proxy-authorization"];
1161
+ var BODY_LIMIT_BYTES = 262144;
1162
+ async function ask(url, sent = {}) {
1163
+ const capped = AbortSignal.timeout(HTTP_TIMEOUT_MS2);
1164
+ const signal = sent.deadline ? AbortSignal.any([sent.deadline, capped]) : capped;
1165
+ let target = url;
1166
+ let carried = sent;
1167
+ for (let hop = 0; hop <= HOPS_FOLLOWED; hop += 1) {
1168
+ await refuseUnlessPublic(target);
1169
+ const response = await fetch(target, {
1170
+ method: carried.method ?? "GET",
1171
+ headers: carried.headers ?? {},
1172
+ body: carried.body,
1173
+ redirect: "manual",
1174
+ signal
1175
+ });
1176
+ if (!REDIRECTS.includes(response.status)) return await answerOf(response);
1177
+ const location = response.headers.get("location");
1178
+ if (location === null) {
1179
+ throw new Error(`${target} answered ${response.status} without saying where to`);
1180
+ }
1181
+ await response.body?.cancel();
1182
+ const next = new URL(location, target).toString();
1183
+ carried = KEEPS_THE_METHOD.includes(response.status) ? carried : withoutBody(carried);
1184
+ carried = sameOrigin(target, next) ? carried : withoutCredentials(carried);
1185
+ target = next;
1186
+ }
1187
+ throw new Error(`${url} redirected more than ${HOPS_FOLLOWED} times`);
1188
+ }
1189
+ async function resolvesNothingPrivate(url) {
1190
+ const host = new URL(url).hostname.replace(/^\[|]$/g, "");
1191
+ const found = await Promise.race([
1192
+ lookup(host, { all: true, verbatim: true }).catch(() => []),
1193
+ sleep(LOOKUP_TIMEOUT_MS, null, { ref: false })
1194
+ ]);
1195
+ return found !== null && found.every((one) => publicAddress(one.address));
1196
+ }
1197
+ async function refuseUnlessPublic(url) {
1198
+ if (!publicHttps(url)) throw new Error(`${url} is not a public https URL`);
1199
+ if (!await resolvesNothingPrivate(url)) throw new Error(`${url} resolves to an address we do not reach`);
1200
+ }
1201
+ async function answerOf(response) {
1202
+ const { status, ok } = response;
1203
+ const reader = response.body?.getReader();
1204
+ if (!reader) return { status, ok, body: "", truncated: false };
1205
+ const read = [];
1206
+ let bytes = 0;
1207
+ while (bytes <= BODY_LIMIT_BYTES) {
1208
+ const { done, value } = await reader.read();
1209
+ if (done) return { status, ok, body: text2(read), truncated: false };
1210
+ read.push(value);
1211
+ bytes += value.length;
1212
+ }
1213
+ await reader.cancel();
1214
+ return { status, ok, body: text2(read), truncated: true };
1215
+ }
1216
+ function withoutBody(sent) {
1217
+ const headers = { ...sent.headers };
1218
+ delete headers["content-type"];
1219
+ return { ...sent, method: "GET", headers, body: void 0 };
1220
+ }
1221
+ function withoutCredentials(sent) {
1222
+ const headers = { ...sent.headers };
1223
+ for (const named of CREDENTIALS) delete headers[named];
1224
+ return { ...sent, headers };
1225
+ }
1226
+ function text2(chunks) {
1227
+ const decoder = new TextDecoder();
1228
+ let decoded = "";
1229
+ for (const chunk of chunks) decoded += decoder.decode(chunk, { stream: true });
1230
+ return decoded + decoder.decode();
1231
+ }
1232
+
1119
1233
  // ../core/refusal.ts
1120
1234
  var WalletRefused = class extends Error {
1121
1235
  reason;
@@ -1134,7 +1248,6 @@ var NoWalletAvailable = class extends Error {
1134
1248
 
1135
1249
  // ../core/lnurl.ts
1136
1250
  var VERIFY_WITHOUT_PREIMAGE = ["zeuspay.com", "zeusnuts.com", "ecash.love"];
1137
- var HTTP_TIMEOUT_MS2 = 15e3;
1138
1251
  var BECH32_CHARSET2 = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
1139
1252
  var BECH32_GENERATOR = [996825010, 642813549, 513874426, 1027748829, 705979059];
1140
1253
  var CHECKSUM_SLOTS = [0, 1, 2, 3, 4, 5];
@@ -1299,14 +1412,12 @@ function splitAddress(address) {
1299
1412
  return [user, domain];
1300
1413
  }
1301
1414
  async function fetchJson(url, deadline) {
1302
- if (!publicHttps(url)) throw new Error(`${url} is not a public https URL`);
1303
- const capped = AbortSignal.timeout(HTTP_TIMEOUT_MS2);
1304
- const response = await fetch(url, {
1305
- signal: deadline ? AbortSignal.any([deadline, capped]) : capped,
1306
- headers: { accept: "application/json" }
1307
- });
1308
- if (!response.ok) throw new Error(`${url} answered ${response.status}`);
1309
- return await response.json();
1415
+ const answer = await ask(url, { headers: { accept: "application/json" }, deadline });
1416
+ if (!answer.ok) throw new Error(`${url} answered ${answer.status}`);
1417
+ if (answer.truncated) {
1418
+ throw new Error(`${url} answered with more than the ${BODY_LIMIT_BYTES} bytes we read`);
1419
+ }
1420
+ return JSON.parse(answer.body);
1310
1421
  }
1311
1422
 
1312
1423
  // src/trigger.ts
@@ -1450,6 +1561,7 @@ var FORBIDDEN_IN_SPD = "*";
1450
1561
  async function bankTransfer(params) {
1451
1562
  const currency = params.currency ?? DEFAULT_CURRENCY;
1452
1563
  refuseUnusable(params, currency);
1564
+ await refuseAnOpenGateway(params);
1453
1565
  const spd = shortPaymentDescriptor(params, currency);
1454
1566
  const subject = subjectOf(params.reference, params.amountMinor, currency);
1455
1567
  const polling = new URL(params.verifyUrl);
@@ -1516,12 +1628,14 @@ function shortPaymentDescriptor(params, currency) {
1516
1628
  function major(amountMinor, currency) {
1517
1629
  return (amountMinor / minorScaleOf(currency)).toFixed(minorUnitsOf(currency));
1518
1630
  }
1631
+ async function refuseAnOpenGateway(params) {
1632
+ if (params.allowPublicGateway === true) return;
1633
+ if (await params.gateway.refusesStrangers()) return;
1634
+ throw new Error(
1635
+ "this gateway serves callers with no token, so it is not yours, and its operator would read the amount and the reference off every verify URL. Point at one that answers 401 to a stranger, or say allowPublicGateway"
1636
+ );
1637
+ }
1519
1638
  function refuseUnusable(params, currency) {
1520
- if (!params.gateway.isPrivate && params.allowPublicGateway !== true) {
1521
- throw new Error(
1522
- "this gateway has no token, so it is not yours, and its operator would read the amount and the reference off every verify URL. Give it a token or say allowPublicGateway"
1523
- );
1524
- }
1525
1639
  if (!IBAN.test(params.iban)) throw new Error(`${params.iban} is not an IBAN`);
1526
1640
  if (!Number.isInteger(params.amountMinor) || params.amountMinor <= 0) {
1527
1641
  throw new Error("amountMinor must be a whole number of minor units above zero");
@@ -1560,6 +1674,7 @@ var WHERE_A_PAYER_WRITES = [
1560
1674
  USER_IDENTIFICATION,
1561
1675
  PAYER_REFERENCE
1562
1676
  ];
1677
+ var UTC_OFFSET = /^[+-]\d{4}$/;
1563
1678
  function fioStatement(config) {
1564
1679
  const named = typeof config.token === "string" ? [config.token] : config.token;
1565
1680
  if (named.length === 0) throw new Error("fioStatement needs at least one token to read with");
@@ -1612,9 +1727,21 @@ function asCredit(transaction) {
1612
1727
  amountMinor: Math.round(numberIn(transaction[AMOUNT]) * MINOR_UNITS),
1613
1728
  currency: textIn(transaction[CURRENCY]),
1614
1729
  reference: WHERE_A_PAYER_WRITES.map((column) => textIn(transaction[column])).filter((written) => written.length > 0).join(" "),
1615
- bookedAt: Math.floor(numberIn(transaction[BOOKED_AT]) / MILLIS)
1730
+ bookedAt: bookedAtIn(transaction[BOOKED_AT])
1616
1731
  };
1617
1732
  }
1733
+ function bookedAtIn(cell) {
1734
+ const value = cell?.value;
1735
+ if (typeof value === "number") return Math.floor(value / MILLIS);
1736
+ const at = Date.parse(asIso8601(textIn(cell)));
1737
+ return Number.isFinite(at) ? Math.floor(at / MILLIS) : 0;
1738
+ }
1739
+ function asIso8601(booked) {
1740
+ const day = booked.slice(0, DATE_CHARS);
1741
+ const zone = booked.slice(DATE_CHARS);
1742
+ if (!UTC_OFFSET.test(zone)) return `${day}T00:00:00Z`;
1743
+ return `${day}T00:00:00${zone.slice(0, 3)}:${zone.slice(3)}`;
1744
+ }
1618
1745
  function numberIn(cell) {
1619
1746
  const value = Number(cell?.value ?? Number.NaN);
1620
1747
  return Number.isFinite(value) ? value : 0;
@@ -1627,6 +1754,149 @@ function asDate(unix) {
1627
1754
  return new Date(unix * MILLIS).toISOString().slice(0, DATE_CHARS);
1628
1755
  }
1629
1756
 
1757
+ // src/qr.ts
1758
+ import { encode } from "uqr";
1759
+ var SAFE_COLOR = /^#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$|^rgba?\([\d.,%/\s]+\)$|^[a-z]+$/i;
1760
+ function encodeForQr(destination) {
1761
+ const scanned = isLnAddress(destination) ? destination : destination.toUpperCase();
1762
+ return `LIGHTNING:${scanned}`;
1763
+ }
1764
+ function invoiceToSvg(destination, options) {
1765
+ return svgOf(encodeForQr(destination), options);
1766
+ }
1767
+ function invoiceToDataUrl(destination, options) {
1768
+ return asDataUrl(invoiceToSvg(destination, options));
1769
+ }
1770
+ function lnurlToSvg(endpoint, options) {
1771
+ return svgOf(toLnurl(endpoint), options);
1772
+ }
1773
+ function lnurlToDataUrl(endpoint, options) {
1774
+ return asDataUrl(lnurlToSvg(endpoint, options));
1775
+ }
1776
+ function spdToSvg(spd, options) {
1777
+ return svgOf(spd, options);
1778
+ }
1779
+ function spdToDataUrl(spd, options) {
1780
+ return asDataUrl(spdToSvg(spd, options));
1781
+ }
1782
+ function qrToSvg(payload, options) {
1783
+ return svgOf(payload, options);
1784
+ }
1785
+ function qrToDataUrl(payload, options) {
1786
+ return asDataUrl(qrToSvg(payload, options));
1787
+ }
1788
+ function svgOf(scanned, options) {
1789
+ const size = options?.size ?? 256;
1790
+ const color = options?.color ?? "#000";
1791
+ if (!SAFE_COLOR.test(color)) throw new Error(`Invalid color: ${color}`);
1792
+ return renderSvg(scanned, size, color);
1793
+ }
1794
+ function asDataUrl(svg) {
1795
+ return `data:image/svg+xml,${encodeURIComponent(svg)}`;
1796
+ }
1797
+ function isLnAddress(destination) {
1798
+ const at = destination.indexOf("@");
1799
+ return at > 0 && destination.slice(at + 1).includes(".");
1800
+ }
1801
+ function renderSvg(content, size, color) {
1802
+ const { data } = encode(content);
1803
+ const modules = data.length;
1804
+ const cellSize = size / modules;
1805
+ let path = "";
1806
+ for (let y = 0; y < modules; y++) {
1807
+ for (let x = 0; x < modules; x++) {
1808
+ if (data[y][x]) {
1809
+ path += `M${x * cellSize},${y * cellSize}h${cellSize}v${cellSize}h-${cellSize}z`;
1810
+ }
1811
+ }
1812
+ }
1813
+ return [
1814
+ `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${size} ${size}" width="${size}" height="${size}">`,
1815
+ `<rect width="100%" height="100%" fill="white"/>`,
1816
+ `<path d="${path}" fill="${color}"/>`,
1817
+ `</svg>`
1818
+ ].join("");
1819
+ }
1820
+
1821
+ // src/rail.ts
1822
+ var BANK = "bank";
1823
+ var LIGHTNING = "lightning";
1824
+ function bankRail(config) {
1825
+ return async (order) => {
1826
+ const expiresAt = config.expiresAt(order);
1827
+ const transfer = await bankTransfer({
1828
+ gateway: config.gateway,
1829
+ secret: config.secret,
1830
+ iban: config.iban,
1831
+ verifyUrl: config.verifyUrl,
1832
+ reference: order.reference,
1833
+ amountMinor: order.amountMinor,
1834
+ currency: order.currency,
1835
+ expiresAt,
1836
+ trigger: config.trigger,
1837
+ sealed: await config.sealed?.(order),
1838
+ variableSymbol: config.variableSymbol?.(order),
1839
+ allowPublicGateway: config.allowPublicGateway
1840
+ });
1841
+ return {
1842
+ id: transfer.id,
1843
+ rail: config.name ?? BANK,
1844
+ scan: transfer.spd,
1845
+ qr: transfer.spd,
1846
+ expiresAt
1847
+ };
1848
+ };
1849
+ }
1850
+ function lightningRail(config) {
1851
+ return async (order) => {
1852
+ const payment = await config.gateway.createPayment(
1853
+ {
1854
+ lnAddresses: config.lnAddresses,
1855
+ amountMsat: await config.amountMsat(order),
1856
+ webhookUrl: config.webhookUrl,
1857
+ webhookSecret: config.webhookSecret
1858
+ },
1859
+ { idempotencyKey: config.idempotencyKey?.(order), trigger: config.trigger }
1860
+ );
1861
+ return {
1862
+ id: payment.id,
1863
+ rail: config.name ?? LIGHTNING,
1864
+ scan: payment.bolt11,
1865
+ qr: encodeForQr(payment.bolt11),
1866
+ expiresAt: payment.expiresAt
1867
+ };
1868
+ };
1869
+ }
1870
+ function blindLightningRail(config) {
1871
+ return async (order) => {
1872
+ const resolved = await invoiceFrom(config.lnAddresses, await config.amountMsat(order));
1873
+ const watched = await config.gateway.watchPayment({
1874
+ paymentHash: resolved.paymentHash,
1875
+ verifyUrl: resolved.verifyUrl,
1876
+ expiresAt: resolved.expiresAt,
1877
+ trigger: config.trigger,
1878
+ sealed: await config.sealed?.(order)
1879
+ });
1880
+ return {
1881
+ id: watched.id,
1882
+ rail: config.name ?? LIGHTNING,
1883
+ scan: resolved.bolt11,
1884
+ qr: encodeForQr(resolved.bolt11),
1885
+ expiresAt: resolved.expiresAt
1886
+ };
1887
+ };
1888
+ }
1889
+ async function invoiceFrom(lnAddresses, amountMsat) {
1890
+ try {
1891
+ return await resolve(lnAddresses, amountMsat);
1892
+ } catch (refused) {
1893
+ if (refused instanceof NoWalletAvailable) {
1894
+ throw new NoWalletAvailableError({ title: refused.message }, refused.wallets);
1895
+ }
1896
+ throw refused;
1897
+ }
1898
+ }
1899
+
1630
1900
  // src/price.ts
1631
1901
  var MSAT_PER_BTC2 = 1e11;
1632
1902
  var BASIS_POINTS = 1e4;
@@ -1749,64 +2019,6 @@ function refusals2(answers) {
1749
2019
  ).join("; ");
1750
2020
  }
1751
2021
 
1752
- // src/qr.ts
1753
- import { encode } from "uqr";
1754
- var SAFE_COLOR = /^#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$|^rgba?\([\d.,%/\s]+\)$|^[a-z]+$/i;
1755
- function encodeForQr(destination) {
1756
- const scanned = isLnAddress(destination) ? destination : destination.toUpperCase();
1757
- return `LIGHTNING:${scanned}`;
1758
- }
1759
- function invoiceToSvg(destination, options) {
1760
- return svgOf(encodeForQr(destination), options);
1761
- }
1762
- function invoiceToDataUrl(destination, options) {
1763
- return asDataUrl(invoiceToSvg(destination, options));
1764
- }
1765
- function lnurlToSvg(endpoint, options) {
1766
- return svgOf(toLnurl(endpoint), options);
1767
- }
1768
- function lnurlToDataUrl(endpoint, options) {
1769
- return asDataUrl(lnurlToSvg(endpoint, options));
1770
- }
1771
- function spdToSvg(spd, options) {
1772
- return svgOf(spd, options);
1773
- }
1774
- function spdToDataUrl(spd, options) {
1775
- return asDataUrl(spdToSvg(spd, options));
1776
- }
1777
- function svgOf(scanned, options) {
1778
- const size = options?.size ?? 256;
1779
- const color = options?.color ?? "#000";
1780
- if (!SAFE_COLOR.test(color)) throw new Error(`Invalid color: ${color}`);
1781
- return renderSvg(scanned, size, color);
1782
- }
1783
- function asDataUrl(svg) {
1784
- return `data:image/svg+xml,${encodeURIComponent(svg)}`;
1785
- }
1786
- function isLnAddress(destination) {
1787
- const at = destination.indexOf("@");
1788
- return at > 0 && destination.slice(at + 1).includes(".");
1789
- }
1790
- function renderSvg(content, size, color) {
1791
- const { data } = encode(content);
1792
- const modules = data.length;
1793
- const cellSize = size / modules;
1794
- let path = "";
1795
- for (let y = 0; y < modules; y++) {
1796
- for (let x = 0; x < modules; x++) {
1797
- if (data[y][x]) {
1798
- path += `M${x * cellSize},${y * cellSize}h${cellSize}v${cellSize}h-${cellSize}z`;
1799
- }
1800
- }
1801
- }
1802
- return [
1803
- `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${size} ${size}" width="${size}" height="${size}">`,
1804
- `<rect width="100%" height="100%" fill="white"/>`,
1805
- `<path d="${path}" fill="${color}"/>`,
1806
- `</svg>`
1807
- ].join("");
1808
- }
1809
-
1810
2022
  // src/webhook.ts
1811
2023
  var SIGNATURE_HEADER = "x-signature";
1812
2024
  var TIMESTAMP_HEADER = "x-timestamp";
@@ -1819,9 +2031,9 @@ async function verifyWebhookSignature(body, signature, secret, timestamp, option
1819
2031
  }
1820
2032
  async function parseWebhook(body, signature, secret, timestamp, options = {}) {
1821
2033
  if (!await verifyWebhookSignature(body, signature, secret, timestamp, options)) return null;
1822
- const text2 = typeof body === "string" ? body : new TextDecoder().decode(body);
2034
+ const text3 = typeof body === "string" ? body : new TextDecoder().decode(body);
1823
2035
  try {
1824
- return paymentFromWire(JSON.parse(text2));
2036
+ return paymentFromWire(JSON.parse(text3));
1825
2037
  } catch {
1826
2038
  return null;
1827
2039
  }
@@ -1860,9 +2072,11 @@ export {
1860
2072
  REQUEST_IN_FLIGHT,
1861
2073
  ThunderBridge,
1862
2074
  UnverifiedRecipientError,
2075
+ bankRail,
1863
2076
  bankTransfer,
1864
2077
  bankVerifyEndpoint,
1865
2078
  bitstamp,
2079
+ blindLightningRail,
1866
2080
  coinbase,
1867
2081
  coinmate,
1868
2082
  decodeInvoice,
@@ -1872,6 +2086,7 @@ export {
1872
2086
  isProblemType,
1873
2087
  isProvablyPaid,
1874
2088
  kraken,
2089
+ lightningRail,
1875
2090
  lnurlPayEndpoint,
1876
2091
  lnurlToDataUrl,
1877
2092
  lnurlToSvg,
@@ -1884,6 +2099,8 @@ export {
1884
2099
  preimageMatchesHash,
1885
2100
  proveOrigin,
1886
2101
  proveSettlement,
2102
+ qrToDataUrl,
2103
+ qrToSvg,
1887
2104
  seal,
1888
2105
  spdToDataUrl,
1889
2106
  spdToSvg,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thunder-bridge",
3
- "version": "0.8.1",
3
+ "version": "0.8.2",
4
4
  "description": "Trustless JavaScript client for the Thunder Bridge Lightning payment gateway. Proves the invoice came from your own wallet before the payer sees it.",
5
5
  "author": "i-am-fatik",
6
6
  "homepage": "https://agora.gripe/en/tools/thunder-bridge",
@@ -39,10 +39,10 @@
39
39
  },
40
40
  "devDependencies": {
41
41
  "@biomejs/biome": "^2.4.16",
42
- "@types/node": "^25.5.0",
42
+ "@types/node": "^24.13.3",
43
43
  "tsup": "^8.0.0",
44
44
  "typescript": "^5.5.0",
45
- "vitest": "^2.0.0"
45
+ "vitest": "^4.1.10"
46
46
  },
47
47
  "keywords": [
48
48
  "lightning",