thunder-bridge 1.4.2 → 2.1.1

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
@@ -34,7 +34,7 @@ so the same wallet on another domain can answer differently.
34
34
  A refusal happens at creation rather than leaving a payment pending until a
35
35
  watcher gives up, so a recipient finds out before a payer sees a QR code.
36
36
 
37
- If your recipient is on a refused name, `nwcRail` is the way round it: your own
37
+ If your recipient is on a refused name, `nwcRail` from `thunder-bridge/nwc` is the way round it: your own
38
38
  wallet answers over NIP-47 instead of over an address, and the gateway watches the
39
39
  hash exactly the same. [docs/lud21-coverage.md](../docs/lud21-coverage.md) is the
40
40
  measured list rather than a reading of changelogs, last surveyed 2026-08-12. Read
@@ -47,23 +47,27 @@ as refused here until the next survey says otherwise.
47
47
  npm install thunder-bridge
48
48
  ```
49
49
 
50
- Node 22 or newer, for anything that opens a socket: `waitForPayment`,
51
- `waitForWatched`, `firstToSettle` and `followTrigger` on the gateway side, and every
52
- NWC call on the wallet side, since a nostr relay is a socket too. Node only exposes
53
- a global `WebSocket` from 22 onwards and there is no fallback to install. The rest,
54
- which is every call that is one or more `fetch` requests, runs on any runtime with
55
- `fetch` and `crypto.subtle`.
50
+ Node 22 or newer, for anything that opens a socket: `requestPayment`, `settled`,
51
+ `firstSettled` and `follow` on the gateway side, and every NWC call on the wallet
52
+ side, since a nostr relay is a socket too. Node only exposes a global `WebSocket`
53
+ from 22 onwards and there is no fallback to install. The rest, which is every call
54
+ that is one or more `fetch` requests, runs on any runtime with `fetch` and
55
+ `crypto.subtle`.
56
56
 
57
- The package has two entry points.
57
+ One import is the whole thing. The other four are for what a checkout page has no
58
+ reason to download.
58
59
 
59
- | Import | Needs | Has |
60
- |---|---|---|
61
- | `thunder-bridge` | `fetch`, `crypto.subtle`, `URL`, `WebSocket` | everything except the server-only exports |
62
- | `thunder-bridge/server` | `node:dns` as well | `invoiceFrom`, `askWallet`, `lnurlPayEndpoint`, the ticket handlers, `lightningVerifyEndpoint`, `nwcVerifyEndpoint`, and the blind and NWC rails |
60
+ | Import | What it is for |
61
+ |---|---|
62
+ | `thunder-bridge` | the gateway, the proofs, the errors and the amounts. Everything is reached through one instance |
63
+ | `thunder-bridge/qr` | a payload as an SVG or a data URL, for a page that draws a QR of its own |
64
+ | `thunder-bridge/price` | the exchange venues behind `fiat`, for pricing off your own book instead |
65
+ | `thunder-bridge/bank` | a bank statement reader, currently Fio |
66
+ | `thunder-bridge/nwc` | your own wallet over NIP-47, which carries the nostr crypto no browser wants |
63
67
 
64
- The second one resolves lightning addresses and refuses a private host, so it needs
65
- DNS and will not run on Cloudflare Workers. `bankVerifyEndpoint` is on the main
66
- entry rather than there, because a bank statement needs no DNS.
68
+ `invoiceFrom` resolves lightning addresses and refuses a private host, so it needs
69
+ `node:dns` and will not run on Cloudflare Workers. Nothing else on the main import
70
+ does.
67
71
 
68
72
  ## Quick start
69
73
 
@@ -74,49 +78,52 @@ gives a `404` and [`/health`](https://public.thunder-bridge.agora.gripe/health)
74
78
  what tells you it is up.
75
79
 
76
80
  ```ts
77
- import {
78
- ThunderBridge,
79
- invoiceToSvg,
80
- proveSettlement,
81
- type CreatePaymentParams,
82
- } from "thunder-bridge";
81
+ import { sats, ThunderBridge } from "thunder-bridge";
83
82
 
