thunder-bridge 0.8.8 → 0.8.10

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
@@ -402,20 +402,22 @@ var REDIRECTS = [301, 302, 303, 307, 308];
402
402
  var KEEPS_THE_METHOD = [307, 308];
403
403
  var CREDENTIALS = ["authorization", "cookie", "proxy-authorization"];
404
404
  var BODY_LIMIT_BYTES = 262144;
405
+ var throughFetch = (url, sent, signal) => fetch(url, {
406
+ method: sent.method ?? "GET",
407
+ headers: sent.headers ?? {},
408
+ body: sent.body,
409
+ redirect: "manual",
410
+ signal
411
+ });
412
+ var send = throughFetch;
405
413
  async function ask(url, sent = {}) {
406
414
  const capped = AbortSignal.timeout(HTTP_TIMEOUT_MS);
407
415
  const signal = sent.deadline ? AbortSignal.any([sent.deadline, capped]) : capped;
408
416
  let target = url;
409
417
  let carried = sent;
410
418
  for (let hop = 0; hop <= HOPS_FOLLOWED; hop += 1) {
411
- await refuseUnlessPublic(target);
412
- const response = await fetch(target, {
413
- method: carried.method ?? "GET",
414
- headers: carried.headers ?? {},
415
- body: carried.body,
416
- redirect: "manual",
417
- signal
418
- });
419
+ const at = await addressesToReach(target);
420
+ const response = await send(target, carried, signal, at);
419
421
  if (!REDIRECTS.includes(response.status)) return await answerOf(response);
420
422
  const location = response.headers.get("location");
421
423
  if (location === null) {
@@ -429,7 +431,15 @@ async function ask(url, sent = {}) {
429
431
  }
430
432
  throw new Error(`${url} redirected more than ${HOPS_FOLLOWED} times`);
431
433
  }
432
- async function resolvesNothingPrivate(url) {
434
+ async function addressesToReach(url) {
435
+ if (!publicHttps(url)) throw new Error(`${url} is not a public https URL`);
436
+ const found = await resolved(url);
437
+ if (found.length === 0 || !found.every((one) => publicAddress(one.address))) {
438
+ throw new Error(`${url} resolves to an address we do not reach`);
439
+ }
440
+ return found;
441
+ }
442
+ async function resolved(url) {
433
443
  const { lookup } = await import("dns/promises");
434
444
  const { setTimeout: sleep } = await import("timers/promises");
435
445
  const host = new URL(url).hostname.replace(/^\[|]$/g, "");
@@ -437,11 +447,7 @@ async function resolvesNothingPrivate(url) {
437
447
  lookup(host, { all: true, verbatim: true }).catch(() => []),
438
448
  sleep(LOOKUP_TIMEOUT_MS, null, { ref: false })
439
449
  ]);
440
- return found !== null && found.every((one) => publicAddress(one.address));
441
- }
442
- async function refuseUnlessPublic(url) {
443
- if (!publicHttps(url)) throw new Error(`${url} is not a public https URL`);
444
- if (!await resolvesNothingPrivate(url)) throw new Error(`${url} resolves to an address we do not reach`);
450
+ return found ?? [];
445
451
  }
446
452
  async function answerOf(response) {
447
453
  const { status, ok, headers } = response;
@@ -494,6 +500,7 @@ var NoWalletAvailable = class extends Error {
494
500
  // ../core/lnurl.ts
495
501
  var VERIFY_WITHOUT_PREIMAGE = ["zeuspay.com", "zeusnuts.com", "ecash.love"];
496
502
  var RESOLVE_TIMEOUT_MS = 3e4;
503
+ var NAMES_ONE_ACCOUNT = /^[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)*$/;
497
504
  var MIN_PACE_SECS = 1;
498
505
  var MAX_PACE_SECS = 3600;
499
506
  var MIN_PER_SECOND = 0.01;
@@ -622,11 +629,19 @@ function withAmount(callback, amountMsat) {
622
629
  }
623
630
  function splitAddress(address) {
624
631
  const [user, domain, ...rest] = address.split("@");
625
- if (!user || !domain || rest.length > 0 || !publicHttps(`https://${domain}/`)) {
632
+ if (!user || !domain || rest.length > 0 || !NAMES_ONE_ACCOUNT.test(user) || !isNothingButAHost(domain) || !publicHttps(`https://${domain}/`)) {
626
633
  throw new WalletRefused("address-unusable", `${address} is not a usable lightning address`);
627
634
  }
628
635
  return [user, domain];
629
636
  }
637
+ function isNothingButAHost(domain) {
638
+ try {
639
+ const parsed = new URL(`https://${domain}/`);
640
+ return parsed.host === domain.toLowerCase() && parsed.pathname === "/" && parsed.search === "" && parsed.hash === "";
641
+ } catch {
642
+ return false;
643
+ }
644
+ }
630
645
  async function fetchJson(url, deadline) {
631
646
  return (await answeredJson(url, deadline)).said;
632
647
  }
@@ -799,21 +814,21 @@ async function mintThroughGateway(config, address, amountMsat, nonce) {
799
814
  return { bolt11: payment.bolt11, verifyUrl: payment.verifyUrl };
800
815
  }
801
816
  async function mintBlind(config, address, amountMsat) {
802
- const resolved = await resolve([address], amountMsat);
803
- const minted = { ...resolved, amountMsat, lnAddress: resolved.address };
817
+ const resolved2 = await resolve([address], amountMsat);
818
+ const minted = { ...resolved2, amountMsat, lnAddress: resolved2.address };
804
819
  const locked = config.sealed;
805
820
  try {
806
821
  await config.gateway.watchPayment({
807
- paymentHash: resolved.paymentHash,
808
- verifyUrl: resolved.verifyUrl,
809
- expiresAt: resolved.expiresAt,
822
+ paymentHash: resolved2.paymentHash,
823
+ verifyUrl: resolved2.verifyUrl,
824
+ expiresAt: resolved2.expiresAt,
810
825
  trigger: config.watchSecret,
811
826
  sealed: locked ? await seal(locked.secret, JSON.stringify(locked.data(minted))) : void 0
812
827
  });
813
828
  } catch (refused) {
814
829
  if (!alreadyWatched(refused)) throw refused;
815
830
  }
816
- return { bolt11: resolved.bolt11, verifyUrl: resolved.verifyUrl };
831
+ return { bolt11: resolved2.bolt11, verifyUrl: resolved2.verifyUrl };
817
832
  }
818
833
  function alreadyWatched(refused) {
819
834
  return refused instanceof ProblemError && isProblemType(refused, PAYMENT_ALREADY_WATCHED);
@@ -831,6 +846,30 @@ function refuse(reason) {
831
846
  return Response.json({ status: "ERROR", reason });
832
847
  }
833
848
 
849
+ // ../core/ed25519.ts
850
+ var PKCS8_HEADER = hexToBytes("302e020100300506032b657004220420");
851
+
852
+ // src/webhook.ts
853
+ var VERIFY_CHALLENGE = "verify-challenge";
854
+ function nonceOf(text2, type) {
855
+ try {
856
+ const said = JSON.parse(text2);
857
+ if (said["type"] !== type || typeof said["nonce"] !== "string") return null;
858
+ return said["nonce"];
859
+ } catch {
860
+ return null;
861
+ }
862
+ }
863
+ function answerVerifyChallenge(body) {
864
+ const nonce = nonceOf(body, VERIFY_CHALLENGE);
865
+ return nonce === null ? null : JSON.stringify({ nonce });
866
+ }
867
+ async function answerVerifyChallengeRequest(request) {
868
+ if (request.method !== "POST") return null;
869
+ const answer = answerVerifyChallenge(await request.clone().text());
870
+ return answer === null ? null : new Response(answer, { headers: { "content-type": "application/json" } });
871
+ }
872
+
834
873
  // src/qr.ts
835
874
  var import_uqr = require("uqr");
836
875
  function encodeForQr(destination) {
@@ -850,6 +889,8 @@ function lightningVerifyEndpoint(config) {
850
889
  "cache-control": `max-age=${config.pollEverySecs ?? DEFAULT_POLL_EVERY_SECS}`
851
890
  };
852
891
  return async (request) => {
892
+ const consented = await answerVerifyChallengeRequest(request);
893
+ if (consented !== null) return consented;
853
894
  const sealed = new URL(request.url).searchParams.get(WALLET);
854
895
  if (sealed === null) return Response.json({ settled: false }, { status: 400 });
855
896
  const opened = await unseal(config.secret, sealed);
@@ -873,16 +914,16 @@ async function relayedVerifyUrl(endpoint, wallet, secret) {
873
914
  var LIGHTNING = "lightning";
874
915
  function blindLightningRail(config) {
875
916
  return async (order) => {
876
- const resolved = await invoiceFrom(config.lnAddresses, await config.amountMsat(order));
917
+ const resolved2 = await invoiceFrom(config.lnAddresses, await config.amountMsat(order));
877
918
  const relay = config.relayVerifyThrough;
878
919
  const watched = await config.gateway.watchPayment({
879
- paymentHash: resolved.paymentHash,
920
+ paymentHash: resolved2.paymentHash,
880
921
  verifyUrl: relay ? await relayedVerifyUrl(
881
922
  relay.endpoint,
882
- { url: resolved.verifyUrl, hash: resolved.paymentHash },
923
+ { url: resolved2.verifyUrl, hash: resolved2.paymentHash },
883
924
  relay.secret
884
- ) : resolved.verifyUrl,
885
- expiresAt: resolved.expiresAt,
925
+ ) : resolved2.verifyUrl,
926
+ expiresAt: resolved2.expiresAt,
886
927
  trigger: config.trigger,
887
928
  sealed: await config.sealed?.(order),
888
929
  webhookUrl: config.webhookUrl,
@@ -891,9 +932,9 @@ function blindLightningRail(config) {
891
932
  return {
892
933
  id: watched.id,
893
934
  rail: config.name ?? LIGHTNING,
894
- scan: resolved.bolt11,
895
- qr: encodeForQr(resolved.bolt11),
896
- expiresAt: resolved.expiresAt
935
+ scan: resolved2.bolt11,
936
+ qr: encodeForQr(resolved2.bolt11),
937
+ expiresAt: resolved2.expiresAt
897
938
  };
898
939
  };
899
940
  }
package/dist/server.js CHANGED
@@ -363,20 +363,22 @@ var REDIRECTS = [301, 302, 303, 307, 308];
363
363
  var KEEPS_THE_METHOD = [307, 308];
364
364
  var CREDENTIALS = ["authorization", "cookie", "proxy-authorization"];
365
365
  var BODY_LIMIT_BYTES = 262144;
366
+ var throughFetch = (url, sent, signal) => fetch(url, {
367
+ method: sent.method ?? "GET",
368
+ headers: sent.headers ?? {},
369
+ body: sent.body,
370
+ redirect: "manual",
371
+ signal
372
+ });
373
+ var send = throughFetch;
366
374
  async function ask(url, sent = {}) {
367
375
  const capped = AbortSignal.timeout(HTTP_TIMEOUT_MS);
368
376
  const signal = sent.deadline ? AbortSignal.any([sent.deadline, capped]) : capped;
369
377
  let target = url;
370
378
  let carried = sent;
371
379
  for (let hop = 0; hop <= HOPS_FOLLOWED; hop += 1) {
372
- await refuseUnlessPublic(target);
373
- const response = await fetch(target, {
374
- method: carried.method ?? "GET",
375
- headers: carried.headers ?? {},
376
- body: carried.body,
377
- redirect: "manual",
378
- signal
379
- });
380
+ const at = await addressesToReach(target);
381
+ const response = await send(target, carried, signal, at);
380
382
  if (!REDIRECTS.includes(response.status)) return await answerOf(response);
381
383
  const location = response.headers.get("location");
382
384
  if (location === null) {
@@ -390,7 +392,15 @@ async function ask(url, sent = {}) {
390
392
  }
391
393
  throw new Error(`${url} redirected more than ${HOPS_FOLLOWED} times`);
392
394
  }
393
- async function resolvesNothingPrivate(url) {
395
+ async function addressesToReach(url) {
396
+ if (!publicHttps(url)) throw new Error(`${url} is not a public https URL`);
397
+ const found = await resolved(url);
398
+ if (found.length === 0 || !found.every((one) => publicAddress(one.address))) {
399
+ throw new Error(`${url} resolves to an address we do not reach`);
400
+ }
401
+ return found;
402
+ }
403
+ async function resolved(url) {
394
404
  const { lookup } = await import("dns/promises");
395
405
  const { setTimeout: sleep } = await import("timers/promises");
396
406
  const host = new URL(url).hostname.replace(/^\[|]$/g, "");
@@ -398,11 +408,7 @@ async function resolvesNothingPrivate(url) {
398
408
  lookup(host, { all: true, verbatim: true }).catch(() => []),
399
409
  sleep(LOOKUP_TIMEOUT_MS, null, { ref: false })
400
410
  ]);
401
- return found !== null && found.every((one) => publicAddress(one.address));
402
- }
403
- async function refuseUnlessPublic(url) {
404
- if (!publicHttps(url)) throw new Error(`${url} is not a public https URL`);
405
- if (!await resolvesNothingPrivate(url)) throw new Error(`${url} resolves to an address we do not reach`);
411
+ return found ?? [];
406
412
  }
407
413
  async function answerOf(response) {
408
414
  const { status, ok, headers } = response;
@@ -455,6 +461,7 @@ var NoWalletAvailable = class extends Error {
455
461
  // ../core/lnurl.ts
456
462
  var VERIFY_WITHOUT_PREIMAGE = ["zeuspay.com", "zeusnuts.com", "ecash.love"];
457
463
  var RESOLVE_TIMEOUT_MS = 3e4;
464
+ var NAMES_ONE_ACCOUNT = /^[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)*$/;
458
465
  var MIN_PACE_SECS = 1;
459
466
  var MAX_PACE_SECS = 3600;
460
467
  var MIN_PER_SECOND = 0.01;
@@ -583,11 +590,19 @@ function withAmount(callback, amountMsat) {
583
590
  }
584
591
  function splitAddress(address) {
585
592
  const [user, domain, ...rest] = address.split("@");
586
- if (!user || !domain || rest.length > 0 || !publicHttps(`https://${domain}/`)) {
593
+ if (!user || !domain || rest.length > 0 || !NAMES_ONE_ACCOUNT.test(user) || !isNothingButAHost(domain) || !publicHttps(`https://${domain}/`)) {
587
594
  throw new WalletRefused("address-unusable", `${address} is not a usable lightning address`);
588
595
  }
589
596
  return [user, domain];
590
597
  }
598
+ function isNothingButAHost(domain) {
599
+ try {
600
+ const parsed = new URL(`https://${domain}/`);
601
+ return parsed.host === domain.toLowerCase() && parsed.pathname === "/" && parsed.search === "" && parsed.hash === "";
602
+ } catch {
603
+ return false;
604
+ }
605
+ }
591
606
  async function fetchJson(url, deadline) {
592
607
  return (await answeredJson(url, deadline)).said;
593
608
  }
@@ -760,21 +775,21 @@ async function mintThroughGateway(config, address, amountMsat, nonce) {
760
775
  return { bolt11: payment.bolt11, verifyUrl: payment.verifyUrl };
761
776
  }
762
777
  async function mintBlind(config, address, amountMsat) {
763
- const resolved = await resolve([address], amountMsat);
764
- const minted = { ...resolved, amountMsat, lnAddress: resolved.address };
778
+ const resolved2 = await resolve([address], amountMsat);
779
+ const minted = { ...resolved2, amountMsat, lnAddress: resolved2.address };
765
780
  const locked = config.sealed;
766
781
  try {
767
782
  await config.gateway.watchPayment({
768
- paymentHash: resolved.paymentHash,
769
- verifyUrl: resolved.verifyUrl,
770
- expiresAt: resolved.expiresAt,
783
+ paymentHash: resolved2.paymentHash,
784
+ verifyUrl: resolved2.verifyUrl,
785
+ expiresAt: resolved2.expiresAt,
771
786
  trigger: config.watchSecret,
772
787
  sealed: locked ? await seal(locked.secret, JSON.stringify(locked.data(minted))) : void 0
773
788
  });
774
789
  } catch (refused) {
775
790
  if (!alreadyWatched(refused)) throw refused;
776
791
  }
777
- return { bolt11: resolved.bolt11, verifyUrl: resolved.verifyUrl };
792
+ return { bolt11: resolved2.bolt11, verifyUrl: resolved2.verifyUrl };
778
793
  }
779
794
  function alreadyWatched(refused) {
780
795
  return refused instanceof ProblemError && isProblemType(refused, PAYMENT_ALREADY_WATCHED);
@@ -792,6 +807,30 @@ function refuse(reason) {
792
807
  return Response.json({ status: "ERROR", reason });
793
808
  }
794
809
 
810
+ // ../core/ed25519.ts
811
+ var PKCS8_HEADER = hexToBytes("302e020100300506032b657004220420");
812
+
813
+ // src/webhook.ts
814
+ var VERIFY_CHALLENGE = "verify-challenge";
815
+ function nonceOf(text2, type) {
816
+ try {
817
+ const said = JSON.parse(text2);
818
+ if (said["type"] !== type || typeof said["nonce"] !== "string") return null;
819
+ return said["nonce"];
820
+ } catch {
821
+ return null;
822
+ }
823
+ }
824
+ function answerVerifyChallenge(body) {
825
+ const nonce = nonceOf(body, VERIFY_CHALLENGE);
826
+ return nonce === null ? null : JSON.stringify({ nonce });
827
+ }
828
+ async function answerVerifyChallengeRequest(request) {
829
+ if (request.method !== "POST") return null;
830
+ const answer = answerVerifyChallenge(await request.clone().text());
831
+ return answer === null ? null : new Response(answer, { headers: { "content-type": "application/json" } });
832
+ }
833
+
795
834
  // src/qr.ts
796
835
  import { encode } from "uqr";
797
836
  function encodeForQr(destination) {
@@ -811,6 +850,8 @@ function lightningVerifyEndpoint(config) {
811
850
  "cache-control": `max-age=${config.pollEverySecs ?? DEFAULT_POLL_EVERY_SECS}`
812
851
  };
813
852
  return async (request) => {
853
+ const consented = await answerVerifyChallengeRequest(request);
854
+ if (consented !== null) return consented;
814
855
  const sealed = new URL(request.url).searchParams.get(WALLET);
815
856
  if (sealed === null) return Response.json({ settled: false }, { status: 400 });
816
857
  const opened = await unseal(config.secret, sealed);
@@ -834,16 +875,16 @@ async function relayedVerifyUrl(endpoint, wallet, secret) {
834
875
  var LIGHTNING = "lightning";
835
876
  function blindLightningRail(config) {
836
877
  return async (order) => {
837
- const resolved = await invoiceFrom(config.lnAddresses, await config.amountMsat(order));
878
+ const resolved2 = await invoiceFrom(config.lnAddresses, await config.amountMsat(order));
838
879
  const relay = config.relayVerifyThrough;
839
880
  const watched = await config.gateway.watchPayment({
840
- paymentHash: resolved.paymentHash,
881
+ paymentHash: resolved2.paymentHash,
841
882
  verifyUrl: relay ? await relayedVerifyUrl(
842
883
  relay.endpoint,
843
- { url: resolved.verifyUrl, hash: resolved.paymentHash },
884
+ { url: resolved2.verifyUrl, hash: resolved2.paymentHash },
844
885
  relay.secret
845
- ) : resolved.verifyUrl,
846
- expiresAt: resolved.expiresAt,
886
+ ) : resolved2.verifyUrl,
887
+ expiresAt: resolved2.expiresAt,
847
888
  trigger: config.trigger,
848
889
  sealed: await config.sealed?.(order),
849
890
  webhookUrl: config.webhookUrl,
@@ -852,9 +893,9 @@ function blindLightningRail(config) {
852
893
  return {
853
894
  id: watched.id,
854
895
  rail: config.name ?? LIGHTNING,
855
- scan: resolved.bolt11,
856
- qr: encodeForQr(resolved.bolt11),
857
- expiresAt: resolved.expiresAt
896
+ scan: resolved2.bolt11,
897
+ qr: encodeForQr(resolved2.bolt11),
898
+ expiresAt: resolved2.expiresAt
858
899
  };
859
900
  };
860
901
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thunder-bridge",
3
- "version": "0.8.8",
3
+ "version": "0.8.10",
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",