thunder-bridge 0.8.10 → 1.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.
- package/README.md +60 -62
- package/dist/index.cjs +291 -118
- package/dist/index.d.cts +63 -10
- package/dist/index.d.ts +63 -10
- package/dist/index.js +287 -118
- package/dist/{rail-J8QoYbSr.d.cts → rail-CL9QkiHo.d.cts} +30 -6
- package/dist/{rail-J8QoYbSr.d.ts → rail-CL9QkiHo.d.ts} +30 -6
- package/dist/server.cjs +1 -2
- package/dist/server.d.cts +2 -2
- package/dist/server.d.ts +2 -2
- package/dist/server.js +1 -2
- package/openapi.yaml +43 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -328,6 +328,105 @@ function wordsToBytes(words) {
|
|
|
328
328
|
return Uint8Array.from(bytes);
|
|
329
329
|
}
|
|
330
330
|
|
|
331
|
+
// ../core/ed25519.ts
|
|
332
|
+
var ALGORITHM = { name: "Ed25519" };
|
|
333
|
+
var SEED_BYTES = 32;
|
|
334
|
+
var SIGNATURE_BYTES = 64;
|
|
335
|
+
var PUBLIC_KEY_BYTES = 32;
|
|
336
|
+
var PKCS8_HEADER = hexToBytes("302e020100300506032b657004220420");
|
|
337
|
+
async function signingKeyFromSeed(seed) {
|
|
338
|
+
if (seed.length !== SEED_BYTES) throw new Error(`an ed25519 seed is ${SEED_BYTES} bytes`);
|
|
339
|
+
const wrapped = new Uint8Array(PKCS8_HEADER.length + SEED_BYTES);
|
|
340
|
+
wrapped.set(PKCS8_HEADER);
|
|
341
|
+
wrapped.set(seed, PKCS8_HEADER.length);
|
|
342
|
+
const key = await crypto.subtle.importKey("pkcs8", asBuffer(wrapped), ALGORITHM, true, ["sign"]);
|
|
343
|
+
const jwk = await crypto.subtle.exportKey("jwk", key);
|
|
344
|
+
return {
|
|
345
|
+
publicKeyHex: bytesToHex(fromBase64Url(jwk.x ?? "")),
|
|
346
|
+
sign: async (payload) => bytesToHex(new Uint8Array(await crypto.subtle.sign(ALGORITHM, key, asBuffer(payload))))
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
async function verifyHex(publicKeyHex, signatureHex, payload) {
|
|
350
|
+
if (!isHex(publicKeyHex) || publicKeyHex.length !== PUBLIC_KEY_BYTES * 2) return false;
|
|
351
|
+
if (!isHex(signatureHex) || signatureHex.length !== SIGNATURE_BYTES * 2) return false;
|
|
352
|
+
try {
|
|
353
|
+
const key = await crypto.subtle.importKey(
|
|
354
|
+
"raw",
|
|
355
|
+
asBuffer(hexToBytes(publicKeyHex)),
|
|
356
|
+
ALGORITHM,
|
|
357
|
+
false,
|
|
358
|
+
["verify"]
|
|
359
|
+
);
|
|
360
|
+
return await crypto.subtle.verify(
|
|
361
|
+
ALGORITHM,
|
|
362
|
+
key,
|
|
363
|
+
asBuffer(hexToBytes(signatureHex)),
|
|
364
|
+
asBuffer(payload)
|
|
365
|
+
);
|
|
366
|
+
} catch {
|
|
367
|
+
return false;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
function fromBase64Url(text2) {
|
|
371
|
+
const padded = text2.replace(/-/g, "+").replace(/_/g, "/");
|
|
372
|
+
const raw = atob(padded + "=".repeat((4 - padded.length % 4) % 4));
|
|
373
|
+
const bytes = new Uint8Array(raw.length);
|
|
374
|
+
for (let at = 0; at < raw.length; at++) bytes[at] = raw.charCodeAt(at);
|
|
375
|
+
return bytes;
|
|
376
|
+
}
|
|
377
|
+
function asBuffer(bytes) {
|
|
378
|
+
return bytes.buffer instanceof ArrayBuffer ? bytes : new Uint8Array(bytes);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// ../core/hmac.ts
|
|
382
|
+
async function hmacHex(secret, payload) {
|
|
383
|
+
const raw = typeof secret === "string" ? new TextEncoder().encode(secret) : secret;
|
|
384
|
+
const key = await crypto.subtle.importKey(
|
|
385
|
+
"raw",
|
|
386
|
+
asBuffer2(raw),
|
|
387
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
388
|
+
false,
|
|
389
|
+
["sign"]
|
|
390
|
+
);
|
|
391
|
+
const message = typeof payload === "string" ? new TextEncoder().encode(payload) : payload;
|
|
392
|
+
return bytesToHex(new Uint8Array(await crypto.subtle.sign("HMAC", key, asBuffer2(message))));
|
|
393
|
+
}
|
|
394
|
+
function equalInConstantTime(one, other) {
|
|
395
|
+
if (one.length !== other.length) return false;
|
|
396
|
+
let difference = 0;
|
|
397
|
+
for (let at = 0; at < one.length; at++) difference |= one.charCodeAt(at) ^ other.charCodeAt(at);
|
|
398
|
+
return difference === 0;
|
|
399
|
+
}
|
|
400
|
+
function asBuffer2(bytes) {
|
|
401
|
+
return bytes instanceof Uint8Array && bytes.buffer instanceof ArrayBuffer ? bytes : new Uint8Array(bytes);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// ../core/caller.ts
|
|
405
|
+
var KEY_HEADER = "x-client-key";
|
|
406
|
+
var SIGNATURE_HEADER = "x-signature";
|
|
407
|
+
var TIMESTAMP_HEADER = "x-timestamp";
|
|
408
|
+
var PREFIX = "ed25519=";
|
|
409
|
+
var SIGNING_LABEL = "request-signing";
|
|
410
|
+
async function callerKey(secret) {
|
|
411
|
+
return await signingKeyFromSeed(hexToBytes(await hmacHex(secret, SIGNING_LABEL)));
|
|
412
|
+
}
|
|
413
|
+
function paymentNamedBy(caller, paymentHash) {
|
|
414
|
+
return sha256Hex(`${caller}.payment-id.${paymentHash}`);
|
|
415
|
+
}
|
|
416
|
+
async function signedAs(key, method, path, body) {
|
|
417
|
+
const timestamp = String(Math.floor(Date.now() / 1e3));
|
|
418
|
+
return {
|
|
419
|
+
[KEY_HEADER]: key.publicKeyHex,
|
|
420
|
+
[TIMESTAMP_HEADER]: timestamp,
|
|
421
|
+
[SIGNATURE_HEADER]: `${PREFIX}${await key.sign(spoken(timestamp, method, path, body))}`
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
function spoken(timestamp, method, path, body) {
|
|
425
|
+
return new TextEncoder().encode(
|
|
426
|
+
`${timestamp}.${method.toUpperCase()}.${path}.${sha256Hex(body)}`
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
|
|
331
430
|
// ../core/url.ts
|
|
332
431
|
var PRIVATE_SUFFIXES = ["localhost", "local", "internal", "lan", "arpa", "test", "invalid"];
|
|
333
432
|
function publicHttps(raw) {
|
|
@@ -476,7 +575,7 @@ function createRequestBody(params, trigger) {
|
|
|
476
575
|
return JSON.stringify({
|
|
477
576
|
ln_addresses: params.lnAddresses,
|
|
478
577
|
incoming_amount: toAmount(params.amountMsat),
|
|
479
|
-
webhook: params.webhookUrl ? { url: params.webhookUrl
|
|
578
|
+
webhook: params.webhookUrl ? { url: params.webhookUrl } : void 0,
|
|
480
579
|
trigger: trigger ?? void 0
|
|
481
580
|
});
|
|
482
581
|
}
|
|
@@ -542,9 +641,21 @@ function watchRequestBody(params, trigger) {
|
|
|
542
641
|
expires_at: expiresAt.toISOString(),
|
|
543
642
|
trigger: trigger ?? void 0,
|
|
544
643
|
sealed: params.sealed,
|
|
545
|
-
webhook: params.webhookUrl ? { url: params.webhookUrl
|
|
644
|
+
webhook: params.webhookUrl ? { url: params.webhookUrl } : void 0
|
|
546
645
|
});
|
|
547
646
|
}
|
|
647
|
+
function settlementFromWire(body) {
|
|
648
|
+
const wire = asObject(body);
|
|
649
|
+
if (wire === null) return null;
|
|
650
|
+
const id = text(wire["id"]);
|
|
651
|
+
const paymentHash = text(wire["payment_hash"]);
|
|
652
|
+
const settledAt = secondsFrom(wire["settled_at"]);
|
|
653
|
+
const status = wire["status"];
|
|
654
|
+
const preimage = wire["preimage"] ?? null;
|
|
655
|
+
if (id === null || paymentHash === null || settledAt === null || !isStatus(status)) return null;
|
|
656
|
+
if (preimage !== null && typeof preimage !== "string") return null;
|
|
657
|
+
return { id, status, paymentHash, preimage, settledAt };
|
|
658
|
+
}
|
|
548
659
|
function triggerEventFromWire(body) {
|
|
549
660
|
const wire = asObject(body);
|
|
550
661
|
if (wire === null) return null;
|
|
@@ -642,11 +753,14 @@ var ThunderBridge = class {
|
|
|
642
753
|
baseUrl;
|
|
643
754
|
verify;
|
|
644
755
|
token;
|
|
756
|
+
secret;
|
|
645
757
|
strangers = null;
|
|
758
|
+
speaks = null;
|
|
646
759
|
constructor(baseUrl, options) {
|
|
647
760
|
this.baseUrl = baseUrl.replace(/\/+$/, "");
|
|
648
761
|
this.verify = options?.verify ?? true;
|
|
649
762
|
this.token = options?.token ?? null;
|
|
763
|
+
this.secret = options?.secret ?? null;
|
|
650
764
|
}
|
|
651
765
|
/**
|
|
652
766
|
* Whether a token was given, which is what makes an instance yours: a gateway
|
|
@@ -677,15 +791,16 @@ var ThunderBridge = class {
|
|
|
677
791
|
* and `GatewayCheatError` when what comes back is not what you asked for
|
|
678
792
|
*/
|
|
679
793
|
async createPayment(params, options) {
|
|
680
|
-
const
|
|
794
|
+
const sent = createRequestBody(
|
|
795
|
+
params,
|
|
796
|
+
options?.trigger ? sha256Hex(unguessable(options.trigger)) : null
|
|
797
|
+
);
|
|
798
|
+
const headers = await this.sending("/incoming-payments", sent);
|
|
681
799
|
if (options?.idempotencyKey) headers["idempotency-key"] = options.idempotencyKey;
|
|
682
800
|
const response = await fetch(`${this.baseUrl}/incoming-payments`, {
|
|
683
801
|
method: "POST",
|
|
684
802
|
headers,
|
|
685
|
-
body:
|
|
686
|
-
params,
|
|
687
|
-
options?.trigger ? sha256Hex(unguessable(options.trigger)) : null
|
|
688
|
-
)
|
|
803
|
+
body: sent
|
|
689
804
|
});
|
|
690
805
|
if (!response.ok) throw await problemFrom(response);
|
|
691
806
|
const payment = await paymentFrom(response);
|
|
@@ -700,10 +815,11 @@ var ThunderBridge = class {
|
|
|
700
815
|
* it for one, and asking mints it
|
|
701
816
|
*/
|
|
702
817
|
async createQuote(params) {
|
|
818
|
+
const sent = quoteRequestBody(params);
|
|
703
819
|
const response = await fetch(`${this.baseUrl}/quotes`, {
|
|
704
820
|
method: "POST",
|
|
705
|
-
headers: this.sending(),
|
|
706
|
-
body:
|
|
821
|
+
headers: await this.sending("/quotes", sent),
|
|
822
|
+
body: sent
|
|
707
823
|
});
|
|
708
824
|
if (!response.ok) throw await problemFrom(response);
|
|
709
825
|
const quote = quoteFromWire(await response.json().catch(() => null));
|
|
@@ -734,8 +850,9 @@ var ThunderBridge = class {
|
|
|
734
850
|
return body.public_key;
|
|
735
851
|
}
|
|
736
852
|
async getPayment(id) {
|
|
737
|
-
const
|
|
738
|
-
|
|
853
|
+
const path = `/incoming-payments/${encodeURIComponent(id)}`;
|
|
854
|
+
const response = await fetch(`${this.baseUrl}${path}`, {
|
|
855
|
+
headers: await this.reading("GET", path)
|
|
739
856
|
});
|
|
740
857
|
if (response.status === 404) return null;
|
|
741
858
|
if (!response.ok) throw await problemFrom(response);
|
|
@@ -747,8 +864,9 @@ var ThunderBridge = class {
|
|
|
747
864
|
* `getPayment` refuses it and this reads the shape both rails share
|
|
748
865
|
*/
|
|
749
866
|
async getWatched(id) {
|
|
750
|
-
const
|
|
751
|
-
|
|
867
|
+
const path = `/incoming-payments/${encodeURIComponent(id)}`;
|
|
868
|
+
const response = await fetch(`${this.baseUrl}${path}`, {
|
|
869
|
+
headers: await this.reading("GET", path)
|
|
752
870
|
});
|
|
753
871
|
if (response.status === 404) return null;
|
|
754
872
|
if (!response.ok) throw await problemFrom(response);
|
|
@@ -771,9 +889,9 @@ var ThunderBridge = class {
|
|
|
771
889
|
* pretend otherwise
|
|
772
890
|
*/
|
|
773
891
|
async listPayments(limit) {
|
|
774
|
-
const
|
|
775
|
-
const response = await fetch(`${this.baseUrl}
|
|
776
|
-
headers: this.reading()
|
|
892
|
+
const path = `/incoming-payments${limit === void 0 ? "" : `?limit=${limit}`}`;
|
|
893
|
+
const response = await fetch(`${this.baseUrl}${path}`, {
|
|
894
|
+
headers: await this.reading("GET", path)
|
|
777
895
|
});
|
|
778
896
|
if (!response.ok) throw await problemFrom(response);
|
|
779
897
|
const body = await response.json().catch(() => null);
|
|
@@ -941,13 +1059,14 @@ var ThunderBridge = class {
|
|
|
941
1059
|
* the watcher needs goes in `sealed`, which the gateway cannot read
|
|
942
1060
|
*/
|
|
943
1061
|
async watchPayment(params) {
|
|
1062
|
+
const sent = watchRequestBody(
|
|
1063
|
+
params,
|
|
1064
|
+
params.trigger ? sha256Hex(unguessable(params.trigger)) : null
|
|
1065
|
+
);
|
|
944
1066
|
const response = await fetch(`${this.baseUrl}/watched-payments`, {
|
|
945
1067
|
method: "POST",
|
|
946
|
-
headers: this.sending(),
|
|
947
|
-
body:
|
|
948
|
-
params,
|
|
949
|
-
params.trigger ? sha256Hex(unguessable(params.trigger)) : null
|
|
950
|
-
)
|
|
1068
|
+
headers: await this.sending("/watched-payments", sent),
|
|
1069
|
+
body: sent
|
|
951
1070
|
});
|
|
952
1071
|
if (!response.ok) throw await problemFrom(response);
|
|
953
1072
|
const watched = triggerEventFromWire(await response.json().catch(() => null));
|
|
@@ -957,8 +1076,22 @@ var ThunderBridge = class {
|
|
|
957
1076
|
title: "The gateway answered with something that is not a watched payment"
|
|
958
1077
|
});
|
|
959
1078
|
}
|
|
1079
|
+
const named = await this.nameFor(params.paymentHash);
|
|
1080
|
+
if (named !== null && watched.id !== named) {
|
|
1081
|
+
throw new GatewayCheatError("id_not_mine", watched.id);
|
|
1082
|
+
}
|
|
960
1083
|
return watched;
|
|
961
1084
|
}
|
|
1085
|
+
/**
|
|
1086
|
+
* What this payment is called, which you can work out before any gateway has
|
|
1087
|
+
* heard of it. Every gateway you hand the same invoice to answers with the same
|
|
1088
|
+
* name, so watching at several of them adds up to one payment rather than
|
|
1089
|
+
* several, and no gateway's key is in the answer. Null when no secret was given,
|
|
1090
|
+
* because then the gateway names the payment and only it can
|
|
1091
|
+
*/
|
|
1092
|
+
async nameFor(paymentHash) {
|
|
1093
|
+
return this.secret === null ? null : paymentNamedBy((await this.speaking()).publicKeyHex, paymentHash);
|
|
1094
|
+
}
|
|
962
1095
|
/**
|
|
963
1096
|
* Follow every payment made to one trigger, replayed from the recent ones on
|
|
964
1097
|
* connect and then live, reconnecting on its own until the returned function
|
|
@@ -1015,10 +1148,11 @@ var ThunderBridge = class {
|
|
|
1015
1148
|
return asked === true || this.token !== null;
|
|
1016
1149
|
}
|
|
1017
1150
|
async wsTicket(body) {
|
|
1151
|
+
const sent = JSON.stringify(body);
|
|
1018
1152
|
const response = await fetch(`${this.baseUrl}/ws-tickets`, {
|
|
1019
1153
|
method: "POST",
|
|
1020
|
-
headers: this.sending(),
|
|
1021
|
-
body:
|
|
1154
|
+
headers: await this.sending("/ws-tickets", sent),
|
|
1155
|
+
body: sent
|
|
1022
1156
|
});
|
|
1023
1157
|
if (!response.ok) throw await problemFrom(response);
|
|
1024
1158
|
const minted = await response.json().catch(() => null);
|
|
@@ -1030,11 +1164,18 @@ var ThunderBridge = class {
|
|
|
1030
1164
|
}
|
|
1031
1165
|
return encodeURIComponent(minted.ticket);
|
|
1032
1166
|
}
|
|
1033
|
-
sending() {
|
|
1034
|
-
return { "content-type": "application/json", ...this.reading() };
|
|
1167
|
+
async sending(path, body) {
|
|
1168
|
+
return { "content-type": "application/json", ...await this.reading("POST", path, body) };
|
|
1169
|
+
}
|
|
1170
|
+
async reading(method, path, body = "") {
|
|
1171
|
+
return {
|
|
1172
|
+
...this.token === null ? {} : { authorization: `Bearer ${this.token}` },
|
|
1173
|
+
...this.secret === null ? {} : await signedAs(await this.speaking(), method, path, body)
|
|
1174
|
+
};
|
|
1035
1175
|
}
|
|
1036
|
-
|
|
1037
|
-
|
|
1176
|
+
async speaking() {
|
|
1177
|
+
this.speaks ??= callerKey(this.secret ?? "");
|
|
1178
|
+
return await this.speaks;
|
|
1038
1179
|
}
|
|
1039
1180
|
proven(settled) {
|
|
1040
1181
|
const unproven = settled.status === "paid" && (settled.preimage === null || !preimageMatchesHash(settled.preimage, settled.paymentHash));
|
|
@@ -1079,6 +1220,80 @@ function refusals(wallets) {
|
|
|
1079
1220
|
});
|
|
1080
1221
|
}
|
|
1081
1222
|
|
|
1223
|
+
// src/gateways.ts
|
|
1224
|
+
var Gateways = class {
|
|
1225
|
+
each;
|
|
1226
|
+
urls;
|
|
1227
|
+
onRefused;
|
|
1228
|
+
constructor(baseUrls, options) {
|
|
1229
|
+
if (baseUrls.length === 0) throw new Error("name at least one gateway to watch at");
|
|
1230
|
+
this.urls = [...baseUrls];
|
|
1231
|
+
this.each = baseUrls.map((baseUrl) => new ThunderBridge(baseUrl, options));
|
|
1232
|
+
this.onRefused = options?.onRefused ?? (() => {
|
|
1233
|
+
});
|
|
1234
|
+
}
|
|
1235
|
+
/** What this payment is called, which every gateway here will agree on */
|
|
1236
|
+
async nameFor(paymentHash) {
|
|
1237
|
+
return await this.each[0].nameFor(paymentHash);
|
|
1238
|
+
}
|
|
1239
|
+
/**
|
|
1240
|
+
* Hand the same invoice to every gateway. Throws only when none of them took it,
|
|
1241
|
+
* carrying the first refusal, because one gateway that agreed is enough to be
|
|
1242
|
+
* watched
|
|
1243
|
+
*/
|
|
1244
|
+
async watchPayment(params) {
|
|
1245
|
+
const asked = await Promise.allSettled(this.each.map((gateway) => gateway.watchPayment(params)));
|
|
1246
|
+
const taken = [];
|
|
1247
|
+
const refusals3 = [];
|
|
1248
|
+
for (const [at, answer] of asked.entries()) {
|
|
1249
|
+
if (answer.status === "fulfilled") {
|
|
1250
|
+
taken.push(answer.value);
|
|
1251
|
+
continue;
|
|
1252
|
+
}
|
|
1253
|
+
refusals3.push(answer.reason);
|
|
1254
|
+
this.onRefused(this.urls[at], answer.reason);
|
|
1255
|
+
}
|
|
1256
|
+
if (taken[0] === void 0) throw refusals3[0];
|
|
1257
|
+
return taken[0];
|
|
1258
|
+
}
|
|
1259
|
+
/**
|
|
1260
|
+
* Wait for whichever gateway speaks first. A settlement from any of them is the
|
|
1261
|
+
* settlement, and each has already proved the preimage against the hash before
|
|
1262
|
+
* saying so. When they all end without a payment, the first ending is the answer,
|
|
1263
|
+
* and when they all fail, the first failure is thrown
|
|
1264
|
+
*/
|
|
1265
|
+
async waitForWatched(id, options) {
|
|
1266
|
+
const stopLosers = new AbortController();
|
|
1267
|
+
const signal = options?.signal ? AbortSignal.any([stopLosers.signal, options.signal]) : stopLosers.signal;
|
|
1268
|
+
const unpaid = [];
|
|
1269
|
+
let refused = null;
|
|
1270
|
+
try {
|
|
1271
|
+
const settled = await new Promise((resolve2) => {
|
|
1272
|
+
let waiting = this.each.length;
|
|
1273
|
+
const lost = () => {
|
|
1274
|
+
waiting -= 1;
|
|
1275
|
+
if (waiting === 0) resolve2(null);
|
|
1276
|
+
};
|
|
1277
|
+
for (const gateway of this.each) {
|
|
1278
|
+
gateway.waitForWatched(id, { ...options, signal }).then((watched) => {
|
|
1279
|
+
if (watched.status === "paid") return resolve2(watched);
|
|
1280
|
+
unpaid.push(watched);
|
|
1281
|
+
lost();
|
|
1282
|
+
}).catch((failure) => {
|
|
1283
|
+
refused ??= failure;
|
|
1284
|
+
lost();
|
|
1285
|
+
});
|
|
1286
|
+
}
|
|
1287
|
+
});
|
|
1288
|
+
if (settled !== null) return settled;
|
|
1289
|
+
if (unpaid.length > 0) return unpaid[0];
|
|
1290
|
+
throw refused;
|
|
1291
|
+
} finally {
|
|
1292
|
+
stopLosers.abort();
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
};
|
|
1296
|
+
|
|
1082
1297
|
// ../core/sealed.ts
|
|
1083
1298
|
var VERSION = "v1";
|
|
1084
1299
|
var IV_BYTES = 12;
|
|
@@ -1101,7 +1316,7 @@ async function seal(secret, plaintext) {
|
|
|
1101
1316
|
async function unseal(secret, sealed) {
|
|
1102
1317
|
const key = await keyFor(secret);
|
|
1103
1318
|
if (!sealed.startsWith(`${VERSION}.`)) return null;
|
|
1104
|
-
const joined =
|
|
1319
|
+
const joined = fromBase64Url2(sealed.slice(VERSION.length + 1));
|
|
1105
1320
|
if (joined === null || joined.length <= IV_BYTES) return null;
|
|
1106
1321
|
try {
|
|
1107
1322
|
const body = await crypto.subtle.decrypt(
|
|
@@ -1138,7 +1353,7 @@ function toBase64Url(bytes) {
|
|
|
1138
1353
|
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
1139
1354
|
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
1140
1355
|
}
|
|
1141
|
-
function
|
|
1356
|
+
function fromBase64Url2(text2) {
|
|
1142
1357
|
if (text2.length === 0 || !/^[A-Za-z0-9_-]+$/.test(text2)) return null;
|
|
1143
1358
|
try {
|
|
1144
1359
|
const binary = atob(text2.replace(/-/g, "+").replace(/_/g, "/"));
|
|
@@ -1150,29 +1365,6 @@ function fromBase64Url(text2) {
|
|
|
1150
1365
|
}
|
|
1151
1366
|
}
|
|
1152
1367
|
|
|
1153
|
-
// ../core/hmac.ts
|
|
1154
|
-
async function hmacHex(secret, payload) {
|
|
1155
|
-
const raw = typeof secret === "string" ? new TextEncoder().encode(secret) : secret;
|
|
1156
|
-
const key = await crypto.subtle.importKey(
|
|
1157
|
-
"raw",
|
|
1158
|
-
asBuffer(raw),
|
|
1159
|
-
{ name: "HMAC", hash: "SHA-256" },
|
|
1160
|
-
false,
|
|
1161
|
-
["sign"]
|
|
1162
|
-
);
|
|
1163
|
-
const message = typeof payload === "string" ? new TextEncoder().encode(payload) : payload;
|
|
1164
|
-
return bytesToHex(new Uint8Array(await crypto.subtle.sign("HMAC", key, asBuffer(message))));
|
|
1165
|
-
}
|
|
1166
|
-
function equalInConstantTime(one, other) {
|
|
1167
|
-
if (one.length !== other.length) return false;
|
|
1168
|
-
let difference = 0;
|
|
1169
|
-
for (let at = 0; at < one.length; at++) difference |= one.charCodeAt(at) ^ other.charCodeAt(at);
|
|
1170
|
-
return difference === 0;
|
|
1171
|
-
}
|
|
1172
|
-
function asBuffer(bytes) {
|
|
1173
|
-
return bytes instanceof Uint8Array && bytes.buffer instanceof ArrayBuffer ? bytes : new Uint8Array(bytes);
|
|
1174
|
-
}
|
|
1175
|
-
|
|
1176
1368
|
// src/currency.ts
|
|
1177
1369
|
var ISO_4217_MINOR_UNITS = {
|
|
1178
1370
|
BHD: 3,
|
|
@@ -1215,57 +1407,21 @@ function minorScaleOf(currency) {
|
|
|
1215
1407
|
return 10 ** minorUnitsOf(currency);
|
|
1216
1408
|
}
|
|
1217
1409
|
|
|
1218
|
-
// ../core/ed25519.ts
|
|
1219
|
-
var ALGORITHM = { name: "Ed25519" };
|
|
1220
|
-
var SIGNATURE_BYTES = 64;
|
|
1221
|
-
var PUBLIC_KEY_BYTES = 32;
|
|
1222
|
-
var PKCS8_HEADER = hexToBytes("302e020100300506032b657004220420");
|
|
1223
|
-
async function verifyHex(publicKeyHex, signatureHex, payload) {
|
|
1224
|
-
if (!isHex(publicKeyHex) || publicKeyHex.length !== PUBLIC_KEY_BYTES * 2) return false;
|
|
1225
|
-
if (!isHex(signatureHex) || signatureHex.length !== SIGNATURE_BYTES * 2) return false;
|
|
1226
|
-
try {
|
|
1227
|
-
const key = await crypto.subtle.importKey(
|
|
1228
|
-
"raw",
|
|
1229
|
-
asBuffer2(hexToBytes(publicKeyHex)),
|
|
1230
|
-
ALGORITHM,
|
|
1231
|
-
false,
|
|
1232
|
-
["verify"]
|
|
1233
|
-
);
|
|
1234
|
-
return await crypto.subtle.verify(
|
|
1235
|
-
ALGORITHM,
|
|
1236
|
-
key,
|
|
1237
|
-
asBuffer2(hexToBytes(signatureHex)),
|
|
1238
|
-
asBuffer2(payload)
|
|
1239
|
-
);
|
|
1240
|
-
} catch {
|
|
1241
|
-
return false;
|
|
1242
|
-
}
|
|
1243
|
-
}
|
|
1244
|
-
function asBuffer2(bytes) {
|
|
1245
|
-
return bytes.buffer instanceof ArrayBuffer ? bytes : new Uint8Array(bytes);
|
|
1246
|
-
}
|
|
1247
|
-
|
|
1248
1410
|
// src/webhook.ts
|
|
1249
|
-
var
|
|
1250
|
-
var
|
|
1251
|
-
var SHARED_SECRET_PREFIX = "sha256=";
|
|
1411
|
+
var SIGNATURE_HEADER2 = "x-signature";
|
|
1412
|
+
var TIMESTAMP_HEADER2 = "x-timestamp";
|
|
1252
1413
|
var GATEWAY_KEY_PREFIX = "ed25519=";
|
|
1253
1414
|
var DEFAULT_TOLERANCE_SECS = 300;
|
|
1254
1415
|
var CHALLENGE = "webhook-challenge";
|
|
1255
1416
|
var VERIFY_CHALLENGE = "verify-challenge";
|
|
1256
1417
|
async function verifyWebhookSignature(body, signature, credential, timestamp, options = {}) {
|
|
1257
1418
|
if (!recent(timestamp, options.toleranceSecs ?? DEFAULT_TOLERANCE_SECS)) return false;
|
|
1258
|
-
if (
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
);
|
|
1265
|
-
}
|
|
1266
|
-
if (signature.startsWith(GATEWAY_KEY_PREFIX)) return false;
|
|
1267
|
-
const received = signature.startsWith(SHARED_SECRET_PREFIX) ? signature.slice(SHARED_SECRET_PREFIX.length) : signature;
|
|
1268
|
-
return equalInConstantTime(await sign(credential, timestamp, body), received.toLowerCase());
|
|
1419
|
+
if (!signature.startsWith(GATEWAY_KEY_PREFIX)) return false;
|
|
1420
|
+
return await verifyHex(
|
|
1421
|
+
credential.publicKey.toLowerCase(),
|
|
1422
|
+
signature.slice(GATEWAY_KEY_PREFIX.length).toLowerCase(),
|
|
1423
|
+
signed(timestamp, body)
|
|
1424
|
+
);
|
|
1269
1425
|
}
|
|
1270
1426
|
async function parseWebhook(body, signature, credential, timestamp, options = {}) {
|
|
1271
1427
|
if (!await verifyWebhookSignature(body, signature, credential, timestamp, options)) return null;
|
|
@@ -1277,8 +1433,8 @@ async function parseWebhook(body, signature, credential, timestamp, options = {}
|
|
|
1277
1433
|
}
|
|
1278
1434
|
}
|
|
1279
1435
|
async function parseWebhookRequest(request, credential, options = {}) {
|
|
1280
|
-
const signature = request.headers.get(
|
|
1281
|
-
const timestamp = request.headers.get(
|
|
1436
|
+
const signature = request.headers.get(SIGNATURE_HEADER2);
|
|
1437
|
+
const timestamp = request.headers.get(TIMESTAMP_HEADER2);
|
|
1282
1438
|
if (signature === null || timestamp === null) return null;
|
|
1283
1439
|
return parseWebhook(await request.text(), signature, credential, timestamp, options);
|
|
1284
1440
|
}
|
|
@@ -1292,25 +1448,37 @@ async function parseWatchedWebhook(body, signature, credential, timestamp, optio
|
|
|
1292
1448
|
}
|
|
1293
1449
|
}
|
|
1294
1450
|
async function parseWatchedWebhookRequest(request, credential, options = {}) {
|
|
1295
|
-
const signature = request.headers.get(
|
|
1296
|
-
const timestamp = request.headers.get(
|
|
1451
|
+
const signature = request.headers.get(SIGNATURE_HEADER2);
|
|
1452
|
+
const timestamp = request.headers.get(TIMESTAMP_HEADER2);
|
|
1297
1453
|
if (signature === null || timestamp === null) return null;
|
|
1298
1454
|
return parseWatchedWebhook(await request.text(), signature, credential, timestamp, options);
|
|
1299
1455
|
}
|
|
1456
|
+
async function parseSettlement(body, signature, credential, timestamp, options = {}) {
|
|
1457
|
+
if (!await verifyWebhookSignature(body, signature, credential, timestamp, options)) return null;
|
|
1458
|
+
try {
|
|
1459
|
+
return settlementFromWire(JSON.parse(textOf(body)));
|
|
1460
|
+
} catch {
|
|
1461
|
+
return null;
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
async function parseSettlementRequest(request, credential, options = {}) {
|
|
1465
|
+
const signature = request.headers.get(SIGNATURE_HEADER2);
|
|
1466
|
+
const timestamp = request.headers.get(TIMESTAMP_HEADER2);
|
|
1467
|
+
if (signature === null || timestamp === null) return null;
|
|
1468
|
+
return await parseSettlement(await request.text(), signature, credential, timestamp, options);
|
|
1469
|
+
}
|
|
1470
|
+
function isProvablySettled(settled) {
|
|
1471
|
+
if (settled.status !== "paid" || settled.preimage === null) return false;
|
|
1472
|
+
return preimageMatchesHash(settled.preimage, settled.paymentHash);
|
|
1473
|
+
}
|
|
1300
1474
|
async function answerWebhookChallenge(body, signature, credential, timestamp, options = {}) {
|
|
1301
1475
|
if (!await verifyWebhookSignature(body, signature, credential, timestamp, options)) return null;
|
|
1302
|
-
const
|
|
1303
|
-
|
|
1304
|
-
if (nonce === null) return null;
|
|
1305
|
-
if (typeof credential !== "string") return JSON.stringify({ nonce });
|
|
1306
|
-
return JSON.stringify({
|
|
1307
|
-
nonce,
|
|
1308
|
-
signature: `${SHARED_SECRET_PREFIX}${await hmacHex(credential, nonce)}`
|
|
1309
|
-
});
|
|
1476
|
+
const nonce = challenged(typeof body === "string" ? body : new TextDecoder().decode(body));
|
|
1477
|
+
return nonce === null ? null : JSON.stringify({ nonce });
|
|
1310
1478
|
}
|
|
1311
1479
|
async function answerWebhookChallengeRequest(request, credential, options = {}) {
|
|
1312
|
-
const signature = request.headers.get(
|
|
1313
|
-
const timestamp = request.headers.get(
|
|
1480
|
+
const signature = request.headers.get(SIGNATURE_HEADER2);
|
|
1481
|
+
const timestamp = request.headers.get(TIMESTAMP_HEADER2);
|
|
1314
1482
|
if (signature === null || timestamp === null) return null;
|
|
1315
1483
|
const answer = await answerWebhookChallenge(
|
|
1316
1484
|
await request.clone().text(),
|
|
@@ -1321,6 +1489,9 @@ async function answerWebhookChallengeRequest(request, credential, options = {})
|
|
|
1321
1489
|
);
|
|
1322
1490
|
return answer === null ? null : new Response(answer, { headers: { "content-type": "application/json" } });
|
|
1323
1491
|
}
|
|
1492
|
+
function textOf(body) {
|
|
1493
|
+
return typeof body === "string" ? body : new TextDecoder().decode(body);
|
|
1494
|
+
}
|
|
1324
1495
|
function challenged(text2) {
|
|
1325
1496
|
return nonceOf(text2, CHALLENGE);
|
|
1326
1497
|
}
|
|
@@ -1347,9 +1518,6 @@ function recent(timestamp, toleranceSecs) {
|
|
|
1347
1518
|
if (!Number.isFinite(sent)) return false;
|
|
1348
1519
|
return Math.abs(Math.floor(Date.now() / 1e3) - sent) <= toleranceSecs;
|
|
1349
1520
|
}
|
|
1350
|
-
async function sign(secret, timestamp, body) {
|
|
1351
|
-
return hmacHex(secret, signed(timestamp, body));
|
|
1352
|
-
}
|
|
1353
1521
|
function signed(timestamp, body) {
|
|
1354
1522
|
const encoder = new TextEncoder();
|
|
1355
1523
|
const stamp = encoder.encode(`${timestamp}.`);
|
|
@@ -1384,8 +1552,7 @@ async function bankTransfer(params) {
|
|
|
1384
1552
|
expiresAt: params.expiresAt,
|
|
1385
1553
|
trigger: params.trigger,
|
|
1386
1554
|
sealed: params.sealed,
|
|
1387
|
-
webhookUrl: params.webhookUrl
|
|
1388
|
-
webhookSecret: params.webhookSecret
|
|
1555
|
+
webhookUrl: params.webhookUrl
|
|
1389
1556
|
});
|
|
1390
1557
|
return { id: watched.id, paymentHash: watched.paymentHash, verifyUrl, spd };
|
|
1391
1558
|
}
|
|
@@ -1712,7 +1879,6 @@ function bankRail(config) {
|
|
|
1712
1879
|
sealed: await config.sealed?.(order),
|
|
1713
1880
|
variableSymbol: config.variableSymbol?.(order),
|
|
1714
1881
|
webhookUrl: config.webhookUrl,
|
|
1715
|
-
webhookSecret: config.webhookSecret,
|
|
1716
1882
|
allowPublicGateway: config.allowPublicGateway
|
|
1717
1883
|
});
|
|
1718
1884
|
return {
|
|
@@ -1730,8 +1896,7 @@ function lightningRail(config) {
|
|
|
1730
1896
|
{
|
|
1731
1897
|
lnAddresses: config.lnAddresses,
|
|
1732
1898
|
amountMsat: await config.amountMsat(order),
|
|
1733
|
-
webhookUrl: config.webhookUrl
|
|
1734
|
-
webhookSecret: config.webhookSecret
|
|
1899
|
+
webhookUrl: config.webhookUrl
|
|
1735
1900
|
},
|
|
1736
1901
|
{ idempotencyKey: config.idempotencyKey?.(order), trigger: config.trigger }
|
|
1737
1902
|
);
|
|
@@ -1868,6 +2033,7 @@ function refusals2(answers) {
|
|
|
1868
2033
|
}
|
|
1869
2034
|
export {
|
|
1870
2035
|
GatewayCheatError,
|
|
2036
|
+
Gateways,
|
|
1871
2037
|
IDEMPOTENCY_KEY_REUSED,
|
|
1872
2038
|
IdempotencyConflictError,
|
|
1873
2039
|
NO_WALLET_AVAILABLE,
|
|
@@ -1893,6 +2059,7 @@ export {
|
|
|
1893
2059
|
invoiceToSvg,
|
|
1894
2060
|
isProblemType,
|
|
1895
2061
|
isProvablyPaid,
|
|
2062
|
+
isProvablySettled,
|
|
1896
2063
|
kraken,
|
|
1897
2064
|
lightningRail,
|
|
1898
2065
|
lnurlToDataUrl,
|
|
@@ -1901,6 +2068,8 @@ export {
|
|
|
1901
2068
|
minorScaleOf,
|
|
1902
2069
|
minorUnitsOf,
|
|
1903
2070
|
msatFor,
|
|
2071
|
+
parseSettlement,
|
|
2072
|
+
parseSettlementRequest,
|
|
1904
2073
|
parseWatchedWebhook,
|
|
1905
2074
|
parseWatchedWebhookRequest,
|
|
1906
2075
|
parseWebhook,
|