84
83
  const gateway = new ThunderBridge("https://public.thunder-bridge.agora.gripe");
85
84
 
86
- const request: CreatePaymentParams = {
87
- lnAddresses: ["iamfatik@blink.sv", "iamfatik@coinos.io"],
88
- amountMsat: 21_000,
89
- };
90
-
91
- const payment = await gateway.createPayment(request);
85
+ const asked = await gateway.requestPayment({
86
+ paidTo: ["iamfatik@blink.sv", "iamfatik@coinos.io"],
87
+ amount: sats(21),
88
+ signal: AbortSignal.timeout(600_000),
89
+ });
92
90
 
93
91
  const target = document.querySelector("#qr");
94
92
  if (target !== null) {
95
- target.innerHTML = invoiceToSvg(payment.bolt11);
93
+ target.innerHTML = asked.qr;
96
94
  }
97
95
 
98
- const settled = await gateway.waitForPayment(payment.id, {
99
- signal: AbortSignal.timeout(600_000),
100
- });
96
+ await asked.paid();
101
97
 
102
- const preimage = settled.status === "paid" ? await proveSettlement(settled, request) : null;
98
+ const preimage = await asked.prove();
103
99
  ```
104
100
 
105
- Keep the `request` object. Every proof that asks the recipient takes it, because
106
- what you asked for is the side of each comparison the gateway did not supply.
107
- `proveWrapped` is the exception, since it compares two invoices and asks nobody. `waitForPayment` tells you what
108
- the gateway says. `proveSettlement` goes to the recipient's own server. Only the
109
- second is evidence the money arrived, and
101
+ Two names and one call. `requestPayment` mints the invoice on the first address that can
102
+ prove one, checks that invoice against the recipient's own domain before returning,
103
+ and draws the QR.
104
+
105
+ Then there are two different questions and both are worth asking. `paid` tells you
106
+ what the gateway says, and it is the fast one. `prove` asks the recipient's own
107
+ server, and only that is evidence the money arrived.
110
108
  [docs/proving-a-payment.md](../docs/proving-a-payment.md) is the whole argument for
111
109
  why.
112
110
 
111
+ An amount is `sats(21)`, `msat(21_000)` or `fiat("4.99", "USD")`, and a bare number
112
+ does not compile: the type is branded, so `21` cannot pass for a price and quietly
113
+ mean twenty-one thousandths of a satoshi. A fiat price is converted when the invoice
114
+ is minted, off the median of four MiCA authorised venues unless you pass your own,
115
+ and a decimal string is read digit by digit rather than through a float. Every
116
+ refusal is an `AmountError` carrying a `code`, and `AmountError.is(error)` is how
117
+ you recognise one: each entry point bundles its own copy of the class, so
118
+ `instanceof` holds within one import and that static holds across all of them.
119
+
113
120
  ## How each payment method gets verified
114
121
 
115
122
  Every rail ends the same way, with a preimage that has to hash to the payment hash
116
123
  the gateway was given. What differs is who obtains the invoice, who is asked for the
117
124
  preimage, and which side does the checking.
118
125
 
119
- | `lightningRail` | the gateway asks, at the recipient's LNURL callback |
126
+ | `rails.lightning` | the gateway asks, at the recipient's LNURL callback |
120
127
  |---|---|
121
128
  | the gateway is told | the address list and the amount, and nothing else. It derives the hash and the `verify` URL by resolving the address, and hands both back |
122
129
  | the invoice is checked by | you, `proveOrigin` runs five checks against the recipient's own domain |
@@ -125,12 +132,12 @@ preimage, and which side does the checking.
125
132
  | `settled` comes from | the wallet releasing its preimage |
126
133
  | the pace is set by | the wallet, when it sends `Cache-Control: max-age`. When it sends none the gateway's own schedule decides |
127
134
 
128
- | `blindLightningRail` | you ask, with `invoiceFrom` on your server |
135
+ | `rails.blindLightning` | you ask, with `invoiceFrom` on your server |
129
136
  |---|---|
130
137
  | the gateway is told | a hash, an expiry and your URL, with the wallet's sealed inside |
131
138
  | the invoice is checked by | nobody needs to, you resolved the address yourself |
132
139
  | the gateway probes first | `speaksVerify`: a GET on the URL, then a signed POST nonce it must echo |
133
- | the gateway polls | your `lightningVerifyEndpoint`, once `relayVerifyThrough` is set. Leave it off and the gateway polls the wallet directly, as on the minted rail |
140
+ | the gateway polls | your `serve.verify` endpoint, once `relayThrough` is set. Leave it off and the gateway polls the wallet directly, as on the minted rail |
134
141
  | `settled` comes from | your endpoint, which unseals, asks the wallet and relays the answer |
135
142
  | the pace is set by | you, `pollEverySecs` |
136
143
 
@@ -143,12 +150,12 @@ preimage, and which side does the checking.
143
150
  | `settled` comes from | `lookup_invoice`, refused unless the wallet's own key signed it |
144
151
  | the pace is set by | you, `pollEverySecs` |
145
152
 
146
- | `bankRail` | nobody, there is no invoice |
153
+ | `rails.bank` | nobody, there is no invoice |
147
154
  |---|---|
148
155
  | the gateway is told | a hash and your URL, which names the amount and the reference |
149
156
  | the invoice is checked by | nobody, there is no invoice to check |
150
157
  | the gateway probes first | the same GET and signed nonce |
151
- | the gateway polls | your `bankVerifyEndpoint` |
158
+ | the gateway polls | your `serve.bankVerify` endpoint |
152
159
  | `settled` comes from | a `Statement` credit matching amount and currency exactly, with the reference anywhere in the payer's text |
153
160
  | the pace is set by | you, `pollEverySecs` |
154
161
 
@@ -160,7 +167,7 @@ the whole defence. A `webhookUrl` is challenged on both paths. On every watched
160
167
  and challenges it with a nonce before accepting the watch, refusing with `424` if
161
168
  nothing answers. Deploy the endpoint before you register it. The challenge is on
162
169
  unless the operator set `VERIFY_CHALLENGE=0`, which is also why a bare wallet
163
- `verify` URL cannot be handed to `watchPayment`: a wallet will not echo a nonce.
170
+ `verify` URL cannot be handed to `watch`: a wallet will not echo a nonce.
164
171
 
165
172
  **What a preimage proves is the same on all four, and narrower than it looks:** that
166
173
  the server holding the secret says the money arrived, made unforgeable by anyone
@@ -175,7 +182,7 @@ without saying whether either invoice was paid.
175
182
 
176
183
  ### What each one costs you
177
184
 
178
- **`lightningRail`**
185
+ **`rails.lightning`**
179
186
 
180
187
  - the gateway holds the address and the amount, so your order book is readable
181
188
  from its own logs
@@ -184,7 +191,7 @@ without saying whether either invoice was paid.
184
191
  - the wallet's `Cache-Control` sets the poll pace, so how fast a settlement is
185
192
  noticed is not yours to decide
186
193
 
187
- **`blindLightningRail`**
194
+ **`rails.blindLightning`**
188
195
 
189
196
  - a service of your own that has to stay up, so a browser-only integration cannot
190
197
  use this rail at all
@@ -204,7 +211,7 @@ without saying whether either invoice was paid.
204
211
  the wallet
205
212
  - relays unreachable means no verification
206
213
 
207
- **`bankRail`**
214
+ **`rails.bank`**
208
215
 
209
216
  - the gateway has to be one of your own: the verify URL names the amount and the
210
217
  reference, so whoever runs the gateway reads your order book from the watches
@@ -245,63 +252,74 @@ Four sharp edges, worth reading before you build:
245
252
  metadata and answers the verify requests. Every check passes. This protects a
246
253
  payer against the operator, never against the recipient's own custodian.
247
254
  - **The two proof fetches vet the first hop and no further.** `proveOrigin` and
248
- `proveSettlement` use the runtime's default redirect handling, so a public https
249
- host answering `302` to a private address is followed there. `invoiceFrom` is not
250
- like this: it resolves through the outbound guard, which sets `redirect: "manual"`
251
- and re-vets every hop. Keep egress control outside this package if that matters.
252
- - **A payment read cold is only as pinned as its creation.** `getPayment` checks
253
- the preimage against the `paymentHash` in the same record, and it was
254
- `proveOrigin` at creation, against the request you wrote, that tied that hash to
255
- an invoice the recipient issued. Store the request alongside the payment id, or a
256
- cold read is checking the gateway's numbers against each other and nothing more.
255
+ `proveSettlement` use the runtime's redirect handling, so a public https host
256
+ answering `302` to a private address on its own origin is followed there, while a
257
+ redirect off the recipient's origin fails the proof. `invoiceFrom` is not like
258
+ this: it resolves through the outbound guard, which sets `redirect: "manual"` and
259
+ re-vets every hop. Keep egress control outside this package if that matters.
260
+ - **A payment read cold is only as pinned as its creation.** `payment` checks
261
+ the report against the `paymentHash` you hand it, and it was `proveOrigin` at
262
+ creation, against the request you wrote, that tied that hash to an invoice the
263
+ recipient issued. Store the hash you proved alongside the payment id, and never a
264
+ hash a later read handed back, or a cold read is checking the gateway's numbers
265
+ against each other and nothing more.
257
266
  - **Availability is not provable, and an address is not a person.** Every check
258
267
  here is about an invoice you were given, none about one you were refused, and
259
268
  proving an invoice belongs to an address never proves the address belongs to
260
269
  whoever you think it does.
261
270
 
262
- `isProvablyPaid` is the one to be careful with: it asks only whether the gateway's
263
- own report holds together, so a gateway that generates a preimage, hashes it and
264
- builds an invoice around that hash passes it. If a payment matters, ask the
265
- recipient with `proveSettlement`. The full argument, including the five origin
266
- checks and their failure codes, is in
271
+ `carriesProof` is the one to be careful with: it asks only whether a report holds
272
+ together, so a gateway that generates a preimage, hashes it and builds an invoice
273
+ around that hash passes it. If a payment matters, ask the recipient with
274
+ `prove` on the payment request or with `proveSettlement`. The full argument, including the five
275
+ origin checks and their failure codes, is in
267
276
  [docs/proving-a-payment.md](../docs/proving-a-payment.md).
268
277
 
269
278
  ## What you call
270
279
 
271
- Signatures and the caveats on each export are in the TSDoc on the export itself, so
272
- your editor has them and this table does not repeat them.
280
+ [docs/api.md](../docs/api.md) is the whole surface, generated from the TSDoc on
281
+ every export by [`tools/api-reference.ts`](../tools/api-reference.ts) and checked in
282
+ CI, so nothing there can be out of date and nothing here repeats it. Your editor
283
+ has the same text on the export itself.
273
284
 
274
- | Export | What it does |
275
- |---|---|
276
- | `new ThunderBridge(baseUrl, options?)` | a gateway handle. `{ secret }` signs the calls that create something, so a payment you create comes back to you and nobody else, while the reads and `webhookKey` stay unsigned, `{ token }` makes the instance yours, `{ verify: false }` turns off the automatic proof |
277
- | `gateway.createPayment(params, options?)` | mint an invoice on the first address that can prove one, and prove it before returning |
278
- | `gateway.watchPayment(params)` | hand over an invoice you obtained yourself, so the gateway never learns the address or the amount |
279
- | `gateway.waitForPayment(id, options?)` | follow one payment over WebSocket until it is paid or expired. `waitForWatched` is the same for a watched one |
280
- | `gateway.followTrigger(secret, options)` | stream every payment carrying one trigger, reconnecting on its own. The secret is the only thing guarding that stream and nothing rate limits a guess, so it is refused under 16 characters |
281
- | `proveSettlement(payment, request)` | ask the recipient whether it settled, returns the preimage or `null` |
282
- | `invoiceFrom(lnAddresses, amountMsat)` | get a provable invoice yourself, from `thunder-bridge/server` |
283
- | `lnurlPayEndpoint(config)` | a whole LNURL-pay endpoint as one Fetch handler, so a static QR points at your own domain. From `thunder-bridge/server` |
284
- | `invoiceToSvg(bolt11, options?)` | a QR as a string, no canvas involved. `lnurlToSvg` and `spdToSvg` are the siblings, each with a `…ToDataUrl` twin |
285
-
286
- The rest of the surface, by job:
287
-
288
- - **more gateway calls:** `Gateways`, `createQuote`, `getPayment`, `getWatched`,
289
- `listPayments`, `firstToSettle`, `nameFor`, `createSocketTicket`, `webhookKey`,
290
- `isPrivate`
291
- - **proving:** `proveOrigin`, `isProvablyPaid`, `preimageMatchesHash`,
292
- `decodeInvoice`, `proveWrapped`, `wrapFeeCeiling`
293
- - **serving your own endpoints:** `lightningVerifyEndpoint`, `bankVerifyEndpoint`,
294
- `nwcVerifyEndpoint`, `watchTicketEndpoint`, `publicWatchTicketEndpoint`,
295
- `relayedVerifyUrl`, `seal`, `unseal`, `toLnurl`
296
- - **one shape per payment method:** `lightningRail`, `blindLightningRail`,
297
- `bankRail`, `nwcRail`, `bankTransfer`, `fioStatement`
298
- - **pricing a fiat order:** `medianOf`, `msatFor`, `coinbase`, `kraken`, `bitstamp`,
299
- `coinmate`, `minorUnitsOf`, `minorScaleOf`
300
- - **webhooks:** `parseSettlementRequest`, `answerWebhookChallengeRequest`,
301
- `isProvablySettled`, `verifyWebhookSignature`, and the `parse*` variants for each
302
- shape
303
- - **errors:** `ProblemError`, `GatewayCheatError`, `NoWalletAvailableError`,
304
- `UnverifiedRecipientError`, `IdempotencyConflictError`, `isProblemType`
285
+ The shape of it is worth stating once, because it is the thing that makes the rest
286
+ findable. One instance is the entry to everything:
287
+
288
+ ```ts
289
+ import { sats, ThunderBridge } from "thunder-bridge";
290
+
291
+ const gateway = new ThunderBridge("https://public.thunder-bridge.agora.gripe", {
292
+ secret: "a-long-lived-server-side-secret",
293
+ });
294
+
295
+ const asked = await gateway.requestPayment({ paidTo: "iamfatik@blink.sv", amount: sats(21) });
296
+ const read = await gateway.payment(asked);
297
+ const quoted = await gateway.quote({ paidTo: "iamfatik@blink.sv", amount: sats(21) });
298
+
299
+ const lnurl = gateway.serve.lnurlPay({
300
+ paidTo: "iamfatik@blink.sv",
301
+ amount: sats(21),
302
+ secret: "a-long-lived-server-side-secret",
303
+ });
304
+ const rail = gateway.rails.lightning({ paidTo: "iamfatik@blink.sv", amount: () => sats(21) });
305
+ ```
306
+
307
+ - **the payments themselves** are `requestPayment`, `mint`, `quote`, `watch`, `payment`,
308
+ `payments`, `settled`, `firstSettled`, `follow`, `ticket`, `nameFor` and
309
+ `webhookKey`, all on the instance
310
+ - **what you mount** is on `gateway.serve`: an LNURL-pay endpoint, the two ticket
311
+ endpoints, the verify endpoints for Lightning and for a bank, the webhook route,
312
+ and the readers under it
313
+ - **one call per sale** is on `gateway.rails`: `lightning`, `blindLightning`,
314
+ `bank`, and `transfer` for a bank transfer on its own
315
+ - **the proofs** are free functions, deliberately, because a proof you cannot run
316
+ without the thing being audited is not a proof: `proveOrigin`, `proveSettlement`,
317
+ `proveWrapped`, `carriesProof`, `decodeInvoice`, `preimageMatchesHash`
318
+ - **a payment reads without an assertion.** `Payment` is `MintedPayment |
319
+ WatchedPayment`, so checking `kind` is what makes the address, the amount and the
320
+ invoice non-null. The gateway writes those three together or writes none of them,
321
+ and a record carrying some of the three is refused rather than read
322
+ - **the amounts** are `sats`, `msat` and `fiat`
305
323
 
306
324
  What your service answers once those handlers are mounted is written out in
307
325
  [`openapi.yaml`](openapi.yaml), shipped with this package.
@@ -312,18 +330,20 @@ Every failure from the gateway is an RFC 9457 problem document. Branch on `type`
312
330
  never on prose. `error.status` is what the transport carried, and a document naming
313
331
  a different status in its own body does not override it.
314
332
 
315
- Every `type` below is prefixed `urn:problem-type:thunder-bridge:`, and the four the
316
- SDK exports as constants are named in the last column.
333
+ Every `type` below is prefixed `urn:problem-type:thunder-bridge:`, and every one of
334
+ them is a static string on `ProblemError`, so nothing has to be copied out of this
335
+ table by hand. `ProblemError.is(error, ProblemError.PAYMENT_ALREADY_WATCHED)` is how
336
+ you branch on a type that has no error class of its own.
317
337
 
318
338
  | `type` | Status | What it is |
319
339
  |---|---|---|
320
340
  | `invalid-request` | 400, or 413 for a body over the size ceiling | `detail` names the field |
321
- | `no-wallet-available` | 502, else 422, else 400, following the worst wallet | `NoWalletAvailableError`, `wallets` says why each failed. `NO_WALLET_AVAILABLE` |
322
- | `request-in-flight` | 409 | a request with this `Idempotency-Key` is still running. `REQUEST_IN_FLIGHT` |
323
- | `idempotency-key-reused` | 409 | that key was used for a different request. `IdempotencyConflictError`, `IDEMPOTENCY_KEY_REUSED` |
324
- | `payment-already-watched` | 409 | that payment hash is already watched here. `PAYMENT_ALREADY_WATCHED` |
341
+ | `no-wallet-available` | 502, else 422, else 400, following the worst wallet | `NoWalletAvailableError`, `wallets` says why each failed |
342
+ | `request-in-flight` | 409 | a request with this `Idempotency-Key` is still running, as `IdempotencyConflictError` |
343
+ | `idempotency-key-reused` | 409 | that key was used for a different request, as `IdempotencyConflictError` |
344
+ | `payment-already-watched` | 409 | that payment hash is already watched here |
325
345
  | `caller-unknown` | 403 | the instance keeps a list of callers and your key is not on it |
326
- | `verify-host-refused` | 403 | this instance will not mint, because minting is off or `VERIFY_HOSTS` pins it to a list. Resolve the address yourself and use `watchPayment`. A verify URL that is not public https is `invalid-request` instead |
346
+ | `verify-host-refused` | 403 | this instance will not mint, because minting is off or `VERIFY_HOSTS` pins it to a list. Resolve the address yourself and use `watch`. A verify URL that is not public https is `invalid-request` instead |
327
347
  | `verify-unconfirmed` | 424 | the URL did not answer the LUD-21 shape |
328
348
  | `verify-unconsented` | 424 | the URL did not echo the challenge nonce |
329
349
  | `webhook-unconfirmed` | 424 | the webhook URL did not answer its challenge |
@@ -334,7 +354,7 @@ problem body: `401` when the bearer token does not match, `404` both for an id t
334
354
  gateway never heard of and for one it knows that belongs to a different caller key,
335
355
  so a `403` can never confirm an id exists, `410` when you replay an
336
356
  `Idempotency-Key` whose payment has since been pruned, `500`, and `503` while the
337
- instance is draining or its own health check reads stalled. On a `404` `getPayment`
357
+ instance is draining or its own health check reads stalled. On a `404` `payment`
338
358
  returns `null` rather than throwing.
339
359
 
340
360
  `GatewayCheatError` is different in kind. It reports a gateway that demonstrably
@@ -347,6 +367,7 @@ unproven invoice, and decide it explicitly.
347
367
  ```ts
