thunder-bridge 0.8.8 → 0.8.9

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
@@ -30,6 +30,8 @@ __export(index_exports, {
30
30
  REQUEST_IN_FLIGHT: () => REQUEST_IN_FLIGHT,
31
31
  ThunderBridge: () => ThunderBridge,
32
32
  UnverifiedRecipientError: () => UnverifiedRecipientError,
33
+ answerVerifyChallenge: () => answerVerifyChallenge,
34
+ answerVerifyChallengeRequest: () => answerVerifyChallengeRequest,
33
35
  answerWebhookChallenge: () => answerWebhookChallenge,
34
36
  answerWebhookChallengeRequest: () => answerWebhookChallengeRequest,
35
37
  bankRail: () => bankRail,
@@ -1287,6 +1289,151 @@ function minorScaleOf(currency) {
1287
1289
  return 10 ** minorUnitsOf(currency);
1288
1290
  }
1289
1291
 
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
+ // src/webhook.ts
1323
+ var SIGNATURE_HEADER = "x-signature";
1324
+ var TIMESTAMP_HEADER = "x-timestamp";
1325
+ var SHARED_SECRET_PREFIX = "sha256=";
1326
+ var GATEWAY_KEY_PREFIX = "ed25519=";
1327
+ var DEFAULT_TOLERANCE_SECS = 300;
1328
+ var CHALLENGE = "webhook-challenge";
1329
+ var VERIFY_CHALLENGE = "verify-challenge";
1330
+ async function verifyWebhookSignature(body, signature, credential, timestamp, options = {}) {
1331
+ 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());
1343
+ }
1344
+ async function parseWebhook(body, signature, credential, timestamp, options = {}) {
1345
+ if (!await verifyWebhookSignature(body, signature, credential, timestamp, options)) return null;
1346
+ const text2 = typeof body === "string" ? body : new TextDecoder().decode(body);
1347
+ try {
1348
+ return paymentFromWire(JSON.parse(text2));
1349
+ } catch {
1350
+ return null;
1351
+ }
1352
+ }
1353
+ async function parseWebhookRequest(request, credential, options = {}) {
1354
+ const signature = request.headers.get(SIGNATURE_HEADER);
1355
+ const timestamp = request.headers.get(TIMESTAMP_HEADER);
1356
+ if (signature === null || timestamp === null) return null;
1357
+ return parseWebhook(await request.text(), signature, credential, timestamp, options);
1358
+ }
1359
+ async function parseWatchedWebhook(body, signature, credential, timestamp, options = {}) {
1360
+ if (!await verifyWebhookSignature(body, signature, credential, timestamp, options)) return null;
1361
+ const text2 = typeof body === "string" ? body : new TextDecoder().decode(body);
1362
+ try {
1363
+ return triggerEventFromWire(JSON.parse(text2));
1364
+ } catch {
1365
+ return null;
1366
+ }
1367
+ }
1368
+ async function parseWatchedWebhookRequest(request, credential, options = {}) {
1369
+ const signature = request.headers.get(SIGNATURE_HEADER);
1370
+ const timestamp = request.headers.get(TIMESTAMP_HEADER);
1371
+ if (signature === null || timestamp === null) return null;
1372
+ return parseWatchedWebhook(await request.text(), signature, credential, timestamp, options);
1373
+ }
1374
+ async function answerWebhookChallenge(body, signature, credential, timestamp, options = {}) {
1375
+ 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
+ });
1384
+ }
1385
+ async function answerWebhookChallengeRequest(request, credential, options = {}) {
1386
+ const signature = request.headers.get(SIGNATURE_HEADER);
1387
+ const timestamp = request.headers.get(TIMESTAMP_HEADER);
1388
+ if (signature === null || timestamp === null) return null;
1389
+ const answer = await answerWebhookChallenge(
1390
+ await request.clone().text(),
1391
+ signature,
1392
+ credential,
1393
+ timestamp,
1394
+ options
1395
+ );
1396
+ return answer === null ? null : new Response(answer, { headers: { "content-type": "application/json" } });
1397
+ }
1398
+ function challenged(text2) {
1399
+ return nonceOf(text2, CHALLENGE);
1400
+ }
1401
+ function nonceOf(text2, type) {
1402
+ try {
1403
+ const said = JSON.parse(text2);
1404
+ if (said["type"] !== type || typeof said["nonce"] !== "string") return null;
1405
+ return said["nonce"];
1406
+ } catch {
1407
+ return null;
1408
+ }
1409
+ }
1410
+ function answerVerifyChallenge(body) {
1411
+ const nonce = nonceOf(body, VERIFY_CHALLENGE);
1412
+ return nonce === null ? null : JSON.stringify({ nonce });
1413
+ }
1414
+ async function answerVerifyChallengeRequest(request) {
1415
+ if (request.method !== "POST") return null;
1416
+ const answer = answerVerifyChallenge(await request.clone().text());
1417
+ return answer === null ? null : new Response(answer, { headers: { "content-type": "application/json" } });
1418
+ }
1419
+ function recent(timestamp, toleranceSecs) {
1420
+ const sent = Number(timestamp);
1421
+ if (!Number.isFinite(sent)) return false;
1422
+ return Math.abs(Math.floor(Date.now() / 1e3) - sent) <= toleranceSecs;
1423
+ }
1424
+ async function sign(secret, timestamp, body) {
1425
+ return hmacHex(secret, signed(timestamp, body));
1426
+ }
1427
+ function signed(timestamp, body) {
1428
+ const encoder = new TextEncoder();
1429
+ const stamp = encoder.encode(`${timestamp}.`);
1430
+ const rest = typeof body === "string" ? encoder.encode(body) : new Uint8Array(body);
1431
+ const payload = new Uint8Array(stamp.length + rest.length);
1432
+ payload.set(stamp);
1433
+ payload.set(rest, stamp.length);
1434
+ return payload;
1435
+ }
1436
+
1290
1437
  // src/bank.ts
