thunder-bridge 0.8.9 → 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/dist/index.cjs CHANGED
@@ -21,6 +21,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
23
  GatewayCheatError: () => GatewayCheatError,
24
+ Gateways: () => Gateways,
24
25
  IDEMPOTENCY_KEY_REUSED: () => IDEMPOTENCY_KEY_REUSED,
25
26
  IdempotencyConflictError: () => IdempotencyConflictError,
26
27
  NO_WALLET_AVAILABLE: () => NO_WALLET_AVAILABLE,
@@ -46,6 +47,7 @@ __export(index_exports, {
46
47
  invoiceToSvg: () => invoiceToSvg,
47
48
  isProblemType: () => isProblemType,
48
49
  isProvablyPaid: () => isProvablyPaid,
50
+ isProvablySettled: () => isProvablySettled,
49
51
  kraken: () => kraken,
50
52
  lightningRail: () => lightningRail,
51
53
  lnurlToDataUrl: () => lnurlToDataUrl,
@@ -54,6 +56,8 @@ __export(index_exports, {
54
56
  minorScaleOf: () => minorScaleOf,
55
57
  minorUnitsOf: () => minorUnitsOf,
56
58
  msatFor: () => msatFor,
59
+ parseSettlement: () => parseSettlement,
60
+ parseSettlementRequest: () => parseSettlementRequest,
57
61
  parseWatchedWebhook: () => parseWatchedWebhook,
58
62
  parseWatchedWebhookRequest: () => parseWatchedWebhookRequest,
59
63
  parseWebhook: () => parseWebhook,
@@ -402,6 +406,105 @@ function wordsToBytes(words) {
402
406
  return Uint8Array.from(bytes);
403
407
  }
404
408
 
409
+ // ../core/ed25519.ts
410
+ var ALGORITHM = { name: "Ed25519" };
411
+ var SEED_BYTES = 32;
412
+ var SIGNATURE_BYTES = 64;
413
+ var PUBLIC_KEY_BYTES = 32;
414
+ var PKCS8_HEADER = hexToBytes("302e020100300506032b657004220420");
415
+ async function signingKeyFromSeed(seed) {
416
+ if (seed.length !== SEED_BYTES) throw new Error(`an ed25519 seed is ${SEED_BYTES} bytes`);
417
+ const wrapped = new Uint8Array(PKCS8_HEADER.length + SEED_BYTES);
418
+ wrapped.set(PKCS8_HEADER);
419
+ wrapped.set(seed, PKCS8_HEADER.length);
420
+ const key = await crypto.subtle.importKey("pkcs8", asBuffer(wrapped), ALGORITHM, true, ["sign"]);
421
+ const jwk = await crypto.subtle.exportKey("jwk", key);
422
+ return {
423
+ publicKeyHex: bytesToHex(fromBase64Url(jwk.x ?? "")),
424
+ sign: async (payload) => bytesToHex(new Uint8Array(await crypto.subtle.sign(ALGORITHM, key, asBuffer(payload))))
425
+ };
426
+ }
427
+ async function verifyHex(publicKeyHex, signatureHex, payload) {
428
+ if (!isHex(publicKeyHex) || publicKeyHex.length !== PUBLIC_KEY_BYTES * 2) return false;
429
+ if (!isHex(signatureHex) || signatureHex.length !== SIGNATURE_BYTES * 2) return false;
430
+ try {
431
+ const key = await crypto.subtle.importKey(
432
+ "raw",
433
+ asBuffer(hexToBytes(publicKeyHex)),
434
+ ALGORITHM,
435
+ false,
436
+ ["verify"]
437
+ );
438
+ return await crypto.subtle.verify(
439
+ ALGORITHM,
440
+ key,
441
+ asBuffer(hexToBytes(signatureHex)),
442
+ asBuffer(payload)
443
+ );
444
+ } catch {
445
+ return false;
446
+ }
447
+ }
448
+ function fromBase64Url(text2) {
449
+ const padded = text2.replace(/-/g, "+").replace(/_/g, "/");
450
+ const raw = atob(padded + "=".repeat((4 - padded.length % 4) % 4));
451
+ const bytes = new Uint8Array(raw.length);
452
+ for (let at = 0; at < raw.length; at++) bytes[at] = raw.charCodeAt(at);
453
+ return bytes;
454
+ }
455
+ function asBuffer(bytes) {
456
+ return bytes.buffer instanceof ArrayBuffer ? bytes : new Uint8Array(bytes);
457
+ }
458
+
459
+ // ../core/hmac.ts
460
+ async function hmacHex(secret, payload) {
461
+ const raw = typeof secret === "string" ? new TextEncoder().encode(secret) : secret;
462
+ const key = await crypto.subtle.importKey(
463
+ "raw",
464
+ asBuffer2(raw),
465
+ { name: "HMAC", hash: "SHA-256" },
466
+ false,
467
+ ["sign"]
468
+ );
469
+ const message = typeof payload === "string" ? new TextEncoder().encode(payload) : payload;
470
+ return bytesToHex(new Uint8Array(await crypto.subtle.sign("HMAC", key, asBuffer2(message))));
471
+ }
472
+ function equalInConstantTime(one, other) {
473
+ if (one.length !== other.length) return false;
474
+ let difference = 0;
475
+ for (let at = 0; at < one.length; at++) difference |= one.charCodeAt(at) ^ other.charCodeAt(at);
476
+ return difference === 0;
477
+ }
478
+ function asBuffer2(bytes) {
479
+ return bytes instanceof Uint8Array && bytes.buffer instanceof ArrayBuffer ? bytes : new Uint8Array(bytes);
480
+ }
481
+
482
+ // ../core/caller.ts
483
+ var KEY_HEADER = "x-client-key";
484
+ var SIGNATURE_HEADER = "x-signature";
485
+ var TIMESTAMP_HEADER = "x-timestamp";
486
+ var PREFIX = "ed25519=";
487
+ var SIGNING_LABEL = "request-signing";
488
+ async function callerKey(secret) {
489
+ return await signingKeyFromSeed(hexToBytes(await hmacHex(secret, SIGNING_LABEL)));
490
+ }
491
+ function paymentNamedBy(caller, paymentHash) {
492
+ return sha256Hex(`${caller}.payment-id.${paymentHash}`);
493
+ }
494
+ async function signedAs(key, method, path, body) {
495
+ const timestamp = String(Math.floor(Date.now() / 1e3));
496
+ return {
497
+ [KEY_HEADER]: key.publicKeyHex,
498
+ [TIMESTAMP_HEADER]: timestamp,
499
+ [SIGNATURE_HEADER]: `${PREFIX}${await key.sign(spoken(timestamp, method, path, body))}`
500
+ };
501
+ }
502
+ function spoken(timestamp, method, path, body) {
503
+ return new TextEncoder().encode(
504
+ `${timestamp}.${method.toUpperCase()}.${path}.${sha256Hex(body)}`
505
+ );
506
+ }
507
+
405
508
  // ../core/url.ts
406
509
  var PRIVATE_SUFFIXES = ["localhost", "local", "internal", "lan", "arpa", "test", "invalid"];
407
510
  function publicHttps(raw) {
@@ -550,7 +653,7 @@ function createRequestBody(params, trigger) {
550
653
  return JSON.stringify({
551
654
  ln_addresses: params.lnAddresses,
552
655
  incoming_amount: toAmount(params.amountMsat),
553
- webhook: params.webhookUrl ? { url: params.webhookUrl, secret: params.webhookSecret } : void 0,
656
+ webhook: params.webhookUrl ? { url: params.webhookUrl } : void 0,
554
657
  trigger: trigger ?? void 0
555
658
  });
556
659
  }
@@ -616,9 +719,21 @@ function watchRequestBody(params, trigger) {
616
719
  expires_at: expiresAt.toISOString(),
617
720
  trigger: trigger ?? void 0,
618
721
  sealed: params.sealed,
619
- webhook: params.webhookUrl ? { url: params.webhookUrl, secret: params.webhookSecret } : void 0
722
+ webhook: params.webhookUrl ? { url: params.webhookUrl } : void 0
620
723
  });
621
724
  }
725
+ function settlementFromWire(body) {
726
+ const wire = asObject(body);
727
+ if (wire === null) return null;
728
+ const id = text(wire["id"]);
729
+ const paymentHash = text(wire["payment_hash"]);
730
+ const settledAt = secondsFrom(wire["settled_at"]);
731
+ const status = wire["status"];
732
+ const preimage = wire["preimage"] ?? null;
733
+ if (id === null || paymentHash === null || settledAt === null || !isStatus(status)) return null;
734
+ if (preimage !== null && typeof preimage !== "string") return null;
735
+ return { id, status, paymentHash, preimage, settledAt };
736
+ }
622
737
  function triggerEventFromWire(body) {
623
738
  const wire = asObject(body);
624
739
  if (wire === null) return null;
@@ -716,11 +831,14 @@ var ThunderBridge = class {
716
831
  baseUrl;
717
832
  verify;
718
833
  token;
834
+ secret;
719
835
  strangers = null;
836
+ speaks = null;
720
837
  constructor(baseUrl, options) {
721
838
  this.baseUrl = baseUrl.replace(/\/+$/, "");
722
839
  this.verify = options?.verify ?? true;
723
840
  this.token = options?.token ?? null;
841
+ this.secret = options?.secret ?? null;
724
842
  }
725
843
  /**
726
844
  * Whether a token was given, which is what makes an instance yours: a gateway
@@ -751,15 +869,16 @@ var ThunderBridge = class {
751
869
  * and `GatewayCheatError` when what comes back is not what you asked for
752
870
  */
753
871
  async createPayment(params, options) {
754
- const headers = this.sending();
872
+ const sent = createRequestBody(
873
+ params,
874
+ options?.trigger ? sha256Hex(unguessable(options.trigger)) : null
875
+ );
876
+ const headers = await this.sending("/incoming-payments", sent);
755
877
  if (options?.idempotencyKey) headers["idempotency-key"] = options.idempotencyKey;
756
878
  const response = await fetch(`${this.baseUrl}/incoming-payments`, {
757
879
  method: "POST",
758
880
  headers,
759
- body: createRequestBody(
760
- params,
761
- options?.trigger ? sha256Hex(unguessable(options.trigger)) : null
762
- )
881
+ body: sent
763
882
  });
764
883
  if (!response.ok) throw await problemFrom(response);
765
884
  const payment = await paymentFrom(response);
@@ -774,10 +893,11 @@ var ThunderBridge = class {
774
893
  * it for one, and asking mints it
775
894
  */
776
895
  async createQuote(params) {
896
+ const sent = quoteRequestBody(params);
777
897
  const response = await fetch(`${this.baseUrl}/quotes`, {
778
898
  method: "POST",
779
- headers: this.sending(),
780
- body: quoteRequestBody(params)
899
+ headers: await this.sending("/quotes", sent),
900
+ body: sent
781
901
  });
782
902
  if (!response.ok) throw await problemFrom(response);
783
903
  const quote = quoteFromWire(await response.json().catch(() => null));
@@ -808,8 +928,9 @@ var ThunderBridge = class {
808
928
  return body.public_key;
809
929
  }
810
930
  async getPayment(id) {
811
- const response = await fetch(`${this.baseUrl}/incoming-payments/${encodeURIComponent(id)}`, {
812
- headers: this.reading()
931
+ const path = `/incoming-payments/${encodeURIComponent(id)}`;
932
+ const response = await fetch(`${this.baseUrl}${path}`, {
933
+ headers: await this.reading("GET", path)
813
934
  });
814
935
  if (response.status === 404) return null;
815
936
  if (!response.ok) throw await problemFrom(response);
@@ -821,8 +942,9 @@ var ThunderBridge = class {
821
942
  * `getPayment` refuses it and this reads the shape both rails share
822
943
  */
823
944
  async getWatched(id) {
824
- const response = await fetch(`${this.baseUrl}/incoming-payments/${encodeURIComponent(id)}`, {
825
- headers: this.reading()
945
+ const path = `/incoming-payments/${encodeURIComponent(id)}`;
946
+ const response = await fetch(`${this.baseUrl}${path}`, {
947
+ headers: await this.reading("GET", path)
826
948
  });
827
949
  if (response.status === 404) return null;
828
950
  if (!response.ok) throw await problemFrom(response);
@@ -845,9 +967,9 @@ var ThunderBridge = class {
845
967
  * pretend otherwise
846
968
  */
847
969
  async listPayments(limit) {
848
- const query = limit === void 0 ? "" : `?limit=${limit}`;
849
- const response = await fetch(`${this.baseUrl}/incoming-payments${query}`, {
850
- headers: this.reading()
970
+ const path = `/incoming-payments${limit === void 0 ? "" : `?limit=${limit}`}`;
971
+ const response = await fetch(`${this.baseUrl}${path}`, {
972
+ headers: await this.reading("GET", path)
851
973
  });
852
974
  if (!response.ok) throw await problemFrom(response);
853
975
  const body = await response.json().catch(() => null);
@@ -1015,13 +1137,14 @@ var ThunderBridge = class {
1015
1137
  * the watcher needs goes in `sealed`, which the gateway cannot read
1016
1138
  */
1017
1139
  async watchPayment(params) {
1140
+ const sent = watchRequestBody(
1141
+ params,
1142
+ params.trigger ? sha256Hex(unguessable(params.trigger)) : null
1143
+ );
1018
1144
  const response = await fetch(`${this.baseUrl}/watched-payments`, {
1019
1145
  method: "POST",
1020
- headers: this.sending(),
1021
- body: watchRequestBody(
1022
- params,
1023
- params.trigger ? sha256Hex(unguessable(params.trigger)) : null
1024
- )
1146
+ headers: await this.sending("/watched-payments", sent),
1147
+ body: sent
1025
1148
  });
1026
1149
  if (!response.ok) throw await problemFrom(response);
1027
1150
  const watched = triggerEventFromWire(await response.json().catch(() => null));
@@ -1031,8 +1154,22 @@ var ThunderBridge = class {
1031
1154
  title: "The gateway answered with something that is not a watched payment"
1032
1155
  });
1033
1156
  }
1157
+ const named = await this.nameFor(params.paymentHash);
1158
+ if (named !== null && watched.id !== named) {
1159
+ throw new GatewayCheatError("id_not_mine", watched.id);
1160
+ }
1034
1161
  return watched;
1035
1162
  }
1163
+ /**
1164
+ * What this payment is called, which you can work out before any gateway has
1165
+ * heard of it. Every gateway you hand the same invoice to answers with the same
1166
+ * name, so watching at several of them adds up to one payment rather than
1167
+ * several, and no gateway's key is in the answer. Null when no secret was given,
1168
+ * because then the gateway names the payment and only it can
1169
+ */
1170
+ async nameFor(paymentHash) {
1171
+ return this.secret === null ? null : paymentNamedBy((await this.speaking()).publicKeyHex, paymentHash);
1172
+ }
1036
1173
  /**
1037
1174
  * Follow every payment made to one trigger, replayed from the recent ones on
1038
1175
  * connect and then live, reconnecting on its own until the returned function
@@ -1089,10 +1226,11 @@ var ThunderBridge = class {
1089
1226
  return asked === true || this.token !== null;
1090
1227
  }
1091
1228
  async wsTicket(body) {
1229
+ const sent = JSON.stringify(body);
1092
1230
  const response = await fetch(`${this.baseUrl}/ws-tickets`, {
1093
1231
  method: "POST",
1094
- headers: this.sending(),
1095
- body: JSON.stringify(body)
1232
+ headers: await this.sending("/ws-tickets", sent),
1233
+ body: sent
1096
1234
  });
1097
1235
  if (!response.ok) throw await problemFrom(response);
1098
1236
  const minted = await response.json().catch(() => null);
@@ -1104,11 +1242,18 @@ var ThunderBridge = class {
1104
1242
  }
1105
1243
  return encodeURIComponent(minted.ticket);
1106
1244
  }
1107
- sending() {
1108
- return { "content-type": "application/json", ...this.reading() };
1245
+ async sending(path, body) {
1246
+ return { "content-type": "application/json", ...await this.reading("POST", path, body) };
1247
+ }
1248
+ async reading(method, path, body = "") {
1249
+ return {
1250
+ ...this.token === null ? {} : { authorization: `Bearer ${this.token}` },
1251
+ ...this.secret === null ? {} : await signedAs(await this.speaking(), method, path, body)
1252
+ };
1109
1253
  }
1110
- reading() {
1111
- return this.token === null ? {} : { authorization: `Bearer ${this.token}` };
1254
+ async speaking() {
1255
+ this.speaks ??= callerKey(this.secret ?? "");
1256
+ return await this.speaks;
1112
1257
  }
1113
1258
  proven(settled) {
1114
1259
  const unproven = settled.status === "paid" && (settled.preimage === null || !preimageMatchesHash(settled.preimage, settled.paymentHash));
@@ -1153,6 +1298,80 @@ function refusals(wallets) {
1153
1298
  });
1154
1299
  }
1155
1300
 
1301
+ // src/gateways.ts
1302
+ var Gateways = class {
1303
+ each;
1304
+ urls;
1305
+ onRefused;
1306
+ constructor(baseUrls, options) {
1307
+ if (baseUrls.length === 0) throw new Error("name at least one gateway to watch at");
1308
+ this.urls = [...baseUrls];
1309
+ this.each = baseUrls.map((baseUrl) => new ThunderBridge(baseUrl, options));
1310
+ this.onRefused = options?.onRefused ?? (() => {
1311
+ });
1312
+ }
1313
+ /** What this payment is called, which every gateway here will agree on */
1314
+ async nameFor(paymentHash) {
1315
+ return await this.each[0].nameFor(paymentHash);
1316
+ }
1317
+ /**
1318
+ * Hand the same invoice to every gateway. Throws only when none of them took it,
1319
+ * carrying the first refusal, because one gateway that agreed is enough to be
1320
+ * watched
1321
+ */
1322
+ async watchPayment(params) {
1323
+ const asked = await Promise.allSettled(this.each.map((gateway) => gateway.watchPayment(params)));
1324
+ const taken = [];
1325
+ const refusals3 = [];
1326
+ for (const [at, answer] of asked.entries()) {
1327
+ if (answer.status === "fulfilled") {
1328
+ taken.push(answer.value);
1329
+ continue;
1330
+ }
1331
+ refusals3.push(answer.reason);
1332
+ this.onRefused(this.urls[at], answer.reason);
1333
+ }
1334
+ if (taken[0] === void 0) throw refusals3[0];
1335
+ return taken[0];
1336
+ }
1337
+ /**
1338
+ * Wait for whichever gateway speaks first. A settlement from any of them is the
1339
+ * settlement, and each has already proved the preimage against the hash before
1340
+ * saying so. When they all end without a payment, the first ending is the answer,
1341
+ * and when they all fail, the first failure is thrown
1342
+ */
1343
+ async waitForWatched(id, options) {
1344
+ const stopLosers = new AbortController();
1345
+ const signal = options?.signal ? AbortSignal.any([stopLosers.signal, options.signal]) : stopLosers.signal;
1346
+ const unpaid = [];
1347
+ let refused = null;
1348
+ try {
1349
+ const settled = await new Promise((resolve2) => {
1350
+ let waiting = this.each.length;
1351
+ const lost = () => {
1352
+ waiting -= 1;
1353
+ if (waiting === 0) resolve2(null);
1354
+ };
1355
+ for (const gateway of this.each) {
1356
+ gateway.waitForWatched(id, { ...options, signal }).then((watched) => {
1357
+ if (watched.status === "paid") return resolve2(watched);
1358
+ unpaid.push(watched);
1359
+ lost();
1360
+ }).catch((failure) => {
1361
+ refused ??= failure;
1362
+ lost();
1363
+ });
1364
+ }
1365
+ });
1366
+ if (settled !== null) return settled;
1367
+ if (unpaid.length > 0) return unpaid[0];
1368
+ throw refused;
1369
+ } finally {
1370
+ stopLosers.abort();
1371
+ }
1372
+ }
1373
+ };
1374
+
1156
1375
  // ../core/sealed.ts
1157
1376
  var VERSION = "v1";
1158
1377
  var IV_BYTES = 12;
@@ -1175,7 +1394,7 @@ async function seal(secret, plaintext) {
1175
1394
  async function unseal(secret, sealed) {
1176
1395
  const key = await keyFor(secret);
1177
1396
  if (!sealed.startsWith(`${VERSION}.`)) return null;
1178
- const joined = fromBase64Url(sealed.slice(VERSION.length + 1));
1397
+ const joined = fromBase64Url2(sealed.slice(VERSION.length + 1));
1179
1398
  if (joined === null || joined.length <= IV_BYTES) return null;
1180
1399
  try {
1181
1400
  const body = await crypto.subtle.decrypt(
@@ -1212,7 +1431,7 @@ function toBase64Url(bytes) {
1212
1431
  for (const byte of bytes) binary += String.fromCharCode(byte);
1213
1432
  return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1214
1433
  }
1215
- function fromBase64Url(text2) {
1434
+ function fromBase64Url2(text2) {
1216
1435
  if (text2.length === 0 || !/^[A-Za-z0-9_-]+$/.test(text2)) return null;
1217
1436
  try {
1218
1437
  const binary = atob(text2.replace(/-/g, "+").replace(/_/g, "/"));
@@ -1224,29 +1443,6 @@ function fromBase64Url(text2) {
1224
1443
  }
1225
1444
  }
1226
1445
 
1227
- // ../core/hmac.ts
1228
- async function hmacHex(secret, payload) {
1229
- const raw = typeof secret === "string" ? new TextEncoder().encode(secret) : secret;
1230
- const key = await crypto.subtle.importKey(
1231
- "raw",
1232
- asBuffer(raw),
1233
- { name: "HMAC", hash: "SHA-256" },
1234
- false,
1235
- ["sign"]
1236
- );
1237
- const message = typeof payload === "string" ? new TextEncoder().encode(payload) : payload;
1238
- return bytesToHex(new Uint8Array(await crypto.subtle.sign("HMAC", key, asBuffer(message))));
1239
- }
1240
- function equalInConstantTime(one, other) {
1241
- if (one.length !== other.length) return false;
1242
- let difference = 0;
1243
- for (let at = 0; at < one.length; at++) difference |= one.charCodeAt(at) ^ other.charCodeAt(at);
1244
- return difference === 0;
1245
- }
1246
- function asBuffer(bytes) {
1247
- return bytes instanceof Uint8Array && bytes.buffer instanceof ArrayBuffer ? bytes : new Uint8Array(bytes);
1248
- }
1249
-
1250
1446
  // src/currency.ts
1251
1447
  var ISO_4217_MINOR_UNITS = {
1252
1448
  BHD: 3,
@@ -1289,57 +1485,21 @@ function minorScaleOf(currency) {
1289
1485
  return 10 ** minorUnitsOf(currency);
1290
1486
  }
1291
1487
 
1292
- // ../core/ed25519.ts
1293
- var ALGORITHM = { name: "Ed25519" };
1294
- var SIGNATURE_BYTES = 64;
1295
- var PUBLIC_KEY_BYTES = 32;
1296
- var PKCS8_HEADER = hexToBytes("302e020100300506032b657004220420");
1297
- async function verifyHex(publicKeyHex, signatureHex, payload) {
1298
- if (!isHex(publicKeyHex) || publicKeyHex.length !== PUBLIC_KEY_BYTES * 2) return false;
1299
- if (!isHex(signatureHex) || signatureHex.length !== SIGNATURE_BYTES * 2) return false;
1300
- try {
1301
- const key = await crypto.subtle.importKey(
1302
- "raw",
1303
- asBuffer2(hexToBytes(publicKeyHex)),
1304
- ALGORITHM,
1305
- false,
1306
- ["verify"]
1307
- );
1308
- return await crypto.subtle.verify(
1309
- ALGORITHM,
1310
- key,
1311
- asBuffer2(hexToBytes(signatureHex)),
1312
- asBuffer2(payload)
1313
- );
1314
- } catch {
1315
- return false;
1316
- }
1317
- }
1318
- function asBuffer2(bytes) {
1319
- return bytes.buffer instanceof ArrayBuffer ? bytes : new Uint8Array(bytes);
1320
- }
1321
-
1322
1488
  // src/webhook.ts
1323
- var SIGNATURE_HEADER = "x-signature";
1324
- var TIMESTAMP_HEADER = "x-timestamp";
1325
- var SHARED_SECRET_PREFIX = "sha256=";
1489
+ var SIGNATURE_HEADER2 = "x-signature";
1490
+ var TIMESTAMP_HEADER2 = "x-timestamp";
1326
1491
  var GATEWAY_KEY_PREFIX = "ed25519=";
1327
1492
  var DEFAULT_TOLERANCE_SECS = 300;
1328
1493
  var CHALLENGE = "webhook-challenge";
1329
1494
  var VERIFY_CHALLENGE = "verify-challenge";
1330
1495
  async function verifyWebhookSignature(body, signature, credential, timestamp, options = {}) {
1331
1496
  if (!recent(timestamp, options.toleranceSecs ?? DEFAULT_TOLERANCE_SECS)) return false;
1332
- if (typeof credential !== "string") {
1333
- if (!signature.startsWith(GATEWAY_KEY_PREFIX)) return false;
1334
- return verifyHex(
1335
- credential.publicKey.toLowerCase(),
1336
- signature.slice(GATEWAY_KEY_PREFIX.length).toLowerCase(),
1337
- signed(timestamp, body)
1338
- );
1339
- }
1340
- if (signature.startsWith(GATEWAY_KEY_PREFIX)) return false;
1341
- const received = signature.startsWith(SHARED_SECRET_PREFIX) ? signature.slice(SHARED_SECRET_PREFIX.length) : signature;
1342
- return equalInConstantTime(await sign(credential, timestamp, body), received.toLowerCase());
1497
+ if (!signature.startsWith(GATEWAY_KEY_PREFIX)) return false;
1498
+ return await verifyHex(
1499
+ credential.publicKey.toLowerCase(),
1500
+ signature.slice(GATEWAY_KEY_PREFIX.length).toLowerCase(),
1501
+ signed(timestamp, body)
1502
+ );
1343
1503
  }
1344
1504
  async function parseWebhook(body, signature, credential, timestamp, options = {}) {
1345
1505
  if (!await verifyWebhookSignature(body, signature, credential, timestamp, options)) return null;
@@ -1351,8 +1511,8 @@ async function parseWebhook(body, signature, credential, timestamp, options = {}
1351
1511
  }
1352
1512
  }
1353
1513
  async function parseWebhookRequest(request, credential, options = {}) {
1354
- const signature = request.headers.get(SIGNATURE_HEADER);
1355
- const timestamp = request.headers.get(TIMESTAMP_HEADER);
1514
+ const signature = request.headers.get(SIGNATURE_HEADER2);
1515
+ const timestamp = request.headers.get(TIMESTAMP_HEADER2);
1356
1516
  if (signature === null || timestamp === null) return null;
1357
1517
  return parseWebhook(await request.text(), signature, credential, timestamp, options);
1358
1518
  }
@@ -1366,25 +1526,37 @@ async function parseWatchedWebhook(body, signature, credential, timestamp, optio
1366
1526
  }
1367
1527
  }
1368
1528
  async function parseWatchedWebhookRequest(request, credential, options = {}) {
1369
- const signature = request.headers.get(SIGNATURE_HEADER);
1370
- const timestamp = request.headers.get(TIMESTAMP_HEADER);
1529
+ const signature = request.headers.get(SIGNATURE_HEADER2);
1530
+ const timestamp = request.headers.get(TIMESTAMP_HEADER2);
1371
1531
  if (signature === null || timestamp === null) return null;
1372
1532
  return parseWatchedWebhook(await request.text(), signature, credential, timestamp, options);
1373
1533
  }
1534
+ async function parseSettlement(body, signature, credential, timestamp, options = {}) {
1535
+ if (!await verifyWebhookSignature(body, signature, credential, timestamp, options)) return null;
1536
+ try {
1537
+ return settlementFromWire(JSON.parse(textOf(body)));
1538
+ } catch {
1539
+ return null;
1540
+ }
1541
+ }
1542
+ async function parseSettlementRequest(request, credential, options = {}) {
1543
+ const signature = request.headers.get(SIGNATURE_HEADER2);
1544
+ const timestamp = request.headers.get(TIMESTAMP_HEADER2);
1545
+ if (signature === null || timestamp === null) return null;
1546
+ return await parseSettlement(await request.text(), signature, credential, timestamp, options);
1547
+ }
1548
+ function isProvablySettled(settled) {
1549
+ if (settled.status !== "paid" || settled.preimage === null) return false;
1550
+ return preimageMatchesHash(settled.preimage, settled.paymentHash);
1551
+ }
1374
1552
  async function answerWebhookChallenge(body, signature, credential, timestamp, options = {}) {
1375
1553
  if (!await verifyWebhookSignature(body, signature, credential, timestamp, options)) return null;
1376
- const text2 = typeof body === "string" ? body : new TextDecoder().decode(body);
1377
- const nonce = challenged(text2);
1378
- if (nonce === null) return null;
1379
- if (typeof credential !== "string") return JSON.stringify({ nonce });
1380
- return JSON.stringify({
1381
- nonce,
1382
- signature: `${SHARED_SECRET_PREFIX}${await hmacHex(credential, nonce)}`
1383
- });
1554
+ const nonce = challenged(typeof body === "string" ? body : new TextDecoder().decode(body));
1555
+ return nonce === null ? null : JSON.stringify({ nonce });
1384
1556
  }
1385
1557
  async function answerWebhookChallengeRequest(request, credential, options = {}) {
1386
- const signature = request.headers.get(SIGNATURE_HEADER);
1387
- const timestamp = request.headers.get(TIMESTAMP_HEADER);
1558
+ const signature = request.headers.get(SIGNATURE_HEADER2);
1559
+ const timestamp = request.headers.get(TIMESTAMP_HEADER2);
1388
1560
  if (signature === null || timestamp === null) return null;
1389
1561
  const answer = await answerWebhookChallenge(
1390
1562
  await request.clone().text(),
@@ -1395,6 +1567,9 @@ async function answerWebhookChallengeRequest(request, credential, options = {})
1395
1567
  );
1396
1568
  return answer === null ? null : new Response(answer, { headers: { "content-type": "application/json" } });
1397
1569
  }
1570
+ function textOf(body) {
1571
+ return typeof body === "string" ? body : new TextDecoder().decode(body);
1572
+ }
1398
1573
  function challenged(text2) {
1399
1574
  return nonceOf(text2, CHALLENGE);
1400
1575
  }
@@ -1421,9 +1596,6 @@ function recent(timestamp, toleranceSecs) {
1421
1596
  if (!Number.isFinite(sent)) return false;
1422
1597
  return Math.abs(Math.floor(Date.now() / 1e3) - sent) <= toleranceSecs;
1423
1598
  }
1424
- async function sign(secret, timestamp, body) {
1425
- return hmacHex(secret, signed(timestamp, body));
1426
- }
1427
1599
  function signed(timestamp, body) {
1428
1600
  const encoder = new TextEncoder();
1429
1601
  const stamp = encoder.encode(`${timestamp}.`);
@@ -1458,8 +1630,7 @@ async function bankTransfer(params) {
1458
1630
  expiresAt: params.expiresAt,
1459
1631
  trigger: params.trigger,
1460
1632
  sealed: params.sealed,
1461
- webhookUrl: params.webhookUrl,
1462
- webhookSecret: params.webhookSecret
1633
+ webhookUrl: params.webhookUrl
1463
1634
  });
1464
1635
  return { id: watched.id, paymentHash: watched.paymentHash, verifyUrl, spd };
1465
1636
  }
@@ -1786,7 +1957,6 @@ function bankRail(config) {
1786
1957
  sealed: await config.sealed?.(order),
1787
1958
  variableSymbol: config.variableSymbol?.(order),
1788
1959
  webhookUrl: config.webhookUrl,
1789
- webhookSecret: config.webhookSecret,
1790
1960
  allowPublicGateway: config.allowPublicGateway
1791
1961
  });
1792
1962
  return {
@@ -1804,8 +1974,7 @@ function lightningRail(config) {
1804
1974
  {
1805
1975
  lnAddresses: config.lnAddresses,
1806
1976
  amountMsat: await config.amountMsat(order),
1807
- webhookUrl: config.webhookUrl,
1808
- webhookSecret: config.webhookSecret
1977
+ webhookUrl: config.webhookUrl
1809
1978
  },
1810
1979
  { idempotencyKey: config.idempotencyKey?.(order), trigger: config.trigger }
1811
1980
  );
@@ -1943,6 +2112,7 @@ function refusals2(answers) {
1943
2112
  // Annotate the CommonJS export names for ESM import in node:
1944
2113
  0 && (module.exports = {
1945
2114
  GatewayCheatError,
2115
+ Gateways,
1946
2116
  IDEMPOTENCY_KEY_REUSED,
1947
2117
  IdempotencyConflictError,
1948
2118
  NO_WALLET_AVAILABLE,
@@ -1968,6 +2138,7 @@ function refusals2(answers) {
1968
2138
  invoiceToSvg,
1969
2139
  isProblemType,
1970
2140
  isProvablyPaid,
2141
+ isProvablySettled,
1971
2142
  kraken,
1972
2143
  lightningRail,
1973
2144
  lnurlToDataUrl,
@@ -1976,6 +2147,8 @@ function refusals2(answers) {
1976
2147
  minorScaleOf,
1977
2148
  minorUnitsOf,
1978
2149
  msatFor,
2150
+ parseSettlement,
2151
+ parseSettlementRequest,
1979
2152
  parseWatchedWebhook,
1980
2153
  parseWatchedWebhookRequest,
1981
2154
  parseWebhook,