thunder-bridge 0.8.4 → 0.8.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -112,6 +112,8 @@ them and this table does not repeat them.
112
112
  | `bankTransfer(params)` | register a Czech QR platba as a watched payment. Refuses a gateway that serves strangers |
113
113
  | `bankVerifyEndpoint(config)` | the other half, the LUD-21 shape backed by your own statement |
114
114
  | `fioStatement(config)` | a `Statement` reading a Fio account, several tokens used strictly in turn |
115
+ | `lightningVerifyEndpoint(config)` | the same shape for Lightning, asking the wallet on the gateway's behalf. From `thunder-bridge/server` |
116
+ | `relayedVerifyUrl(mount, wallet, secret)` | the URL to hand the gateway instead of the wallet's, with the wallet's sealed inside |
115
117
 
116
118
  What your service answers once those handlers are mounted is written out in
117
119
  [`openapi.yaml`](openapi.yaml), shipped with this package.
@@ -167,7 +169,8 @@ const tipJar = lnurlToSvg("https://agora.gripe/tip");
167
169
 
168
170
  **Webhooks** - [`src/webhook.ts`](src/webhook.ts): `parseWebhookRequest`,
169
171
  `parseWebhook`, `parseWatchedWebhookRequest`, `parseWatchedWebhook`,