1291
1438
  var DEFAULT_CURRENCY = "CZK";
1292
1439
  var DEFAULT_LOOK_BACK_SECS = 7 * 24 * 60 * 60;
@@ -1321,6 +1468,8 @@ function bankVerifyEndpoint(config) {
1321
1468
  "cache-control": `max-age=${config.pollEverySecs ?? DEFAULT_POLL_EVERY_SECS}`
1322
1469
  };
1323
1470
  return async (request) => {
1471
+ const consented = await answerVerifyChallengeRequest(request);
1472
+ if (consented !== null) return consented;
1324
1473
  const asked = readQuery(new URL(request.url));
1325
1474
  if (asked === null) return Response.json({ settled: false }, { status: 400 });
1326
1475
  const subject = subjectOf(asked.reference, asked.amountMinor, asked.currency);
@@ -1791,138 +1940,6 @@ function refusals2(answers) {
1791
1940
  (answer) => answer.reason instanceof Error ? answer.reason.message : String(answer.reason)
1792
1941
  ).join("; ");
1793
1942
  }
1794
-
1795
- // ../core/ed25519.ts
1796
- var ALGORITHM = { name: "Ed25519" };
1797
- var SIGNATURE_BYTES = 64;
1798
- var PUBLIC_KEY_BYTES = 32;
1799
- var PKCS8_HEADER = hexToBytes("302e020100300506032b657004220420");
1800
- async function verifyHex(publicKeyHex, signatureHex, payload) {
1801
- if (!isHex(publicKeyHex) || publicKeyHex.length !== PUBLIC_KEY_BYTES * 2) return false;
1802
- if (!isHex(signatureHex) || signatureHex.length !== SIGNATURE_BYTES * 2) return false;
1803
- try {
1804
- const key = await crypto.subtle.importKey(
1805
- "raw",
1806
- asBuffer2(hexToBytes(publicKeyHex)),
1807
- ALGORITHM,
1808
- false,
1809
- ["verify"]
1810
- );
1811
- return await crypto.subtle.verify(
1812
- ALGORITHM,
1813
- key,
1814
- asBuffer2(hexToBytes(signatureHex)),
1815
- asBuffer2(payload)
1816
- );
1817
- } catch {
1818
- return false;
1819
- }
1820
- }
1821
- function asBuffer2(bytes) {
1822
- return bytes.buffer instanceof ArrayBuffer ? bytes : new Uint8Array(bytes);
1823
- }
1824
-
1825
- // src/webhook.ts
1826
- var SIGNATURE_HEADER = "x-signature";
1827
- var TIMESTAMP_HEADER = "x-timestamp";
1828
- var SHARED_SECRET_PREFIX = "sha256=";
1829
- var GATEWAY_KEY_PREFIX = "ed25519=";
1830
- var DEFAULT_TOLERANCE_SECS = 300;
1831
- var CHALLENGE = "webhook-challenge";
1832
- async function verifyWebhookSignature(body, signature, credential, timestamp, options = {}) {
1833
- if (!recent(timestamp, options.toleranceSecs ?? DEFAULT_TOLERANCE_SECS)) return false;
1834
- if (typeof credential !== "string") {
1835
- if (!signature.startsWith(GATEWAY_KEY_PREFIX)) return false;
1836
- return verifyHex(
1837
- credential.publicKey.toLowerCase(),
1838
- signature.slice(GATEWAY_KEY_PREFIX.length).toLowerCase(),
1839
- signed(timestamp, body)
1840
- );
1841
- }
1842
- if (signature.startsWith(GATEWAY_KEY_PREFIX)) return false;
1843
- const received = signature.startsWith(SHARED_SECRET_PREFIX) ? signature.slice(SHARED_SECRET_PREFIX.length) : signature;
1844
- return equalInConstantTime(await sign(credential, timestamp, body), received.toLowerCase());
1845
- }
1846
- async function parseWebhook(body, signature, credential, timestamp, options = {}) {
1847
- if (!await verifyWebhookSignature(body, signature, credential, timestamp, options)) return null;
1848
- const text2 = typeof body === "string" ? body : new TextDecoder().decode(body);
1849
- try {
1850
- return paymentFromWire(JSON.parse(text2));
1851
- } catch {
1852
- return null;
1853
- }
1854
- }
1855
- async function parseWebhookRequest(request, credential, options = {}) {
1856
- const signature = request.headers.get(SIGNATURE_HEADER);
1857
- const timestamp = request.headers.get(TIMESTAMP_HEADER);
1858
- if (signature === null || timestamp === null) return null;
1859
- return parseWebhook(await request.text(), signature, credential, timestamp, options);
1860
- }
1861
- async function parseWatchedWebhook(body, signature, credential, timestamp, options = {}) {
1862
- if (!await verifyWebhookSignature(body, signature, credential, timestamp, options)) return null;
1863
- const text2 = typeof body === "string" ? body : new TextDecoder().decode(body);
1864
- try {
1865
- return triggerEventFromWire(JSON.parse(text2));
1866
- } catch {
1867
- return null;
1868
- }
1869
- }
1870
- async function parseWatchedWebhookRequest(request, credential, options = {}) {
1871
- const signature = request.headers.get(SIGNATURE_HEADER);
1872
- const timestamp = request.headers.get(TIMESTAMP_HEADER);
1873
- if (signature === null || timestamp === null) return null;
1874
- return parseWatchedWebhook(await request.text(), signature, credential, timestamp, options);
1875
- }
1876
- async function answerWebhookChallenge(body, signature, credential, timestamp, options = {}) {
1877
- if (!await verifyWebhookSignature(body, signature, credential, timestamp, options)) return null;
1878
- const text2 = typeof body === "string" ? body : new TextDecoder().decode(body);
1879
- const nonce = challenged(text2);
1880
- if (nonce === null) return null;
1881
- if (typeof credential !== "string") return JSON.stringify({ nonce });
1882
- return JSON.stringify({
1883
- nonce,
1884
- signature: `${SHARED_SECRET_PREFIX}${await hmacHex(credential, nonce)}`
1885
- });
1886
- }
1887
- async function answerWebhookChallengeRequest(request, credential, options = {}) {
1888
- const signature = request.headers.get(SIGNATURE_HEADER);
1889
- const timestamp = request.headers.get(TIMESTAMP_HEADER);
1890
- if (signature === null || timestamp === null) return null;
1891
- const answer = await answerWebhookChallenge(
1892
- await request.clone().text(),
1893
- signature,
1894
- credential,
1895
- timestamp,
1896
- options
1897
- );
1898
- return answer === null ? null : new Response(answer, { headers: { "content-type": "application/json" } });
1899
- }
1900
- function challenged(text2) {
1901
- try {
1902
- const said = JSON.parse(text2);
1903
- if (said["type"] !== CHALLENGE || typeof said["nonce"] !== "string") return null;
1904
- return said["nonce"];
1905
- } catch {
1906
- return null;
1907
- }
1908
- }
1909
- function recent(timestamp, toleranceSecs) {
1910
- const sent = Number(timestamp);
1911
- if (!Number.isFinite(sent)) return false;
1912
- return Math.abs(Math.floor(Date.now() / 1e3) - sent) <= toleranceSecs;
1913
- }
1914
- async function sign(secret, timestamp, body) {
1915
- return hmacHex(secret, signed(timestamp, body));
1916
- }
1917
- function signed(timestamp, body) {
1918
- const encoder = new TextEncoder();
1919
- const stamp = encoder.encode(`${timestamp}.`);
1920
- const rest = typeof body === "string" ? encoder.encode(body) : new Uint8Array(body);
1921
- const payload = new Uint8Array(stamp.length + rest.length);
1922
- payload.set(stamp);
1923
- payload.set(rest, stamp.length);
1924
- return payload;
1925
- }
1926
1943
  // Annotate the CommonJS export names for ESM import in node:
