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/README.md +105 -7
- package/dist/index.cjs +122 -17
- package/dist/index.d.cts +34 -8
- package/dist/index.d.ts +34 -8
- package/dist/index.js +120 -17
- package/dist/{rail-QZtUN1D-.d.cts → rail-J8QoYbSr.d.cts} +20 -1
- package/dist/{rail-QZtUN1D-.d.ts → rail-J8QoYbSr.d.ts} +20 -1
- package/dist/server.cjs +117 -8
- package/dist/server.d.cts +42 -3
- package/dist/server.d.ts +42 -3
- package/dist/server.js +114 -7
- package/package.json +1 -1
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:
|
|
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
|
-
|
|
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.
|
|
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",
|