348
368
  import {
349
369
  GatewayCheatError,
370
+ msat,
350
371
  NoWalletAvailableError,
351
372
  ProblemError,
352
373
  ThunderBridge,
@@ -357,7 +378,7 @@ declare const gateway: ThunderBridge;
357
378
  declare function report(line: string): void;
358
379
 
359
380
  try {
360
- await gateway.createPayment({ lnAddresses: ["iamfatik@blink.sv"], amountMsat: 21_000 });
381
+ await gateway.mint({ paidTo: "iamfatik@blink.sv", amount: msat(21_000) });
361
382
  } catch (error) {
362
383
  if (error instanceof GatewayCheatError) {
363
384
  report(`the gateway cheated: ${error.code} on payment ${error.paymentId}`);
@@ -390,44 +411,33 @@ open, and refuses the payment with a `424` unless the nonce comes back, so deplo
390
411
  the endpoint before you register it.
391
412
 
392
413
  ```ts
393
- import {
394
- ThunderBridge,
395
- answerWebhookChallengeRequest,
396
- isProvablySettled,
397
- parseSettlementRequest,
398
- } from "thunder-bridge";
414
+ import { ThunderBridge } from "thunder-bridge";
399
415
 
400
416
  declare function fulfil(paymentId: string, preimage: string): Promise<void>;
401
417
 
402
418
  const gateway = new ThunderBridge("https://public.thunder-bridge.agora.gripe");
403
- const signer = { publicKey: await gateway.webhookKey() };
404
419
 
405
- export async function POST(request: Request): Promise<Response> {
406
- const challenge = await answerWebhookChallengeRequest(request, signer);
407
- if (challenge) {
408
- return challenge;
409
- }
410
-
411
- const settled = await parseSettlementRequest(request, signer);
412
- if (settled === null) {
413
- return new Response("bad signature", { status: 401 });
414
- }
415
-
416
- const preimage = isProvablySettled(settled) ? settled.preimage : null;
417
- if (preimage === null) {
418
- return new Response("no preimage that hashes to it", { status: 402 });
419
- }
420
+ export const POST = gateway.serve.webhook({
421
+ onSettled: async (settlement) => {
422
+ await fulfil(settlement.id, settlement.preimage);
423
+ },
424
+ });
425
+ ```
420
426
 
421
- await fulfil(settled.id, preimage);
427
+ That route answers the challenge, reads the gateway's own published key, checks the
428
+ signature, and calls you only for a delivery that proves itself: it says paid and it
429
+ carries a preimage that hashes to the payment hash the same body names. A delivery
430
+ that proves nothing gets a `202` and no callback, because acting on an unproven
431
+ claim is the one thing this refuses to do. Pass `onUnproven` when an expiry is news
432
+ you want.
422
433
 
423
- return new Response("ok");
424
- }
425
- ```
434
+ `onSettled` is handed a `Proven<Settlement>`, so the preimage is a `string` rather
435
+ than something to coerce: the check the route already ran is what narrows it.
426
436
 
427
- `isProvablySettled` answers the only question that matters about a delivery: it says
428
- paid and it carries a preimage that hashes to the payment hash the same body names.
429
- Ask the recipient's own server with `proveSettlement` when you want the proof to come
430
- from somewhere other than the delivery.
437
+ `gateway.serve.readSettlement` and `gateway.serve.readPayment` are the same checks
438
+ without the route, for a handler you would rather write yourself. Ask the
439
+ recipient's own server with `proveSettlement` when you want the proof to come from
440
+ somewhere other than the delivery.
431
441
 
432
442
  ## More
433
443
 
@@ -436,8 +446,10 @@ from somewhere other than the delivery.
436
446
  - [docs/proving-a-payment.md](../docs/proving-a-payment.md) - the five origin checks
437
447
  and their failure codes, what settlement means, making the gateway poll nobody but
438
448
  you, the NWC rail, wrapped invoices, and webhooks in full
449
+ - [docs/api.md](../docs/api.md) - every export, generated from the code
450
+ - [docs/recipes.md](../docs/recipes.md) - one runnable program per use case, every name in
451
+ it linked to its own entry in the reference
439
452
  - [the gateway](../README.md) - one level up in this repository
440
- - [examples](../examples) - a paywall on Deno Deploy, a trigger watcher, a bank rail
441
453
 
442
454
  ## Development
443
455