1927
1944
  0 && (module.exports = {
1928
1945
  GatewayCheatError,
@@ -1935,6 +1952,8 @@ function signed(timestamp, body) {
1935
1952
  REQUEST_IN_FLIGHT,
1936
1953
  ThunderBridge,
1937
1954
  UnverifiedRecipientError,
1955
+ answerVerifyChallenge,
1956
+ answerVerifyChallengeRequest,
1938
1957
  answerWebhookChallenge,
1939
1958
  answerWebhookChallengeRequest,
1940
1959
  bankRail,
package/dist/index.d.cts CHANGED
@@ -381,6 +381,18 @@ declare function answerWebhookChallenge(body: string | Uint8Array, signature: st
381
381
  * the same handler can go on to `parseWebhookRequest` when this was no challenge
382
382
  */
383
383
  declare function answerWebhookChallengeRequest(request: Request, credential: WebhookCredential, options?: WebhookOptions): Promise<Response | null>;
384
+ /**
385
+ * Answer the challenge the gateway sends a verify URL before it will poll it,
386
+ * which is how a caller shows the endpoint agreed to the traffic rather than
387
+ * merely being named. Returns null for anything that is not a challenge, so a
388
+ * verify endpoint hands the request on to its own reading of a payment.
389
+ *
390
+ * The nonce is echoed to whoever asked, which grants them nothing, so there is
391
+ * no signature to check here and no secret to hold
392
+ */
393
+ declare function answerVerifyChallenge(body: string): string | null;
394
+ /** {@link answerVerifyChallenge} against a `Request`, leaving its body unread */
395
+ declare function answerVerifyChallengeRequest(request: Request): Promise<Response | null>;
384
396
 
385
397
  /**
386
398
  * The way a gateway was caught out, every code is a check that held against the
@@ -458,4 +470,4 @@ declare class NoWalletAvailableError extends ProblemError {
458
470
  }, wallets: WalletFailure[]);
459
471
  }
460
472
 
461
- export { type BankTransfer, type BankTransferParams, type BankVerifyConfig, CreatePaymentParams, type Credit, type FioConfig, type GatewayCheatCode, GatewayCheatError, IDEMPOTENCY_KEY_REUSED, type IdempotencyConflict, IdempotencyConflictError, type Invoice, type MedianOptions, NO_WALLET_AVAILABLE, NoWalletAvailableError, PAYMENT_ALREADY_WATCHED, Payment, ProblemError, type QrOptions, REQUEST_IN_FLIGHT, type Statement, ThunderBridge, type Ticker, TriggerEvent, UnverifiedRecipientError, WalletFailure, type WebhookCredential, type WebhookOptions, answerWebhookChallenge, answerWebhookChallengeRequest, bankTransfer, bankVerifyEndpoint, bitstamp, coinbase, coinmate, decodeInvoice, fioStatement, invoiceToDataUrl, invoiceToSvg, isProblemType, isProvablyPaid, kraken, lnurlToDataUrl, lnurlToSvg, medianOf, minorScaleOf, minorUnitsOf, msatFor, parseWatchedWebhook, parseWatchedWebhookRequest, parseWebhook, parseWebhookRequest, preimageMatchesHash, proveOrigin, proveSettlement, qrToDataUrl, qrToSvg, seal, spdToDataUrl, spdToSvg, toLnurl, unseal, verifyWebhookSignature };
473
+ export { type BankTransfer, type BankTransferParams, type BankVerifyConfig, CreatePaymentParams, type Credit, type FioConfig, type GatewayCheatCode, GatewayCheatError, IDEMPOTENCY_KEY_REUSED, type IdempotencyConflict, IdempotencyConflictError, type Invoice, type MedianOptions, NO_WALLET_AVAILABLE, NoWalletAvailableError, PAYMENT_ALREADY_WATCHED, Payment, ProblemError, type QrOptions, REQUEST_IN_FLIGHT, type Statement, ThunderBridge, type Ticker, TriggerEvent, UnverifiedRecipientError, WalletFailure, type WebhookCredential, type WebhookOptions, answerVerifyChallenge, answerVerifyChallengeRequest, answerWebhookChallenge, answerWebhookChallengeRequest, bankTransfer, bankVerifyEndpoint, bitstamp, coinbase, coinmate, decodeInvoice, fioStatement, invoiceToDataUrl, invoiceToSvg, isProblemType, isProvablyPaid, kraken, lnurlToDataUrl, lnurlToSvg, medianOf, minorScaleOf, minorUnitsOf, msatFor, parseWatchedWebhook, parseWatchedWebhookRequest, parseWebhook, parseWebhookRequest, preimageMatchesHash, proveOrigin, proveSettlement, qrToDataUrl, qrToSvg, seal, spdToDataUrl, spdToSvg, toLnurl, unseal, verifyWebhookSignature };
package/dist/index.d.ts CHANGED
@@ -381,6 +381,18 @@ declare function answerWebhookChallenge(body: string | Uint8Array, signature: st
381
381
  * the same handler can go on to `parseWebhookRequest` when this was no challenge
382
382
  */
383
383
  declare function answerWebhookChallengeRequest(request: Request, credential: WebhookCredential, options?: WebhookOptions): Promise<Response | null>;
384
+ /**
385
+ * Answer the challenge the gateway sends a verify URL before it will poll it,
386
+ * which is how a caller shows the endpoint agreed to the traffic rather than
387
+ * merely being named. Returns null for anything that is not a challenge, so a
388
+ * verify endpoint hands the request on to its own reading of a payment.
389
+ *
390
+ * The nonce is echoed to whoever asked, which grants them nothing, so there is
391
+ * no signature to check here and no secret to hold
392
+ */
393
+ declare function answerVerifyChallenge(body: string): string | null;
394
+ /** {@link answerVerifyChallenge} against a `Request`, leaving its body unread */
395
+ declare function answerVerifyChallengeRequest(request: Request): Promise<Response | null>;
384
396
 
385
397
  /**
386
398
  * The way a gateway was caught out, every code is a check that held against the
@@ -458,4 +470,4 @@ declare class NoWalletAvailableError extends ProblemError {
458
470
  }, wallets: WalletFailure[]);
459
471
  }
460
472
 
461
- export { type BankTransfer, type BankTransferParams, type BankVerifyConfig, CreatePaymentParams, type Credit, type FioConfig, type GatewayCheatCode, GatewayCheatError, IDEMPOTENCY_KEY_REUSED, type IdempotencyConflict, IdempotencyConflictError, type Invoice, type MedianOptions, NO_WALLET_AVAILABLE, NoWalletAvailableError, PAYMENT_ALREADY_WATCHED, Payment, ProblemError, type QrOptions, REQUEST_IN_FLIGHT, type Statement, ThunderBridge, type Ticker, TriggerEvent, UnverifiedRecipientError, WalletFailure, type WebhookCredential, type WebhookOptions, answerWebhookChallenge, answerWebhookChallengeRequest, bankTransfer, bankVerifyEndpoint, bitstamp, coinbase, coinmate, decodeInvoice, fioStatement, invoiceToDataUrl, invoiceToSvg, isProblemType, isProvablyPaid, kraken, lnurlToDataUrl, lnurlToSvg, medianOf, minorScaleOf, minorUnitsOf, msatFor, parseWatchedWebhook, parseWatchedWebhookRequest, parseWebhook, parseWebhookRequest, preimageMatchesHash, proveOrigin, proveSettlement, qrToDataUrl, qrToSvg, seal, spdToDataUrl, spdToSvg, toLnurl, unseal, verifyWebhookSignature };
473
+ export { type BankTransfer, type BankTransferParams, type BankVerifyConfig, CreatePaymentParams, type Credit, type FioConfig, type GatewayCheatCode, GatewayCheatError, IDEMPOTENCY_KEY_REUSED, type IdempotencyConflict, IdempotencyConflictError, type Invoice, type MedianOptions, NO_WALLET_AVAILABLE, NoWalletAvailableError, PAYMENT_ALREADY_WATCHED, Payment, ProblemError, type QrOptions, REQUEST_IN_FLIGHT, type Statement, ThunderBridge, type Ticker, TriggerEvent, UnverifiedRecipientError, WalletFailure, type WebhookCredential, type WebhookOptions, answerVerifyChallenge, answerVerifyChallengeRequest, answerWebhookChallenge, answerWebhookChallengeRequest, bankTransfer, bankVerifyEndpoint, bitstamp, coinbase, coinmate, decodeInvoice, fioStatement, invoiceToDataUrl, invoiceToSvg, isProblemType, isProvablyPaid, kraken, lnurlToDataUrl, lnurlToSvg, medianOf, minorScaleOf, minorUnitsOf, msatFor, parseWatchedWebhook, parseWatchedWebhookRequest, parseWebhook, parseWebhookRequest, preimageMatchesHash, proveOrigin, proveSettlement, qrToDataUrl, qrToSvg, seal, spdToDataUrl, spdToSvg, toLnurl, unseal, verifyWebhookSignature };
package/dist/index.js CHANGED
@@ -1215,6 +1215,151 @@ function minorScaleOf(currency) {
1215
1215
  return 10 ** minorUnitsOf(currency);
1216
1216
  }
1217
1217
 
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
+ // src/webhook.ts
1249
+ var SIGNATURE_HEADER = "x-signature";
1250
+ var TIMESTAMP_HEADER = "x-timestamp";
1251
+ var SHARED_SECRET_PREFIX = "sha256=";
1252
+ var GATEWAY_KEY_PREFIX = "ed25519=";
1253
+ var DEFAULT_TOLERANCE_SECS = 300;
1254
+ var CHALLENGE = "webhook-challenge";
1255
+ var VERIFY_CHALLENGE = "verify-challenge";
1256
+ async function verifyWebhookSignature(body, signature, credential, timestamp, options = {}) {
1257
+ 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());
1269
+ }
1270
+ async function parseWebhook(body, signature, credential, timestamp, options = {}) {
1271
+ if (!await verifyWebhookSignature(body, signature, credential, timestamp, options)) return null;
1272
+ const text2 = typeof body === "string" ? body : new TextDecoder().decode(body);
1273
+ try {
1274
+ return paymentFromWire(JSON.parse(text2));
1275
+ } catch {
1276
+ return null;
1277
+ }
1278
+ }
1279
+ async function parseWebhookRequest(request, credential, options = {}) {
1280
+ const signature = request.headers.get(SIGNATURE_HEADER);
1281
+ const timestamp = request.headers.get(TIMESTAMP_HEADER);
1282
+ if (signature === null || timestamp === null) return null;
1283
+ return parseWebhook(await request.text(), signature, credential, timestamp, options);
1284
+ }
1285
+ async function parseWatchedWebhook(body, signature, credential, timestamp, options = {}) {
1286
+ if (!await verifyWebhookSignature(body, signature, credential, timestamp, options)) return null;
1287
+ const text2 = typeof body === "string" ? body : new TextDecoder().decode(body);
1288
+ try {
1289
+ return triggerEventFromWire(JSON.parse(text2));
1290
+ } catch {
1291
+ return null;
1292
+ }
1293
+ }
1294
+ async function parseWatchedWebhookRequest(request, credential, options = {}) {
1295
+ const signature = request.headers.get(SIGNATURE_HEADER);
1296
+ const timestamp = request.headers.get(TIMESTAMP_HEADER);
1297
+ if (signature === null || timestamp === null) return null;
1298
+ return parseWatchedWebhook(await request.text(), signature, credential, timestamp, options);
1299
+ }
1300
+ async function answerWebhookChallenge(body, signature, credential, timestamp, options = {}) {
1301
+ 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
+ });
1310
+ }
1311
+ async function answerWebhookChallengeRequest(request, credential, options = {}) {
1312
+ const signature = request.headers.get(SIGNATURE_HEADER);
1313
+ const timestamp = request.headers.get(TIMESTAMP_HEADER);
1314
+ if (signature === null || timestamp === null) return null;
1315
+ const answer = await answerWebhookChallenge(
1316
+ await request.clone().text(),
1317
+ signature,
1318
+ credential,
1319
+ timestamp,
1320
+ options
1321
+ );
1322
+ return answer === null ? null : new Response(answer, { headers: { "content-type": "application/json" } });
1323
+ }
1324
+ function challenged(text2) {
1325
+ return nonceOf(text2, CHALLENGE);
1326
+ }
1327
+ function nonceOf(text2, type) {
1328
+ try {
1329
+ const said = JSON.parse(text2);
1330
+ if (said["type"] !== type || typeof said["nonce"] !== "string") return null;
1331
+ return said["nonce"];
1332
+ } catch {
1333
+ return null;
1334
+ }
1335
+ }
1336
+ function answerVerifyChallenge(body) {
1337
+ const nonce = nonceOf(body, VERIFY_CHALLENGE);
1338
+ return nonce === null ? null : JSON.stringify({ nonce });
1339
+ }
1340
+ async function answerVerifyChallengeRequest(request) {
1341
+ if (request.method !== "POST") return null;
1342
+ const answer = answerVerifyChallenge(await request.clone().text());
1343
+ return answer === null ? null : new Response(answer, { headers: { "content-type": "application/json" } });
1344
+ }
1345
+ function recent(timestamp, toleranceSecs) {
1346
+ const sent = Number(timestamp);
1347
+ if (!Number.isFinite(sent)) return false;
1348
+ return Math.abs(Math.floor(Date.now() / 1e3) - sent) <= toleranceSecs;
1349
+ }
1350
+ async function sign(secret, timestamp, body) {
1351
+ return hmacHex(secret, signed(timestamp, body));
1352
+ }
1353
+ function signed(timestamp, body) {
1354
+ const encoder = new TextEncoder();
1355
+ const stamp = encoder.encode(`${timestamp}.`);
1356
+ const rest = typeof body === "string" ? encoder.encode(body) : new Uint8Array(body);
1357
+ const payload = new Uint8Array(stamp.length + rest.length);
1358
+ payload.set(stamp);
1359
+ payload.set(rest, stamp.length);
1360
+ return payload;
1361
+ }
1362
+
1218
1363
  // src/bank.ts
1219
1364
  var DEFAULT_CURRENCY = "CZK";
1220
1365
  var DEFAULT_LOOK_BACK_SECS = 7 * 24 * 60 * 60;
@@ -1249,6 +1394,8 @@ function bankVerifyEndpoint(config) {
1249
1394
  "cache-control": `max-age=${config.pollEverySecs ?? DEFAULT_POLL_EVERY_SECS}`
1250
1395
  };
1251
1396
  return async (request) => {
1397
+ const consented = await answerVerifyChallengeRequest(request);
1398
+ if (consented !== null) return consented;
1252
1399
  const asked = readQuery(new URL(request.url));
1253
1400
  if (asked === null) return Response.json({ settled: false }, { status: 400 });
1254
1401
  const subject = subjectOf(asked.reference, asked.amountMinor, asked.currency);
@@ -1719,138 +1866,6 @@ function refusals2(answers) {
1719
1866
  (answer) => answer.reason instanceof Error ? answer.reason.message : String(answer.reason)
1720
1867
  ).join("; ");
1721
1868
  }
1722
-
1723
- // ../core/ed25519.ts
1724
- var ALGORITHM = { name: "Ed25519" };
1725
- var SIGNATURE_BYTES = 64;
1726
- var PUBLIC_KEY_BYTES = 32;
1727
- var PKCS8_HEADER = hexToBytes("302e020100300506032b657004220420");
1728
- async function verifyHex(publicKeyHex, signatureHex, payload) {
1729
- if (!isHex(publicKeyHex) || publicKeyHex.length !== PUBLIC_KEY_BYTES * 2) return false;
1730
- if (!isHex(signatureHex) || signatureHex.length !== SIGNATURE_BYTES * 2) return false;
1731
- try {
1732
- const key = await crypto.subtle.importKey(
1733
- "raw",
1734
- asBuffer2(hexToBytes(publicKeyHex)),
1735
- ALGORITHM,
1736
- false,
1737
- ["verify"]
1738
- );
1739
- return await crypto.subtle.verify(
1740
- ALGORITHM,
1741
- key,
1742
- asBuffer2(hexToBytes(signatureHex)),
1743
- asBuffer2(payload)
1744
- );
1745
- } catch {
1746
- return false;
1747
- }
1748
- }
1749
- function asBuffer2(bytes) {
1750
- return bytes.buffer instanceof ArrayBuffer ? bytes : new Uint8Array(bytes);
1751
- }
1752
-
1753
- // src/webhook.ts
1754
- var SIGNATURE_HEADER = "x-signature";
1755
- var TIMESTAMP_HEADER = "x-timestamp";
1756
- var SHARED_SECRET_PREFIX = "sha256=";
1757
- var GATEWAY_KEY_PREFIX = "ed25519=";
1758
- var DEFAULT_TOLERANCE_SECS = 300;
1759
- var CHALLENGE = "webhook-challenge";
1760
- async function verifyWebhookSignature(body, signature, credential, timestamp, options = {}) {
1761
- if (!recent(timestamp, options.toleranceSecs ?? DEFAULT_TOLERANCE_SECS)) return false;
1762
- if (typeof credential !== "string") {
1763
- if (!signature.startsWith(GATEWAY_KEY_PREFIX)) return false;
1764
- return verifyHex(
1765
- credential.publicKey.toLowerCase(),
1766
- signature.slice(GATEWAY_KEY_PREFIX.length).toLowerCase(),
1767
- signed(timestamp, body)
1768
- );
1769
- }
1770
- if (signature.startsWith(GATEWAY_KEY_PREFIX)) return false;
1771
- const received = signature.startsWith(SHARED_SECRET_PREFIX) ? signature.slice(SHARED_SECRET_PREFIX.length) : signature;
1772
- return equalInConstantTime(await sign(credential, timestamp, body), received.toLowerCase());
1773
- }
1774
- async function parseWebhook(body, signature, credential, timestamp, options = {}) {
1775
- if (!await verifyWebhookSignature(body, signature, credential, timestamp, options)) return null;
1776
- const text2 = typeof body === "string" ? body : new TextDecoder().decode(body);
1777
- try {
1778
- return paymentFromWire(JSON.parse(text2));
1779
- } catch {
1780
- return null;
1781
- }
1782
- }
1783
- async function parseWebhookRequest(request, credential, options = {}) {
1784
- const signature = request.headers.get(SIGNATURE_HEADER);
1785
- const timestamp = request.headers.get(TIMESTAMP_HEADER);
1786
- if (signature === null || timestamp === null) return null;
1787
- return parseWebhook(await request.text(), signature, credential, timestamp, options);
1788
- }
1789
- async function parseWatchedWebhook(body, signature, credential, timestamp, options = {}) {
1790
- if (!await verifyWebhookSignature(body, signature, credential, timestamp, options)) return null;
1791
- const text2 = typeof body === "string" ? body : new TextDecoder().decode(body);
1792
- try {
1793
- return triggerEventFromWire(JSON.parse(text2));
1794
- } catch {
1795
- return null;
1796
- }
1797
- }
1798
- async function parseWatchedWebhookRequest(request, credential, options = {}) {
1799
- const signature = request.headers.get(SIGNATURE_HEADER);
1800
- const timestamp = request.headers.get(TIMESTAMP_HEADER);
1801
- if (signature === null || timestamp === null) return null;
1802
- return parseWatchedWebhook(await request.text(), signature, credential, timestamp, options);
1803
- }
1804
- async function answerWebhookChallenge(body, signature, credential, timestamp, options = {}) {
1805
- if (!await verifyWebhookSignature(body, signature, credential, timestamp, options)) return null;
1806
- const text2 = typeof body === "string" ? body : new TextDecoder().decode(body);
1807
- const nonce = challenged(text2);
1808
- if (nonce === null) return null;
1809
- if (typeof credential !== "string") return JSON.stringify({ nonce });
1810
- return JSON.stringify({
1811
- nonce,
1812
- signature: `${SHARED_SECRET_PREFIX}${await hmacHex(credential, nonce)}`
1813
- });
1814
- }
1815
- async function answerWebhookChallengeRequest(request, credential, options = {}) {
1816
- const signature = request.headers.get(SIGNATURE_HEADER);
1817
- const timestamp = request.headers.get(TIMESTAMP_HEADER);
1818
- if (signature === null || timestamp === null) return null;
1819
- const answer = await answerWebhookChallenge(
1820
- await request.clone().text(),
1821
- signature,
1822
- credential,
1823
- timestamp,
1824
- options
1825
- );
1826
- return answer === null ? null : new Response(answer, { headers: { "content-type": "application/json" } });
1827
- }
1828
- function challenged(text2) {
1829
- try {
1830
- const said = JSON.parse(text2);
1831
- if (said["type"] !== CHALLENGE || typeof said["nonce"] !== "string") return null;
1832
- return said["nonce"];
1833
- } catch {
1834
- return null;
1835
- }
1836
- }
1837
- function recent(timestamp, toleranceSecs) {
1838
- const sent = Number(timestamp);
1839
- if (!Number.isFinite(sent)) return false;
1840
- return Math.abs(Math.floor(Date.now() / 1e3) - sent) <= toleranceSecs;
1841
- }
1842
- async function sign(secret, timestamp, body) {
1843
- return hmacHex(secret, signed(timestamp, body));
1844
- }
1845
- function signed(timestamp, body) {
1846
- const encoder = new TextEncoder();
1847
- const stamp = encoder.encode(`${timestamp}.`);
1848
- const rest = typeof body === "string" ? encoder.encode(body) : new Uint8Array(body);
1849
- const payload = new Uint8Array(stamp.length + rest.length);
1850
- payload.set(stamp);
1851
- payload.set(rest, stamp.length);
1852
- return payload;
1853
- }
1854
1869
  export {
1855
1870
  GatewayCheatError,
1856
1871
  IDEMPOTENCY_KEY_REUSED,
@@ -1862,6 +1877,8 @@ export {
1862
1877
  REQUEST_IN_FLIGHT,
1863
1878
  ThunderBridge,
1864
1879
  UnverifiedRecipientError,
1880
+ answerVerifyChallenge,
1881
+ answerVerifyChallengeRequest,
1865
1882
  answerWebhookChallenge,
1866
1883
  answerWebhookChallengeRequest,
1867
1884
  bankRail,
package/dist/server.cjs CHANGED
@@ -494,6 +494,7 @@ var NoWalletAvailable = class extends Error {
494
494
  // ../core/lnurl.ts
495
495
  var VERIFY_WITHOUT_PREIMAGE = ["zeuspay.com", "zeusnuts.com", "ecash.love"];
496
496
  var RESOLVE_TIMEOUT_MS = 3e4;
497
+ var NAMES_ONE_ACCOUNT = /^[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)*$/;
497
498
  var MIN_PACE_SECS = 1;
498
499
  var MAX_PACE_SECS = 3600;
499
500
  var MIN_PER_SECOND = 0.01;
@@ -622,11 +623,19 @@ function withAmount(callback, amountMsat) {
622
623
  }
623
624
  function splitAddress(address) {
624
625
  const [user, domain, ...rest] = address.split("@");
625
- if (!user || !domain || rest.length > 0 || !publicHttps(`https://${domain}/`)) {
626
+ if (!user || !domain || rest.length > 0 || !NAMES_ONE_ACCOUNT.test(user) || !isNothingButAHost(domain) || !publicHttps(`https://${domain}/`)) {
626
627
  throw new WalletRefused("address-unusable", `${address} is not a usable lightning address`);
627
628
  }
628
629
  return [user, domain];
629
630
  }
631
+ function isNothingButAHost(domain) {
632
+ try {
633
+ const parsed = new URL(`https://${domain}/`);
634
+ return parsed.host === domain.toLowerCase() && parsed.pathname === "/" && parsed.search === "" && parsed.hash === "";
635
+ } catch {
636
+ return false;
637
+ }
638
+ }
630
639
  async function fetchJson(url, deadline) {
631
640
  return (await answeredJson(url, deadline)).said;
632
641
  }
@@ -831,6 +840,30 @@ function refuse(reason) {
831
840
  return Response.json({ status: "ERROR", reason });
832
841
  }
833
842
 
843
+ // ../core/ed25519.ts
844
+ var PKCS8_HEADER = hexToBytes("302e020100300506032b657004220420");
845
+
846
+ // src/webhook.ts
847
+ var VERIFY_CHALLENGE = "verify-challenge";
848
+ function nonceOf(text2, type) {
849
+ try {
850
+ const said = JSON.parse(text2);
851
+ if (said["type"] !== type || typeof said["nonce"] !== "string") return null;
852
+ return said["nonce"];
853
+ } catch {
854
+ return null;
855
+ }
856
+ }
857
+ function answerVerifyChallenge(body) {
858
+ const nonce = nonceOf(body, VERIFY_CHALLENGE);
859
+ return nonce === null ? null : JSON.stringify({ nonce });
860
+ }
861
+ async function answerVerifyChallengeRequest(request) {
862
+ if (request.method !== "POST") return null;
863
+ const answer = answerVerifyChallenge(await request.clone().text());
864
+ return answer === null ? null : new Response(answer, { headers: { "content-type": "application/json" } });
865
+ }
866
+
834
867
  // src/qr.ts
835
868
  var import_uqr = require("uqr");
836
869
  function encodeForQr(destination) {
@@ -850,6 +883,8 @@ function lightningVerifyEndpoint(config) {
850
883
  "cache-control": `max-age=${config.pollEverySecs ?? DEFAULT_POLL_EVERY_SECS}`
851
884
  };
852
885
  return async (request) => {
886
+ const consented = await answerVerifyChallengeRequest(request);
887
+ if (consented !== null) return consented;
853
888
  const sealed = new URL(request.url).searchParams.get(WALLET);
854
889
  if (sealed === null) return Response.json({ settled: false }, { status: 400 });
855
890
  const opened = await unseal(config.secret, sealed);
package/dist/server.js CHANGED
@@ -455,6 +455,7 @@ var NoWalletAvailable = class extends Error {
455
455
  // ../core/lnurl.ts
456
456
  var VERIFY_WITHOUT_PREIMAGE = ["zeuspay.com", "zeusnuts.com", "ecash.love"];
457
457
  var RESOLVE_TIMEOUT_MS = 3e4;
458
+ var NAMES_ONE_ACCOUNT = /^[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)*$/;
458
459
  var MIN_PACE_SECS = 1;
459
460
  var MAX_PACE_SECS = 3600;
460
461
  var MIN_PER_SECOND = 0.01;
@@ -583,11 +584,19 @@ function withAmount(callback, amountMsat) {
583
584
  }
584
585
  function splitAddress(address) {
585
586
  const [user, domain, ...rest] = address.split("@");
586
- if (!user || !domain || rest.length > 0 || !publicHttps(`https://${domain}/`)) {
587
+ if (!user || !domain || rest.length > 0 || !NAMES_ONE_ACCOUNT.test(user) || !isNothingButAHost(domain) || !publicHttps(`https://${domain}/`)) {
587
588
  throw new WalletRefused("address-unusable", `${address} is not a usable lightning address`);
588
589
  }
589
590
  return [user, domain];
590
591
  }
592
+ function isNothingButAHost(domain) {
593
+ try {
594
+ const parsed = new URL(`https://${domain}/`);
595
+ return parsed.host === domain.toLowerCase() && parsed.pathname === "/" && parsed.search === "" && parsed.hash === "";
596
+ } catch {
597
+ return false;
598
+ }
599
+ }
591
600
  async function fetchJson(url, deadline) {
592
601
  return (await answeredJson(url, deadline)).said;
593
602
  }
@@ -792,6 +801,30 @@ function refuse(reason) {
792
801
  return Response.json({ status: "ERROR", reason });
793
802
  }
794
803
 
804
+ // ../core/ed25519.ts
805
+ var PKCS8_HEADER = hexToBytes("302e020100300506032b657004220420");
806
+
807
+ // src/webhook.ts
808
+ var VERIFY_CHALLENGE = "verify-challenge";
809
+ function nonceOf(text2, type) {
810
+ try {
811
+ const said = JSON.parse(text2);
812
+ if (said["type"] !== type || typeof said["nonce"] !== "string") return null;
813
+ return said["nonce"];
814
+ } catch {
815
+ return null;
816
+ }
817
+ }
818
+ function answerVerifyChallenge(body) {
819
+ const nonce = nonceOf(body, VERIFY_CHALLENGE);
820
+ return nonce === null ? null : JSON.stringify({ nonce });
821
+ }
822
+ async function answerVerifyChallengeRequest(request) {
823
+ if (request.method !== "POST") return null;
824
+ const answer = answerVerifyChallenge(await request.clone().text());
825
+ return answer === null ? null : new Response(answer, { headers: { "content-type": "application/json" } });
826
+ }
827
+
795
828
  // src/qr.ts
796
829
  import { encode } from "uqr";
797
830
  function encodeForQr(destination) {
@@ -811,6 +844,8 @@ function lightningVerifyEndpoint(config) {
811
844
  "cache-control": `max-age=${config.pollEverySecs ?? DEFAULT_POLL_EVERY_SECS}`
812
845
  };
813
846
  return async (request) => {
847
+ const consented = await answerVerifyChallengeRequest(request);
848
+ if (consented !== null) return consented;
814
849
  const sealed = new URL(request.url).searchParams.get(WALLET);
815
850
  if (sealed === null) return Response.json({ settled: false }, { status: 400 });
816
851
  const opened = await unseal(config.secret, sealed);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thunder-bridge",
3
- "version": "0.8.8",
3
+ "version": "0.8.9",
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",