170
- `verifyWebhookSignature`. See [Webhooks](#webhooks).
172
+ `verifyWebhookSignature`, `answerWebhookChallengeRequest`,
173
+ `answerWebhookChallenge`. See [Webhooks](#webhooks).
171
174
 
172
175
  **Errors** - [`src/errors.ts`](src/errors.ts): `ProblemError`,
173
176
  `NoWalletAvailableError`, `GatewayCheatError`, `UnverifiedRecipientError`,
@@ -247,6 +250,53 @@ already in the account:
247
250
  Neither has been seen with Fio, which forwards the message untouched. Check it
248
251
  against the banks your payers actually use before you promise them a rail.
249
252
 
253
+ ## Making the gateway poll nobody but you
254
+
255
+ By default a Lightning watch hands the gateway the wallet's own verify URL, so the
256
+ gateway polls `blink.sv` or `coinos.io` directly and its logs, its ledger and its
257
+ peers all carry that domain. If you would rather it never touched a third party and
258
+ never learned which provider your recipient uses, put your own endpoint in between.
259
+
260
+ ```ts
261
+ import { blindLightningRail, lightningVerifyEndpoint } from "thunder-bridge/server";
262
+
263
+ app.get("/verify/lightning", (context) =>
264
+ lightningVerifyEndpoint({ secret: RELAY_SECRET, pollEverySecs: 5 })(context.req.raw),
265
+ );
266
+
267
+ const rail = blindLightningRail({
268
+ gateway,
269
+ lnAddresses: ["you@blink.sv"],
270
+ amountMsat: (order) => order.amountMinor * 40,
271
+ relayVerifyThrough: { endpoint: "https://shop.example/verify/lightning", secret: RELAY_SECRET },
272
+ });
273
+ ```
274
+
275
+ The wallet's URL is sealed into the query with your secret, so what the gateway
276
+ stores and replicates is a blob it cannot read. It polls you, you ask the wallet,
277
+ and the preimage still comes from the recipient's own server and still has to hash
278
+ to the payment hash, so standing in the middle buys privacy and pacing without
279
+ making you something anyone has to trust. A wallet you cannot reach answers `502`
280
+ rather than "not settled", because those are different claims.
281
+
282
+ Both rails then run through endpoints of yours, on a pace you set, and the gateway
283
+ is only ever talking to servers that asked to be talked to. It costs you a service
284
+ that has to stay up: a browser-only integration cannot do this, and should keep
285
+ letting the gateway poll the wallet.
286
+
287
+ ### How often the gateway asks
288
+
289
+ Your endpoint decides, not the gateway. `bankVerifyEndpoint` answers with
290
+ `Cache-Control: max-age=30`, and the gateway uses that as the interval for every
291
+ payment on your host. Set `pollEverySecs` to whatever your bank's own refresh makes
292
+ sensible: reading a statement that moves once an hour every five seconds only burns
293
+ your rate limit.
294
+
295
+ The gateway also asks the URL once, before it accepts the watch, and refuses with
296
+ `424` if it does not answer this shape. So deploy the endpoint first and register
297
+ second. That is what stops anyone pointing a gateway at a server that never asked to
298
+ be polled for three days.
299
+
250
300
  ## What is still trusted
251
301
 
252
302
  - **The gateway chooses which of your addresses gets paid.** Nothing here can
@@ -339,17 +389,34 @@ try {
339
389
 
340
390
  Pass `webhookUrl` and optionally `webhookSecret` when you create a payment, or on
341
391
  any rail. Once it reaches `paid` the gateway POSTs the same JSON the API returns,
342
- so the body is a `Payment`. Every delivery carries `x-timestamp`, and with a secret set
343
- `x-signature: sha256=<hmac>` over `<timestamp>.<body>` rather than the body alone,
344
- so a captured delivery cannot be replayed at you later. Six attempts on a widening
345
- backoff, then it parks. An invoice that expires fires nothing.
392
+ so the body is a `Payment`. Every delivery carries `x-timestamp` and an
393
+ `x-signature` over `<timestamp>.<body>` rather than the body alone, so a captured
394
+ delivery cannot be replayed at you later. With a secret set it is
395
+ `sha256=<hmac>` keyed with that secret, and without one it is `ed25519=<signature>`
396
+ from the gateway's own key, which is the better default and is below. Retries widen
397
+ until the payment itself runs out, never sooner than an hour. An invoice that expires
398
+ fires nothing.
346
399
 
347
400
  Delivery is at-least-once, so deduplicate on `id`.
348
401
 
402
+ Your handler answers one challenge before any of that. The gateway POSTs
403
+ `{"type":"webhook-challenge","nonce":"..."}` to the URL while the create is still open
404
+ and refuses the payment with a 424 unless the nonce comes back, so the endpoint has to
405
+ be deployed before you register it. `answerWebhookChallengeRequest` verifies that
406
+ challenge and hands you the response to return, or `null` when the delivery was a real
407
+ settlement, and it leaves the body unread either way.
408
+
349
409
  ```ts
350
- import { parseWebhookRequest, proveSettlement } from "thunder-bridge";
410
+ import {
411
+ answerWebhookChallengeRequest,
412
+ parseWebhookRequest,
413
+ proveSettlement,
414
+ } from "thunder-bridge";
351
415
 
352
416
  app.post("/hooks/paid", async (context) => {
417
+ const challenge = await answerWebhookChallengeRequest(context.req.raw, secret);
418
+ if (challenge) return challenge;
419
+
353
420
  const payment = await parseWebhookRequest(context.req.raw, secret);
354
421
  if (payment === null) return context.text("bad signature", 401);
355
422
 
@@ -382,7 +449,38 @@ app.post("/hooks/bank", async (context) => {
382
449
  ```
383
450
 
384
451
  Give each rail its own path, as above, and neither endpoint has to guess which body
385
- it was handed.
452
+ it was handed. Both events also carry `kind`, `"minted"` or `"watched"`, so a single
453
+ path serving a trigger that both rails settle on can branch on the field instead of
454
+ on which fields are missing.
455
+
456
+ ### Or hand the gateway no secret at all
457
+
458
+ A secret you give the gateway is kept in its ledger and replicated to its peers,
459
+ because any instance may be the one that delivers. Leave `webhookSecret` out and the
460
+ delivery is signed with the gateway's own key instead, `x-signature:
461
+ ed25519=<signature>` over the same `<timestamp>.<body>`. Fetch the public half once
462
+ and pass it as `{ publicKey }` wherever a secret would go.
463
+
464
+ ```ts
465
+ const publicKey = await gateway.webhookKey();
466
+
467
+ app.post("/hooks/paid", async (context) => {
468
+ const challenge = await answerWebhookChallengeRequest(context.req.raw, { publicKey });
469
+ if (challenge) return challenge;
470
+
471
+ const payment = await parseWebhookRequest(context.req.raw, { publicKey });
472
+ if (payment === null) return context.text("bad signature", 401);
473
+ ...
474
+ });
475
+ ```
476
+
477
+ Answering echoes the nonce and nothing else here, because there is no secret to sign it
478
+ with. Holding the URL the gateway challenged is the whole proof in that case.
479
+
480
+ The key is derived from the gateway's `CLUSTER_KEY`, so every instance in one cluster
481
+ signs alike and an operator rotating that key changes this one too. Neither
482
+ credential is ever accepted for the other's scheme, so a secret cannot check an
483
+ `ed25519=` delivery and a public key cannot check a `sha256=` one.
386
484
 
387
485
  `parseWebhookRequest` refuses anything more than five minutes out of date,
388
486
  adjustable with `toleranceSecs`. The signature proves the body came from someone
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
+ answerWebhookChallenge: () => answerWebhookChallenge,
34
+ answerWebhookChallengeRequest: () => answerWebhookChallengeRequest,
33
35
  bankRail: () => bankRail,
34
36
  bankTransfer: () => bankTransfer,
35
37
  bankVerifyEndpoint: () => bankVerifyEndpoint,
@@ -632,6 +634,7 @@ function triggerEventFromWire(body) {
632
634
  if (sealed !== null && typeof sealed !== "string") return null;
633
635
  return {
634
636
  id,
637
+ kind: kindOf(wire),
635
638
  paymentHash,
636
639
  verifyUrl,
637
640
  status,
@@ -643,6 +646,11 @@ function triggerEventFromWire(body) {
643
646
  amountMsat: wire["incoming_amount"] === void 0 ? null : msatFrom(wire["incoming_amount"], 1)
644
647
  };
645
648
  }
649
+ function kindOf(wire) {
650
+ const said = wire["kind"];
651
+ if (said === "minted" || said === "watched") return said;
652
+ return text(wire["ln_address"]) === null ? "watched" : "minted";
653
+ }
646
654
  function toAmount(msat) {
647
655
  return { value: String(msat), asset_code: ASSET_CODE, asset_scale: ASSET_SCALE };
648
656
  }
@@ -780,6 +788,23 @@ var ThunderBridge = class {
780
788
  return quote;
781
789
  }
782
790
  /** Read a payment back, null when the gateway has never heard of it */
791
+ /**
792
+ * The key this gateway signs webhooks with when you registered none of your own,
793
+ * ready to hand to `parseWebhookRequest` as `{ publicKey }`. Fetch it once and
794
+ * keep it: it is the same for every instance in the cluster
795
+ */
796
+ async webhookKey() {
797
+ const response = await fetch(`${this.baseUrl}/webhook-key`);
798
+ if (!response.ok) throw await problemFrom(response);
799
+ const body = await response.json().catch(() => null);
800
+ if (body?.algorithm !== "ed25519" || typeof body.public_key !== "string") {
801
+ throw new ProblemError({
802
+ status: response.status,
803
+ title: "The gateway published no ed25519 webhook key"
804
+ });
805
+ }
806
+ return body.public_key;
807
+ }
783
808
  async getPayment(id) {
784
809
  const response = await fetch(`${this.baseUrl}/incoming-payments/${encodeURIComponent(id)}`, {
785
810
  headers: this.reading()
@@ -1265,6 +1290,7 @@ function minorScaleOf(currency) {
1265
1290
  // src/bank.ts
1266
1291
  var DEFAULT_CURRENCY = "CZK";
1267
1292
  var DEFAULT_LOOK_BACK_SECS = 7 * 24 * 60 * 60;
1293
+ var DEFAULT_POLL_EVERY_SECS = 30;
1268
1294
  var IBAN = /^[A-Z]{2}[0-9]{2}[0-9A-Z]{8,30}$/;
1269
1295
  var FORBIDDEN_IN_SPD = "*";
1270
1296
  async function bankTransfer(params) {
@@ -1291,6 +1317,9 @@ async function bankTransfer(params) {
1291
1317
  return { id: watched.id, paymentHash: watched.paymentHash, verifyUrl, spd };
1292
1318
  }
1293
1319
  function bankVerifyEndpoint(config) {
1320
+ const paced = {
1321
+ "cache-control": `max-age=${config.pollEverySecs ?? DEFAULT_POLL_EVERY_SECS}`
1322
+ };
1294
1323
  return async (request) => {
1295
1324
  const asked = readQuery(new URL(request.url));
1296
1325
  if (asked === null) return Response.json({ settled: false }, { status: 400 });
@@ -1301,11 +1330,11 @@ function bankVerifyEndpoint(config) {
1301
1330
  }
1302
1331
  const since = unixNow() - (config.lookBackSecs ?? DEFAULT_LOOK_BACK_SECS);
1303
1332
  const landed = (await config.statement(since)).some((credit) => pays(credit, asked));
1304
- if (!landed) return Response.json({ settled: false });
1305
- return Response.json({
1306
- settled: true,
1307
- preimage: await hmacHex(config.secret, `preimage|${subject}`)
1308
- });
1333
+ if (!landed) return Response.json({ settled: false }, { headers: paced });
1334
+ return Response.json(
1335
+ { settled: true, preimage: await hmacHex(config.secret, `preimage|${subject}`) },
1336
+ { headers: paced }
1337
+ );
1309
1338
  };
1310
1339
  }
1311
1340
  function readQuery(url) {
@@ -1763,18 +1792,59 @@ function refusals2(answers) {
1763
1792
  ).join("; ");
1764
1793
  }
1765
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
+
1766
1825
  // src/webhook.ts
1767
1826
  var SIGNATURE_HEADER = "x-signature";
1768
1827
  var TIMESTAMP_HEADER = "x-timestamp";
1769
- var SIGNATURE_PREFIX = "sha256=";
1828
+ var SHARED_SECRET_PREFIX = "sha256=";
1829
+ var GATEWAY_KEY_PREFIX = "ed25519=";
1770
1830
  var DEFAULT_TOLERANCE_SECS = 300;
1771
- async function verifyWebhookSignature(body, signature, secret, timestamp, options = {}) {
1831
+ var CHALLENGE = "webhook-challenge";
1832
+ async function verifyWebhookSignature(body, signature, credential, timestamp, options = {}) {
1772
1833
  if (!recent(timestamp, options.toleranceSecs ?? DEFAULT_TOLERANCE_SECS)) return false;
1773
- const received = signature.startsWith(SIGNATURE_PREFIX) ? signature.slice(SIGNATURE_PREFIX.length) : signature;
1774
- return equalInConstantTime(await sign(secret, timestamp, body), received.toLowerCase());
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());
1775
1845
  }
1776
- async function parseWebhook(body, signature, secret, timestamp, options = {}) {
1777
- if (!await verifyWebhookSignature(body, signature, secret, timestamp, options)) return null;
1846
+ async function parseWebhook(body, signature, credential, timestamp, options = {}) {
1847
+ if (!await verifyWebhookSignature(body, signature, credential, timestamp, options)) return null;
1778
1848
  const text2 = typeof body === "string" ? body : new TextDecoder().decode(body);
1779
1849
  try {
1780
1850
  return paymentFromWire(JSON.parse(text2));
@@ -1782,14 +1852,14 @@ async function parseWebhook(body, signature, secret, timestamp, options = {}) {
1782
1852
  return null;
1783
1853
  }
1784
1854
  }
1785
- async function parseWebhookRequest(request, secret, options = {}) {
1855
+ async function parseWebhookRequest(request, credential, options = {}) {
1786
1856
  const signature = request.headers.get(SIGNATURE_HEADER);
1787
1857
  const timestamp = request.headers.get(TIMESTAMP_HEADER);
1788
1858
  if (signature === null || timestamp === null) return null;
1789
- return parseWebhook(await request.text(), signature, secret, timestamp, options);
1859
+ return parseWebhook(await request.text(), signature, credential, timestamp, options);
1790
1860
  }
1791
- async function parseWatchedWebhook(body, signature, secret, timestamp, options = {}) {
1792
- if (!await verifyWebhookSignature(body, signature, secret, timestamp, options)) return null;
1861
+ async function parseWatchedWebhook(body, signature, credential, timestamp, options = {}) {
1862
+ if (!await verifyWebhookSignature(body, signature, credential, timestamp, options)) return null;
1793
1863
  const text2 = typeof body === "string" ? body : new TextDecoder().decode(body);
1794
1864
  try {
1795
1865
  return triggerEventFromWire(JSON.parse(text2));
@@ -1797,11 +1867,44 @@ async function parseWatchedWebhook(body, signature, secret, timestamp, options =
1797
1867
  return null;
1798
1868
  }
1799
1869
  }
1800
- async function parseWatchedWebhookRequest(request, secret, options = {}) {
1870
+ async function parseWatchedWebhookRequest(request, credential, options = {}) {
1801
1871
  const signature = request.headers.get(SIGNATURE_HEADER);
1802
1872
  const timestamp = request.headers.get(TIMESTAMP_HEADER);
1803
1873
  if (signature === null || timestamp === null) return null;
1804
- return parseWatchedWebhook(await request.text(), signature, secret, timestamp, options);
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
+ }
1805
1908
  }
1806
1909
  function recent(timestamp, toleranceSecs) {
1807
1910
  const sent = Number(timestamp);
@@ -1832,6 +1935,8 @@ function signed(timestamp, body) {
1832
1935
  REQUEST_IN_FLIGHT,
1833
1936
  ThunderBridge,
1834
1937
  UnverifiedRecipientError,
1938
+ answerWebhookChallenge,
1939
+ answerWebhookChallengeRequest,
1835
1940
  bankRail,
1836
1941
  bankTransfer,
1837
1942
  bankVerifyEndpoint,
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { T as ThunderBridge, P as Payment, C as CreatePaymentParams, a as TriggerEvent, W as WalletFailure } from './rail-QZtUN1D-.cjs';
2
- export { B as BankRailConfig, b as CreateOptions, c as CreateQuoteParams, F as FollowOptions, L as Leg, d as LightningRailConfig, O as Order, e as PaymentStatus, Q as Quote, R as Rail, f as ThunderBridgeOptions, g as WaitOptions, h as WalletReason, i as WatchPaymentParams, j as bankRail, l as lightningRail } from './rail-QZtUN1D-.cjs';
1
+ import { T as ThunderBridge, P as Payment, C as CreatePaymentParams, a as TriggerEvent, W as WalletFailure } from './rail-J8QoYbSr.cjs';
2
+ export { B as BankRailConfig, b as CreateOptions, c as CreateQuoteParams, F as FollowOptions, L as Leg, d as LightningRailConfig, O as Order, e as PaymentKind, f as PaymentStatus, Q as Quote, R as Rail, g as ThunderBridgeOptions, h as WaitOptions, i as WalletReason, j as WatchPaymentParams, k as bankRail, l as lightningRail } from './rail-J8QoYbSr.cjs';
3
3
 
4
4
  /**
5
5
  * Encrypt what the watcher needs and the gateway must not have. The gateway
@@ -98,6 +98,13 @@ interface BankVerifyConfig {
98
98
  statement: Statement;
99
99
  /** How far back a credit still counts, seven days by default */
100
100
  lookBackSecs?: number;
101
+ /**
102
+ * How often you want the gateway to ask, in seconds. It goes out as
103
+ * `Cache-Control: max-age`, so the pace is yours to set rather than the
104
+ * gateway's, and a bank that updates once a minute should say so instead of
105
+ * being polled every few seconds. Thirty by default, clamped to an hour
106
+ */
107
+ pollEverySecs?: number;
101
108
  }
102
109
  /**
103
110
  * Ask for a bank transfer and put it under the gateway's watch, so it settles
@@ -338,23 +345,42 @@ declare function toLnurl(endpoint: string): string;
338
345
  type WebhookOptions = {
339
346
  toleranceSecs?: number;
340
347
  };
348
+ /**
349
+ * What checks a delivery. A string is the `webhook.secret` you registered. Pass
350
+ * `{ publicKey }` instead, the hex from the gateway's `/webhook-key`, when you
351
+ * registered no secret and would rather it held nothing of yours
352
+ */
353
+ type WebhookCredential = string | {
354
+ publicKey: string;
355
+ };
341
356
  /** Verify the `X-Signature` header against the raw body and the `X-Timestamp` that came with it */
342
- declare function verifyWebhookSignature(body: string | Uint8Array, signature: string, secret: string, timestamp: string, options?: WebhookOptions): Promise<boolean>;
357
+ declare function verifyWebhookSignature(body: string | Uint8Array, signature: string, credential: WebhookCredential, timestamp: string, options?: WebhookOptions): Promise<boolean>;
343
358
  /** Verify and parse in one step, returns null on a bad signature or a body that is not a payment */
344
- declare function parseWebhook(body: string | Uint8Array, signature: string, secret: string, timestamp: string, options?: WebhookOptions): Promise<Payment | null>;
359
+ declare function parseWebhook(body: string | Uint8Array, signature: string, credential: WebhookCredential, timestamp: string, options?: WebhookOptions): Promise<Payment | null>;
345
360
  /**
346
361
  * Verify and parse from a Fetch API `Request` as used by Hono, Next, SvelteKit,
347
362
  * Cloudflare Workers and Deno, WebCrypto only so it runs anywhere fetch does
348
363
  */
349
- declare function parseWebhookRequest(request: Request, secret: string, options?: WebhookOptions): Promise<Payment | null>;
364
+ declare function parseWebhookRequest(request: Request, credential: WebhookCredential, options?: WebhookOptions): Promise<Payment | null>;
350
365
  /**
351
366
  * The same, for a payment the gateway only watched. A bank transfer and a blind
352
367
  * Lightning leg carry no address, amount or invoice, so they arrive in the shape
353
368
  * `followTrigger` and `getWatched` hand back rather than the minted one
354
369
  */
355
- declare function parseWatchedWebhook(body: string | Uint8Array, signature: string, secret: string, timestamp: string, options?: WebhookOptions): Promise<TriggerEvent | null>;
370
+ declare function parseWatchedWebhook(body: string | Uint8Array, signature: string, credential: WebhookCredential, timestamp: string, options?: WebhookOptions): Promise<TriggerEvent | null>;
356
371
  /** `parseWatchedWebhook` from a Fetch API `Request`, the way `parseWebhookRequest` is */
357
- declare function parseWatchedWebhookRequest(request: Request, secret: string, options?: WebhookOptions): Promise<TriggerEvent | null>;
372
+ declare function parseWatchedWebhookRequest(request: Request, credential: WebhookCredential, options?: WebhookOptions): Promise<TriggerEvent | null>;
373
+ /**
374
+ * Answer the one challenge the gateway sends before it will watch a payment your
375
+ * webhook is registered on. Returns the body to send back with a 200, or null when
376
+ * this delivery is not a challenge, so a handler tries this first and then parses
377
+ */
378
+ declare function answerWebhookChallenge(body: string | Uint8Array, signature: string, credential: WebhookCredential, timestamp: string, options?: WebhookOptions): Promise<string | null>;
379
+ /**
380
+ * `answerWebhookChallenge` from a Fetch API `Request`, leaving the body unread so
381
+ * the same handler can go on to `parseWebhookRequest` when this was no challenge
382
+ */
383
+ declare function answerWebhookChallengeRequest(request: Request, credential: WebhookCredential, options?: WebhookOptions): Promise<Response | null>;
358
384
 
359
385
  /**
360
386
  * The way a gateway was caught out, every code is a check that held against the
@@ -432,4 +458,4 @@ declare class NoWalletAvailableError extends ProblemError {
432
458
  }, wallets: WalletFailure[]);
433
459
  }
434
460
 
435
- 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 WebhookOptions, 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 };
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 };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { T as ThunderBridge, P as Payment, C as CreatePaymentParams, a as TriggerEvent, W as WalletFailure } from './rail-QZtUN1D-.js';
2
- export { B as BankRailConfig, b as CreateOptions, c as CreateQuoteParams, F as FollowOptions, L as Leg, d as LightningRailConfig, O as Order, e as PaymentStatus, Q as Quote, R as Rail, f as ThunderBridgeOptions, g as WaitOptions, h as WalletReason, i as WatchPaymentParams, j as bankRail, l as lightningRail } from './rail-QZtUN1D-.js';
1
+ import { T as ThunderBridge, P as Payment, C as CreatePaymentParams, a as TriggerEvent, W as WalletFailure } from './rail-J8QoYbSr.js';
2
+ export { B as BankRailConfig, b as CreateOptions, c as CreateQuoteParams, F as FollowOptions, L as Leg, d as LightningRailConfig, O as Order, e as PaymentKind, f as PaymentStatus, Q as Quote, R as Rail, g as ThunderBridgeOptions, h as WaitOptions, i as WalletReason, j as WatchPaymentParams, k as bankRail, l as lightningRail } from './rail-J8QoYbSr.js';
3
3
 
4
4
  /**
5
5
  * Encrypt what the watcher needs and the gateway must not have. The gateway
@@ -98,6 +98,13 @@ interface BankVerifyConfig {
98
98
  statement: Statement;
99
99
  /** How far back a credit still counts, seven days by default */
100
100
  lookBackSecs?: number;
101
+ /**
102
+ * How often you want the gateway to ask, in seconds. It goes out as
103
+ * `Cache-Control: max-age`, so the pace is yours to set rather than the
104
+ * gateway's, and a bank that updates once a minute should say so instead of
105
+ * being polled every few seconds. Thirty by default, clamped to an hour
106
+ */
107
+ pollEverySecs?: number;
101
108
  }
102
109
  /**
103
110
  * Ask for a bank transfer and put it under the gateway's watch, so it settles
@@ -338,23 +345,42 @@ declare function toLnurl(endpoint: string): string;
338
345
  type WebhookOptions = {
339
346
  toleranceSecs?: number;
340
347
  };
348
+ /**
349
+ * What checks a delivery. A string is the `webhook.secret` you registered. Pass
350
+ * `{ publicKey }` instead, the hex from the gateway's `/webhook-key`, when you
351
+ * registered no secret and would rather it held nothing of yours
352
+ */
353
+ type WebhookCredential = string | {
354
+ publicKey: string;
355
+ };
341
356
  /** Verify the `X-Signature` header against the raw body and the `X-Timestamp` that came with it */
342
- declare function verifyWebhookSignature(body: string | Uint8Array, signature: string, secret: string, timestamp: string, options?: WebhookOptions): Promise<boolean>;
357
+ declare function verifyWebhookSignature(body: string | Uint8Array, signature: string, credential: WebhookCredential, timestamp: string, options?: WebhookOptions): Promise<boolean>;
343
358
  /** Verify and parse in one step, returns null on a bad signature or a body that is not a payment */
344
- declare function parseWebhook(body: string | Uint8Array, signature: string, secret: string, timestamp: string, options?: WebhookOptions): Promise<Payment | null>;
359
+ declare function parseWebhook(body: string | Uint8Array, signature: string, credential: WebhookCredential, timestamp: string, options?: WebhookOptions): Promise<Payment | null>;
345
360
  /**
346
361
  * Verify and parse from a Fetch API `Request` as used by Hono, Next, SvelteKit,
347
362
  * Cloudflare Workers and Deno, WebCrypto only so it runs anywhere fetch does
348
363
  */
349
- declare function parseWebhookRequest(request: Request, secret: string, options?: WebhookOptions): Promise<Payment | null>;
364
+ declare function parseWebhookRequest(request: Request, credential: WebhookCredential, options?: WebhookOptions): Promise<Payment | null>;
350
365
  /**
351
366
  * The same, for a payment the gateway only watched. A bank transfer and a blind
352
367
  * Lightning leg carry no address, amount or invoice, so they arrive in the shape
353
368
  * `followTrigger` and `getWatched` hand back rather than the minted one
354
369
  */
355
- declare function parseWatchedWebhook(body: string | Uint8Array, signature: string, secret: string, timestamp: string, options?: WebhookOptions): Promise<TriggerEvent | null>;
370
+ declare function parseWatchedWebhook(body: string | Uint8Array, signature: string, credential: WebhookCredential, timestamp: string, options?: WebhookOptions): Promise<TriggerEvent | null>;
356
371
  /** `parseWatchedWebhook` from a Fetch API `Request`, the way `parseWebhookRequest` is */
357
- declare function parseWatchedWebhookRequest(request: Request, secret: string, options?: WebhookOptions): Promise<TriggerEvent | null>;
372
+ declare function parseWatchedWebhookRequest(request: Request, credential: WebhookCredential, options?: WebhookOptions): Promise<TriggerEvent | null>;
373
+ /**
374
+ * Answer the one challenge the gateway sends before it will watch a payment your
375
+ * webhook is registered on. Returns the body to send back with a 200, or null when
376
+ * this delivery is not a challenge, so a handler tries this first and then parses
377
+ */
378
+ declare function answerWebhookChallenge(body: string | Uint8Array, signature: string, credential: WebhookCredential, timestamp: string, options?: WebhookOptions): Promise<string | null>;
379
+ /**
380
+ * `answerWebhookChallenge` from a Fetch API `Request`, leaving the body unread so
381
+ * the same handler can go on to `parseWebhookRequest` when this was no challenge
382
+ */
383
+ declare function answerWebhookChallengeRequest(request: Request, credential: WebhookCredential, options?: WebhookOptions): Promise<Response | null>;
358
384
 
359
385
  /**
360
386
  * The way a gateway was caught out, every code is a check that held against the
@@ -432,4 +458,4 @@ declare class NoWalletAvailableError extends ProblemError {
432
458
  }, wallets: WalletFailure[]);
433
459
  }
434
460
 
435
- 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 WebhookOptions, 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 };
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 };