thunder-bridge 0.8.4 → 0.8.8

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/server.js CHANGED
@@ -4,6 +4,16 @@ function bytesToHex(bytes) {
4
4
  for (const byte of bytes) hex += byte.toString(16).padStart(2, "0");
5
5
  return hex;
6
6
  }
7
+ function hexToBytes(hex) {
8
+ const bytes = new Uint8Array(hex.length >> 1);
9
+ for (let at = 0; at < bytes.length; at++) {
10
+ bytes[at] = parseInt(hex.slice(at * 2, at * 2 + 2), 16);
11
+ }
12
+ return bytes;
13
+ }
14
+ function isHex(text2) {
15
+ return /^[0-9a-f]+$/i.test(text2) && text2.length % 2 === 0;
16
+ }
7
17
 
8
18
  // ../core/hmac.ts
9
19
  async function hmacHex(secret, payload) {
@@ -211,6 +221,10 @@ function decodeInvoice(bolt11) {
211
221
  expiresAt: expiryOf(parts.words, tagged)
212
222
  };
213
223
  }
224
+ function preimageMatchesHash(preimage, paymentHash) {
225
+ if (!isHex(preimage)) return false;
226
+ return bytesToHex(sha256(hexToBytes(preimage))) === paymentHash.toLowerCase();
227
+ }
214
228
  function splitBech32(bolt11) {
215
229
  const lower = bolt11.toLowerCase();
216
230
  if (isBolt12Encoding(lower)) return null;
@@ -391,19 +405,19 @@ async function refuseUnlessPublic(url) {
391
405
  if (!await resolvesNothingPrivate(url)) throw new Error(`${url} resolves to an address we do not reach`);
392
406
  }
393
407
  async function answerOf(response) {
394
- const { status, ok } = response;
408
+ const { status, ok, headers } = response;
395
409
  const reader = response.body?.getReader();
396
- if (!reader) return { status, ok, body: "", truncated: false };
410
+ if (!reader) return { status, ok, body: "", truncated: false, headers };
397
411
  const read = [];
398
412
  let bytes = 0;
399
413
  while (bytes <= BODY_LIMIT_BYTES) {
400
414
  const { done, value } = await reader.read();
401
- if (done) return { status, ok, body: text(read), truncated: false };
415
+ if (done) return { status, ok, body: text(read), truncated: false, headers };
402
416
  read.push(value);
403
417
  bytes += value.length;
404
418
  }
405
419
  await reader.cancel();
406
- return { status, ok, body: text(read), truncated: true };
420
+ return { status, ok, body: text(read), truncated: true, headers };
407
421
  }
408
422
  function withoutBody(sent) {
409
423
  const headers = { ...sent.headers };
@@ -441,6 +455,10 @@ var NoWalletAvailable = class extends Error {
441
455
  // ../core/lnurl.ts
442
456
  var VERIFY_WITHOUT_PREIMAGE = ["zeuspay.com", "zeusnuts.com", "ecash.love"];
443
457
  var RESOLVE_TIMEOUT_MS = 3e4;
458
+ var MIN_PACE_SECS = 1;
459
+ var MAX_PACE_SECS = 3600;
460
+ var MIN_PER_SECOND = 0.01;
461
+ var MAX_PER_SECOND = 100;
444
462
  async function resolve(addresses, amountMsat) {
445
463
  const served = await firstThatServes(
446
464
  addresses,
@@ -528,6 +546,31 @@ function decodeIssued(address, bolt11) {
528
546
  }
529
547
  return { paymentHash, descriptionHash, amountMsat, expiresAt };
530
548
  }
549
+ async function checkSettled(verifyUrl, paymentHash) {
550
+ const answer = await answeredJson(verifyUrl);
551
+ const asked = {
552
+ pace: paceAskedFor(answer.headers),
553
+ ceiling: ceilingAskedFor(answer.headers)
554
+ };
555
+ if (!answer.said.settled || !answer.said.preimage) return { preimage: null, ...asked };
556
+ if (!preimageMatchesHash(answer.said.preimage, paymentHash)) {
557
+ throw new Error(`verify returned a preimage that does not hash to ${paymentHash}`);
558
+ }
559
+ return { preimage: answer.said.preimage, ...asked };
560
+ }
561
+ function paceAskedFor(headers) {
562
+ const asked = /max-age\s*=\s*(\d+)/i.exec(headers.get("cache-control") ?? "");
563
+ if (!asked) return null;
564
+ return Math.min(Math.max(Number(asked[1]), MIN_PACE_SECS), MAX_PACE_SECS);
565
+ }
566
+ function ceilingAskedFor(headers) {
567
+ const asked = /^\s*(\d+)\s*(?:;\s*w\s*=\s*(\d+))?/i.exec(headers.get("ratelimit-limit") ?? "");
568
+ if (!asked) return null;
569
+ const perWindow = Number(asked[1]);
570
+ const window = asked[2] === void 0 ? 1 : Number(asked[2]);
571
+ if (perWindow === 0 || window === 0) return null;
572
+ return Math.min(Math.max(perWindow / window, MIN_PER_SECOND), MAX_PER_SECOND);
573
+ }
531
574
  function cannotReleaseAPreimage(host) {
532
575
  const lowered = host.toLowerCase();
533
576
  return VERIFY_WITHOUT_PREIMAGE.some(
@@ -546,12 +589,15 @@ function splitAddress(address) {
546
589
  return [user, domain];
547
590
  }
548
591
  async function fetchJson(url, deadline) {
592
+ return (await answeredJson(url, deadline)).said;
593
+ }
594
+ async function answeredJson(url, deadline) {
549
595
  const answer = await ask(url, { headers: { accept: "application/json" }, deadline });
550
596
  if (!answer.ok) throw new Error(`${url} answered ${answer.status}`);
551
597
  if (answer.truncated) {
552
598
  throw new Error(`${url} answered with more than the ${BODY_LIMIT_BYTES} bytes we read`);
553
599
  }
554
- return JSON.parse(answer.body);
600
+ return { said: JSON.parse(answer.body), headers: answer.headers };
555
601
  }
556
602
 
557
603
  // ../core/sealed.ts
@@ -573,6 +619,22 @@ async function seal(secret, plaintext) {
573
619
  joined.set(cipher, iv.length);
574
620
  return `${VERSION}.${toBase64Url(joined)}`;
575
621
  }
622
+ async function unseal(secret, sealed) {
623
+ const key = await keyFor(secret);
624
+ if (!sealed.startsWith(`${VERSION}.`)) return null;
625
+ const joined = fromBase64Url(sealed.slice(VERSION.length + 1));
626
+ if (joined === null || joined.length <= IV_BYTES) return null;
627
+ try {
628
+ const body = await crypto.subtle.decrypt(
629
+ { name: "AES-GCM", iv: joined.slice(0, IV_BYTES) },
630
+ key,
631
+ joined.slice(IV_BYTES)
632
+ );
633
+ return new TextDecoder().decode(body);
634
+ } catch {
635
+ return null;
636
+ }
637
+ }
576
638
  async function keyFor(secret) {
577
639
  if (secret.length < MIN_SECRET_CHARS) {
578
640
  throw new Error(`the sealing secret needs ${MIN_SECRET_CHARS} characters of randomness`);
@@ -597,6 +659,17 @@ function toBase64Url(bytes) {
597
659
  for (const byte of bytes) binary += String.fromCharCode(byte);
598
660
  return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
599
661
  }
662
+ function fromBase64Url(text2) {
663
+ if (text2.length === 0 || !/^[A-Za-z0-9_-]+$/.test(text2)) return null;
664
+ try {
665
+ const binary = atob(text2.replace(/-/g, "+").replace(/_/g, "/"));
666
+ const bytes = new Uint8Array(binary.length);
667
+ for (let at = 0; at < bytes.length; at++) bytes[at] = binary.charCodeAt(at);
668
+ return bytes;
669
+ } catch {
670
+ return null;
671
+ }
672
+ }
600
673
 
601
674
  // src/errors.ts
602
675
  var ProblemError = class extends Error {
@@ -730,14 +803,46 @@ function isLnAddress(destination) {
730
803
  return at > 0 && destination.slice(at + 1).includes(".");
731
804
  }
732
805
 
806
+ // src/relay.ts
807
+ var DEFAULT_POLL_EVERY_SECS = 5;
808
+ var WALLET = "w";
809
+ function lightningVerifyEndpoint(config) {
810
+ const paced = {
811
+ "cache-control": `max-age=${config.pollEverySecs ?? DEFAULT_POLL_EVERY_SECS}`
812
+ };
813
+ return async (request) => {
814
+ const sealed = new URL(request.url).searchParams.get(WALLET);
815
+ if (sealed === null) return Response.json({ settled: false }, { status: 400 });
816
+ const opened = await unseal(config.secret, sealed);
817
+ if (opened === null) return Response.json({ settled: false }, { status: 403 });
818
+ const wallet = JSON.parse(opened);
819
+ const asked = await checkSettled(wallet.url, wallet.hash).catch(() => null);
820
+ if (asked === null) return Response.json({ settled: false }, { status: 502 });
821
+ return Response.json(
822
+ { settled: asked.preimage !== null, preimage: asked.preimage },
823
+ { headers: paced }
824
+ );
825
+ };
826
+ }
827
+ async function relayedVerifyUrl(endpoint, wallet, secret) {
828
+ const relayed = new URL(endpoint);
829
+ relayed.searchParams.set(WALLET, await seal(secret, JSON.stringify(wallet)));
830
+ return relayed.toString();
831
+ }
832
+
733
833
  // src/rail.ts
734
834
  var LIGHTNING = "lightning";
735
835
  function blindLightningRail(config) {
736
836
  return async (order) => {
737
837
  const resolved = await invoiceFrom(config.lnAddresses, await config.amountMsat(order));
838
+ const relay = config.relayVerifyThrough;
738
839
  const watched = await config.gateway.watchPayment({
739
840
  paymentHash: resolved.paymentHash,
740
- verifyUrl: resolved.verifyUrl,
841
+ verifyUrl: relay ? await relayedVerifyUrl(
842
+ relay.endpoint,
843
+ { url: resolved.verifyUrl, hash: resolved.paymentHash },
844
+ relay.secret
845
+ ) : resolved.verifyUrl,
741
846
  expiresAt: resolved.expiresAt,
742
847
  trigger: config.trigger,
743
848
  sealed: await config.sealed?.(order),
@@ -765,5 +870,7 @@ async function invoiceFrom(lnAddresses, amountMsat) {
765
870
  }
766
871
  export {
767
872
  blindLightningRail,
768
- lnurlPayEndpoint
873
+ lightningVerifyEndpoint,
874
+ lnurlPayEndpoint,
875
+ relayedVerifyUrl
769
876
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thunder-bridge",
3
- "version": "0.8.4",
3
+ "version": "0.8.8",
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",