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.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, secret: params.webhookSecret } : void 0,
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, secret: params.webhookSecret } : void 0
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 headers = this.sending();
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: createRequestBody(
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: quoteRequestBody(params)
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 response = await fetch(`${this.baseUrl}/incoming-payments/${encodeURIComponent(id)}`, {
738
- headers: this.reading()
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 response = await fetch(`${this.baseUrl}/incoming-payments/${encodeURIComponent(id)}`, {
751
- headers: this.reading()
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 query = limit === void 0 ? "" : `?limit=${limit}`;
775
- const response = await fetch(`${this.baseUrl}/incoming-payments${query}`, {
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: watchRequestBody(
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: JSON.stringify(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
- reading() {
1037
- return this.token === null ? {} : { authorization: `Bearer ${this.token}` };
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 = fromBase64Url(sealed.slice(VERSION.length + 1));
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 fromBase64Url(text2) {
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 SIGNATURE_HEADER = "x-signature";
1250
- var TIMESTAMP_HEADER = "x-timestamp";
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 (typeof credential !== "string") {
1259
- if (!signature.startsWith(GATEWAY_KEY_PREFIX)) return false;
1260
- return verifyHex(
1261
- credential.publicKey.toLowerCase(),
1262
- signature.slice(GATEWAY_KEY_PREFIX.length).toLowerCase(),
1263
- signed(timestamp, body)
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(SIGNATURE_HEADER);
1281
- const timestamp = request.headers.get(TIMESTAMP_HEADER);
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(SIGNATURE_HEADER);
1296
- const timestamp = request.headers.get(TIMESTAMP_HEADER);
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 text2 = typeof body === "string" ? body : new TextDecoder().decode(body);
1303
- const nonce = challenged(text2);
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(SIGNATURE_HEADER);
1313
- const timestamp = request.headers.get(TIMESTAMP_HEADER);
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,