thunder-bridge 2.2.1 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,45 +1,16 @@
1
1
  # thunder-bridge
2
2
 
3
- A JavaScript client for a Thunder Bridge gateway. Give it a priority list of
4
- lightning addresses and an amount, and it hands back an invoice minted by the
5
- recipient's own wallet, proven against that recipient's own server before it
6
- returns. The gateway mints nothing, holds nothing and forwards nothing.
3
+ [![npm](https://img.shields.io/npm/v/thunder-bridge)](https://www.npmjs.com/package/thunder-bridge)
4
+ [![ci](https://github.com/i-am-fatik/thunder-bridge/actions/workflows/ci.yml/badge.svg)](https://github.com/i-am-fatik/thunder-bridge/actions/workflows/ci.yml)
7
5
 
8
- LUD-21 is the shape of the proof rather than the whole of it. Whichever rail a
9
- payment runs on, the gateway holds a payment hash, polls a `verify` URL and reports
10
- what came back, and four different things can be the one answering.
6
+ Take Bitcoin Lightning and bank payments in a JavaScript app, paid straight into
7
+ your own wallet or bank account. There is no Lightning node to run, and nobody holds
8
+ the money on the way.
11
9
 
12
- ## Whose wallets this works with
13
-
14
- Check this before you build on it. The gateway can watch a payment only when the
15
- recipient's lightning address publishes a LUD-21 `verify` URL **and** releases the
16
- preimage through it. Support belongs to the address domain rather than to the app,
17
- so the same wallet on another domain can answer differently.
18
-
19
- | Works | Address ends with |
20
- |---|---|
21
- | Blink | `@blink.sv` |
22
- | Alby | `@getalby.com` |
23
- | coinos | `@coinos.io`, `@coinos.pro` |
24
- | Minibits | `@minibits.cash` |
25
- | Speed | `@speed.app` |
26
- | Cake, Breez, Blitz, the Spark-hosted brands | `@cake.cash`, `@breez.tips`, `@blitzwalletapp.com` |
27
- | a BTCPay Server of your own | your domain, from v2.3.8 |
28
-
29
- | Refused | Why |
30
- |---|---|
31
- | Wallet of Satoshi, Strike, Cash App, ZBD, Primal, Fountain, every LNbits wallet | no `verify` at all |
32
- | ZEUS Pay, ecash.love | `verify` without a preimage, refused deliberately, since `settled: true` with nothing to hash proves nothing |
33
-
34
- A refusal happens at creation rather than leaving a payment pending until a
35
- watcher gives up, so a recipient finds out before a payer sees a QR code.
36
-
37
- If your recipient is on a refused name, `gateway.rails.nwc` is the way round it: your own
38
- wallet answers over NIP-47 instead of over an address, and the gateway watches the
39
- hash exactly the same. [docs/lud21-coverage.md](../docs/lud21-coverage.md) is the
40
- measured list rather than a reading of changelogs, last surveyed 2026-08-12. Read
41
- every row as true of that date: a wallet that has shipped LUD-21 since still reads
42
- as refused here until the next survey says otherwise.
10
+ You ask for a payment and show the payer a QR code. They pay you directly. A
11
+ Thunder Bridge gateway, a small server you can use or run yourself, watches for the
12
+ payment and tells you once it is paid. The SDK checks what the gateway reports
13
+ against what you already hold, so a gateway cannot fake a payment.
43
14
 
44
15
  ## Install
45
16
 
@@ -47,35 +18,10 @@ as refused here until the next survey says otherwise.
47
18
  npm install thunder-bridge
48
19
  ```
49
20
 
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
-
57
- One import is the whole thing. The other four are for what a checkout page has no
58
- reason to download.
59
-
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 |
67
-
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.
71
-
72
21
  ## Quick start
73
22
 
74
- A page can run the whole flow with no backend of its own. The url below is a shared
75
- demo gateway that answers anyone and forgets everything on restart, so this snippet
76
- runs as written. It is a base url rather than a page, so opening it in a browser
77
- gives a `404` and [`/health`](https://public.thunder-bridge.agora.gripe/health) is
78
- what tells you it is up.
23
+ This runs as written in a page with no backend of its own. The url is a public demo
24
+ gateway that answers anyone and forgets everything on restart.
79
25
 
80
26
  ```ts
81
27
  import { sats, ThunderBridge } from "thunder-bridge";
@@ -98,340 +44,64 @@ await asked.paid();
98
44
  const preimage = await asked.prove();
99
45
  ```
100
46
 
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.
108
- [docs/proving-a-payment.md](../docs/proving-a-payment.md) is the whole argument for
109
- why.
47
+ `requestPayment` gets an invoice from the first address that can give one, checks it
48
+ against the recipient's own server, and draws the QR. `paid` is the gateway's report,
49
+ and it resolves only on a preimage that hashes to the payment hash. `prove` asks the
50
+ recipient's own server, a second source for when the gateway goes silent.
110
51
 
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.
52
+ An amount is `sats(21)`, `msat(21_000)` or `fiat("4.99", "EUR")`. A bare number does
53
+ not compile, so `21` is never read as 21 millisatoshi.
119
54
 
120
- ## How each payment method gets verified
55
+ ## What it does
121
56
 
122
- Every rail ends the same way, with a preimage that has to hash to the payment hash
123
- the gateway was given. What differs is who obtains the invoice, who is asked for the
124
- preimage, and which side does the checking.
125
-
126
- | `rails.lightning` | the gateway asks, at the recipient's LNURL callback |
127
- |---|---|
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 |
129
- | the invoice is checked by | you, `proveOrigin` runs five checks against the recipient's own domain |
130
- | the gateway probes first | nothing, it resolved the address itself |
131
- | the gateway polls | the wallet, directly |
132
- | `settled` comes from | the wallet releasing its preimage |
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 |
134
-
135
- | `rails.blindLightning` | you ask, with `invoiceFrom` on your server |
136
- |---|---|
137
- | the gateway is told | a hash, an expiry and your URL, with the wallet's sealed inside |
138
- | the invoice is checked by | nobody needs to, you resolved the address yourself |
139
- | the gateway probes first | `speaksVerify`: a GET on the URL, then a signed POST nonce it must echo |
140
- | the gateway polls | your `serve.lightningVerify` endpoint, once `relayThrough` is set. Leave it off and the gateway polls the wallet directly, as on the minted rail |
141
- | `settled` comes from | your endpoint, which unseals, asks the wallet and relays the answer |
142
- | the pace is set by | you, `pollEverySecs` |
143
-
144
- | `rails.nwc` | your own wallet mints it, over NIP-47 `make_invoice` |
145
- |---|---|
146
- | the gateway is told | a hash and your URL, with the hash sealed inside |
147
- | the invoice is checked by | nobody, it is your wallet |
148
- | the gateway probes first | the same GET and signed nonce |
149
- | the gateway polls | your `serve.nwcVerify` endpoint |
150
- | `settled` comes from | `lookup_invoice`, refused unless the wallet's own key signed it |
151
- | the pace is set by | you, `pollEverySecs` |
152
-
153
- | `rails.bank` | nobody, there is no invoice |
57
+ | You want to | Call |
154
58
  |---|---|
155
- | the gateway is told | a hash and your URL, whose query is one sealed blob naming neither the amount, the reference nor the account |
156
- | the invoice is checked by | nobody, there is no invoice to check |
157
- | the gateway probes first | the same GET and signed nonce |
158
- | the gateway polls | your `serve.bankVerify` endpoint |
159
- | `settled` comes from | a `Statement` credit matching amount and currency exactly, with the reference anywhere in the payer's text |
160
- | the pace is set by | the statement: `fioStatement` reads every thirty seconds divided by its tokens, and `pollEverySecs` overrides it |
161
-
162
- Two things are worth reading off those blocks rather than inferring.
163
-
164
- **The checking side flips.** On the minted path the gateway resolved the address, so
165
- it runs no verify probe and no verify challenge, and `proveOrigin` on your side is
166
- the whole defence. A `webhookUrl` is challenged on both paths. On every watched path the gateway resolved nothing, so it probes the URL
167
- and challenges it with a nonce before accepting the watch, refusing with `424` if
168
- nothing answers. Deploy the endpoint before you register it. The challenge is on
169
- unless the operator set `VERIFY_CHALLENGE=0`, which is also why a bare wallet
170
- `verify` URL cannot be handed to `watch`: a wallet will not echo a nonce.
171
-
172
- **What a preimage proves is the same on all four, and narrower than it looks:** that
173
- the server holding the secret says the money arrived, made unforgeable by anyone
174
- else. On the bank rail that secret is an HMAC you derive, which sounds weaker and is
175
- not, because a wallet also minted the preimage it later releases. It rules out a
176
- gateway inventing a settlement. It does not rule out a recipient lying about one, so
177
- this protects a payer against the operator, not against the person being paid.
178
-
179
- `proveWrapped` sits on a different axis. It compares two invoices on one payment
180
- hash and asks nobody anything, so it says whether an operator's wrap is honest
181
- without saying whether either invoice was paid.
182
-
183
- ### What each one costs you
184
-
185
- **`rails.lightning`**
186
-
187
- - the gateway holds the address and the amount, so your order book is readable
188
- from its own logs
189
- - it polls the wallet directly, which puts the recipient's provider in its logs and
190
- in front of its peers
191
- - the wallet's `Cache-Control` sets the poll pace, so how fast a settlement is
192
- noticed is not yours to decide
193
-
194
- **`rails.blindLightning`**
195
-
196
- - a service of your own that has to stay up, so a browser-only integration cannot
197
- use this rail at all
198
- - one long-lived sealing secret, which `seal` refuses under 32 characters, so
199
- `openssl rand -hex 16` is the shortest thing that works
200
- - your endpoint being down means the gateway cannot verify and the payment sits
201
- `pending`
202
- - a wallet you cannot reach answers `502`, so the gateway retries instead of
203
- concluding the invoice went unpaid. The body still reads `settled: false`, and the
204
- status is what separates "could not ask" from "asked, and no"
205
-
206
- **`rails.nwc`**
207
-
208
- - an NWC connection to your own wallet, and the nostr relays behind it
209
- - **scope the connection to `make_invoice` and `lookup_invoice`, never
210
- `pay_invoice`.** It is a key that spends, and a leak with the wrong scope drains
211
- the wallet
212
- - relays unreachable means no verification
213
-
214
- **`rails.bank`**
215
-
216
- - the gateway has to be one of your own: the verify URL names the amount and the
217
- reference, so whoever runs the gateway reads your order book from the watches
218
- alone
219
- - the secret is the entire proof. **Lose it and every past proof is gone**, because
220
- each preimage is derived from it
221
-
222
- **The bank rail has one silent failure worth testing before you promise anybody a
223
- rail.** Two shapes leave a payment `pending` while the money is already in the
224
- account: a bank that truncates the reference, since the match asks whether the
225
- reference is inside what the bank forwarded rather than the other way round, and a
226
- payer whose bank forwards nothing but a numeric variable symbol, since an
227
- alphanumeric reference cannot travel in a numeric field and `X-VS` is not read as an
228
- alternative. Neither has been seen with Fio, which forwards the message untouched.
229
- Check it against the banks your payers actually use.
230
-
231
- ## Who you still have to trust
232
-
233
- The proof narrows the trust rather than removing it. Three parties are left, and
234
- they are not equally constrained.
235
-
236
- | | You trust it with | It cannot |
237
- |---|---|---|
238
- | the gateway | which of your addresses gets paid, and whether it answers at all | pay an address not on your list, bill you more than you asked, or invent a settlement |
239
- | the recipient's wallet provider | that a preimage it releases means the money arrived | mint an invoice for a different account on the same domain |
240
- | the recipient | that the sum they asked for is the sum they are owed | nothing here checks this at all |
241
-
242
- The gateway also sees your address list and your amount. It cannot invent a
243
- settlement because the preimage comes from the recipient's own server, and the
244
- provider cannot mint for another account because the description hash pins an
245
- invoice to one user's metadata under LUD-06. A recipient inflating a total is
246
- outside what any of it proves.
247
-
248
- Four sharp edges, worth reading before you build:
249
-
250
- - **A colluding custodian defeats all of it.** If the recipient's wallet provider
251
- and the gateway are the same party, then whoever holds the money also serves the
252
- metadata and answers the verify requests. Every check passes. This protects a
253
- payer against the operator, never against the recipient's own custodian.
254
- - **The two proof fetches vet the first hop and no further.** `proveOrigin` and
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.
266
- - **Availability is not provable, and an address is not a person.** Every check
267
- here is about an invoice you were given, none about one you were refused, and
268
- proving an invoice belongs to an address never proves the address belongs to
269
- whoever you think it does.
270
-
271
- `agreesWithItself` 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
276
- [docs/proving-a-payment.md](../docs/proving-a-payment.md).
277
-
278
- ## What you call
279
-
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.
284
-
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:
59
+ | show a QR and wait until it is paid, with no backend of your own | `requestPayment` |
60
+ | sell from your server, paid to your Lightning address | `gateway.rails.lightning` |
61
+ | sell from your server, paid into your own wallet over Nostr Wallet Connect | `gateway.rails.nwc` |
62
+ | sell from your server, paid by bank transfer | `gateway.rails.bank` |
63
+ | hear about every payment on your server | `gateway.serve.webhook` |
287
64
 
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
- });
65
+ A rail is one call per sale. By default the gateway then checks the payment through
66
+ an endpoint on your server, which you mount from `gateway.serve`.
67
+ [docs/recipes.md](../docs/recipes.md) has one runnable program per use case.
294
68
 
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) });
69
+ ## Which wallets work
298
70
 
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
- ```
71
+ | Rail | Works with |
72
+ |---|---|
73
+ | `gateway.rails.nwc` | any wallet whose NWC connection grants `make_invoice` and `lookup_invoice` |
74
+ | `gateway.rails.bank` | any account whose statement you can read. `fioStatement` reads Fio, any other bank is a `Statement` you write |
75
+ | `gateway.rails.lightning`, `requestPayment` | a lightning address on a domain that releases the preimage over LUD-21, below |
306
76
 
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 `lightningVerify`, `bankVerify` and `nwcVerify`, the
312
- webhook route, and the readers under it
313
- - **one call per sale** is on `gateway.rails`: `lightning`, `blindLightning`,
314
- `bank`, `nwc`, and `transfer` for a bank transfer on its own. What the NWC rail
315
- needs to reach your wallet, `nwcConnection` and the wallet calls, stays in
316
- `thunder-bridge/nwc`
317
- - **the proofs** are free functions, deliberately, because a proof you cannot run
318
- without the thing being audited is not a proof: `proveOrigin`, `proveSettlement`,
319
- `proveWrapped`, `decodeInvoice`, `preimageMatchesHash`. `agreesWithItself` sits
320
- beside them and proves less, as the table below says
321
- - **a payment reads without an assertion.** `Payment` is `MintedPayment |
322
- WatchedPayment`, so checking `kind` is what makes the address, the amount and the
323
- invoice non-null. The gateway writes those three together or writes none of them,
324
- and a record carrying some of the three is refused rather than read
325
- - **the amounts** are `sats`, `msat` and `fiat`
326
-
327
- What your service answers once those handlers are mounted is written out in
328
- [`openapi.yaml`](openapi.yaml), shipped with this package.
329
-
330
- ### Which call checks what
331
-
332
- There are several ways to hear that a payment settled because there are several
333
- places to hear it from. They do not check the same thing, and the difference is
334
- what you may act on without asking anyone else.
335
-
336
- | You hear it through | What the claim is checked against |
337
- | --- | --- |
338
- | `payment`, `settled`, `firstSettled`, `paid()` on a `requestPayment`, and `Gateways.settled` | the `{ id, paymentHash }` you hold. Another payment, another hash, or a preimage that does not hash to yours throws `GatewayCheatError` |
339
- | `serve.webhook`, `serve.readSettlement`, `serve.readPayment` | the gateway's key, the timestamp, the URL it was sent to, and the preimage against the hash in the same body. Find your order by that hash before you act, which is what makes a pair the gateway invented find nothing |
340
- | `payments`, `follow` | the report itself. An entry claiming paid whose preimage does not hash to its own hash is left out of `payments` and reaches `follow`'s `onError` rather than `onPayment` |
341
- | `agreesWithItself` | the report itself, and nothing you hold, so a gateway that invents a preimage and names its hash passes it |
342
- | `proveSettlement` | the recipient's own verify URL, after the origin proof, so the gateway is not asked at all |
77
+ | Lightning address on | Ends with |
78
+ |---|---|
79
+ | Blink | `@blink.sv` |
80
+ | Alby | `@getalby.com` |
81
+ | coinos | `@coinos.io`, `@coinos.pro` |
82
+ | Minibits | `@minibits.cash` |
83
+ | Speed | `@speed.app` |
84
+ | Cake, Breez, Blitz, the Spark-hosted brands | `@cake.cash`, `@breez.tips`, `@blitzwalletapp.com` |
85
+ | a BTCPay Server of your own | your domain, from v2.3.8 |
343
86
 
344
- ## Errors
87
+ Wallet of Satoshi, Strike, Cash App, ZBD, Primal, Fountain, LNbits, ZEUS Pay and
88
+ ecash.love release no preimage, so a recipient there needs a wallet with NWC instead.
89
+ The measured list, last surveyed 2026-08-12, is
90
+ [docs/lud21-coverage.md](../docs/lud21-coverage.md).
345
91
 
346
- Every failure from the gateway is an RFC 9457 problem document. Branch on `type`,
347
- never on prose. `error.status` is what the transport carried, and a document naming
348
- a different status in its own body does not override it.
349
-
350
- Every `type` below is prefixed `urn:problem-type:thunder-bridge:`, and every one of
351
- them is a static string on `ProblemError`, so nothing has to be copied out of this
352
- table by hand. `ProblemError.is(error, ProblemError.PAYMENT_ALREADY_WATCHED)` is how
353
- you branch on a type that has no error class of its own.
354
-
355
- | `type` | Status | What it is |
356
- |---|---|---|
357
- | `invalid-request` | 400, or 413 for a body over the size ceiling | `detail` names the field |
358
- | `no-wallet-available` | 502, else 422, else 400, following the worst wallet | `NoWalletAvailableError`, `wallets` says why each failed |
359
- | `request-in-flight` | 409 | a request with this `Idempotency-Key` is still running, as `IdempotencyConflictError` |
360
- | `idempotency-key-reused` | 409 | that key was used for a different request, as `IdempotencyConflictError` |
361
- | `payment-already-watched` | 409 | that payment hash is already watched here |
362
- | `caller-unknown` | 403 | the instance keeps a list of callers and your key is not on it |
363
- | `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 |
364
- | `verify-unconfirmed` | 424 | the URL did not answer the LUD-21 shape |
365
- | `verify-unconsented` | 424 | the URL did not echo the challenge nonce |
366
- | `webhook-unconfirmed` | 424 | the webhook URL did not answer its challenge |
367
- | `too-many-pending` | 429, with `ratelimit-limit` and `ratelimit-remaining` set | the caller is over its share of the instance's `MAX_PENDING` |
368
-
369
- The rest carry `about:blank` as their type, which the gateway seeds into every
370
- problem body: `401` when the bearer token does not match, `404` both for an id this
371
- gateway never heard of and for one it knows that belongs to a different caller key,
372
- so a `403` can never confirm an id exists, `410` when you replay an
373
- `Idempotency-Key` whose payment has since been pruned, `500`, and `503` while the
374
- instance is draining or its own health check reads stalled. On a `404` `payment`
375
- returns `null` rather than throwing.
376
-
377
- `GatewayCheatError` is different in kind. It reports a gateway that demonstrably
378
- misbehaved, and `code` names the check that caught it. `UnverifiedRecipientError` is
379
- neither an accusation nor a clean bill of health: it means a check could not be run
380
- at all, because the recipient's server was down, timed out, answered something
381
- unreadable, or the browser was blocked by CORS. Decide what you want to do with an
382
- unproven invoice, and decide it explicitly.
92
+ ## What the proof covers
383
93
 
384
- ```ts
385
- import {
386
- GatewayCheatError,
387
- msat,
388
- NoWalletAvailableError,
389
- ProblemError,
390
- ThunderBridge,
391
- UnverifiedRecipientError,
392
- } from "thunder-bridge";
393
-
394
- declare const gateway: ThunderBridge;
395
- declare function report(line: string): void;
396
-
397
- try {
398
- await gateway.mint({ paidTo: "iamfatik@blink.sv", amount: msat(21_000) });
399
- } catch (error) {
400
- if (error instanceof GatewayCheatError) {
401
- report(`the gateway cheated: ${error.code} on payment ${error.paymentId}`);
402
- } else if (error instanceof UnverifiedRecipientError) {
403
- report(`could not reach ${error.lnAddress} to check the invoice`);
404
- } else if (error instanceof NoWalletAvailableError) {
405
- for (const wallet of error.wallets) {
406
- report(`${wallet.address}: ${wallet.reason}`);
407
- }
408
- } else if (error instanceof ProblemError) {
409
- report(`${error.status} ${error.title}`);
410
- } else {
411
- throw error;
412
- }
413
- }
414
- ```
94
+ A payment counts as paid only when the recipient's wallet, or your own server on the
95
+ bank rail, releases a preimage that hashes to the payment hash. The gateway cannot
96
+ make one up. The proof does not cover a recipient asking for more than they are owed,
97
+ or a wallet provider that also runs the gateway.
98
+ [docs/proving-a-payment.md](../docs/proving-a-payment.md#who-you-still-have-to-trust)
99
+ says who you still have to trust.
415
100
 
416
101
  ## Webhooks
417
102
 
418
- Pass `webhookUrl` when you create a payment, or on any rail. There is no webhook
419
- secret: a gateway holds nothing of yours, and sending one is refused rather than
420
- ignored. Every delivery carries `x-signature-v2: ed25519=<signature>` with the key
421
- the gateway publishes at `/webhook-key`, over the URL it was sent to, `<x-timestamp>`
422
- and the raw body, so a delivery made for somebody else's endpoint proves nothing at
423
- yours and a captured one cannot be replayed at you later. Behind a proxy that hands
424
- the request on under another host or scheme, pass the URL you registered as `url`.
425
- Until then
426
- `serve.webhook` acts on each settlement once, answering a replay `200` without
427
- calling you again. Delivery is still at-least-once: a retry after that window, or
428
- one reaching another instance of your server, calls you again, so fulfil
429
- idempotently on `id`.
430
-
431
- Your handler answers one challenge before any of that. The gateway POSTs
432
- `{"type":"webhook-challenge","nonce":"..."}` to the URL while the create is still
433
- open, and refuses the payment with a `424` unless the nonce comes back, so deploy
434
- the endpoint before you register it.
103
+ Pass `webhookUrl` when you create a payment, or on any rail, and mount this route at
104
+ that URL first. The gateway checks that the URL answers before it accepts the payment.
435
105
 
436
106
  ```ts
437
107
  import { ThunderBridge } from "thunder-bridge";
@@ -447,39 +117,50 @@ export const POST = gateway.serve.webhook({
447
117
  });
448
118
  ```
449
119
 
450
- That route answers the challenge, reads the gateway's own published key, checks the
451
- signature, and calls you only for a delivery that proves itself: it says paid and it
452
- carries a preimage that hashes to the payment hash the same body names. A delivery
453
- that proves nothing gets a `202` and no callback, because acting on an unproven
454
- claim is the one thing this refuses to do. Pass `onUnproven` when an expiry is news
455
- you want.
120
+ The route checks the gateway's signature and calls `onSettled` only for a delivery
121
+ whose preimage hashes to its payment hash. A delivery can arrive more than once, so
122
+ fulfil idempotently on `id`.
123
+ [Webhooks in full](../docs/proving-a-payment.md#webhooks-in-full) covers the
124
+ signature, replays, and frameworks that hand you a raw body.
125
+
126
+ ## Errors
127
+
128
+ Every failure from the gateway is a `ProblemError` carrying an RFC 9457 `type`.
129
+ [docs/errors.md](../docs/errors.md) lists every type, the failures the client finds
130
+ itself, and a handler.
456
131
 
457
- `onSettled` is handed a `SelfConsistent<Settlement>`, so the preimage is a `string` rather
458
- than something to coerce: the check the route already ran is what narrows it.
132
+ ## Imports and runtimes
459
133
 
460
- `gateway.serve.readSettlement` and `gateway.serve.readPayment` are the same checks
461
- without the route, for a handler you would rather write yourself. Ask the
462
- recipient's own server with `proveSettlement` when you want the proof to come from
463
- somewhere other than the delivery.
134
+ `thunder-bridge` is the whole client. The other four entry points hold what a
135
+ checkout page should not have to download.
136
+
137
+ | Import | What it is for |
138
+ |---|---|
139
+ | `thunder-bridge` | the gateway, the proofs, the errors and the amounts. Everything is reached through one instance |
140
+ | `thunder-bridge/qr` | a payload as an SVG or a data URL, for a page that draws a QR of its own |
141
+ | `thunder-bridge/price` | the exchange venues behind `fiat`, for pricing off your own book instead |
142
+ | `thunder-bridge/bank` | a bank statement reader, currently Fio |
143
+ | `thunder-bridge/nwc` | your own wallet over NIP-47, which carries the nostr crypto no browser wants |
144
+
145
+ Anything that opens a socket needs Node 22 or newer: `requestPayment`, `settled`,
146
+ `firstSettled`, `follow`, `attend` and every NWC call. `invoiceFrom`,
147
+ `serve.lightningVerify` and `rails.lightning` without `gatewayMints` resolve wallet
148
+ hostnames through `node:dns`, so they will not run on Cloudflare Workers. Everything
149
+ else runs on any runtime with `fetch` and `crypto.subtle`.
464
150
 
465
151
  ## More
466
152
 
153
+ - [docs/recipes.md](../docs/recipes.md) - one runnable program per use case
154
+ - [docs/api.md](../docs/api.md) - every export, generated from the code
155
+ - [docs/proving-a-payment.md](../docs/proving-a-payment.md) - how each rail is
156
+ verified, what each one costs you, and who you still have to trust
157
+ - [docs/errors.md](../docs/errors.md) - every problem type and what to do with it
467
158
  - [docs/lud21-coverage.md](../docs/lud21-coverage.md) - which address domains
468
159
  release a preimage, and how that was measured
469
- - [docs/proving-a-payment.md](../docs/proving-a-payment.md) - the five origin checks
470
- and their failure codes, what settlement means, making the gateway poll nobody but
471
- you, the NWC rail, wrapped invoices, and webhooks in full
472
- - [docs/api.md](../docs/api.md) - every export, generated from the code
473
- - [docs/recipes.md](../docs/recipes.md) - one runnable program per use case, every name in
474
- it linked to its own entry in the reference
475
- - [the gateway](../README.md) - one level up in this repository
476
-
477
- ## Development
478
-
479
- ```bash
480
- npm install
481
- npm test
482
- npm run build
483
- ```
160
+ - [`openapi.yaml`](openapi.yaml) - what your endpoints answer once mounted, shipped
161
+ with this package
162
+ - [CHANGELOG.md](CHANGELOG.md) - what changed in each version, and how to move to 3.0
163
+ - [the gateway](../README.md) - running one yourself, and developing this repository
164
+ - [issues](https://github.com/i-am-fatik/thunder-bridge/issues) - bugs and questions
484
165
 
485
166
  MIT.
package/dist/bank.cjs CHANGED
@@ -148,6 +148,9 @@ var INITIAL_STATE = new Uint32Array([
148
148
  1541459225
149
149
  ]);
150
150
 
151
+ // ../core/signature.ts
152
+ var import_structured_headers = require("structured-headers");
153
+
151
154
  // ../core/sealed.ts
152
155
  var MIN_SECRET_CHARS = 32;
153
156
  function refuseAWeakSecret(secret) {
package/dist/bank.d.cts CHANGED
@@ -1,7 +1,7 @@
1
- import { S as Statement } from './bank-DvcV85sa.cjs';
2
- export { B as BankAgentConfig, a as BankOrder, b as BankTransfer, c as BankTransferParams, d as BankVerifyConfig, C as Credit, e as bankAgent } from './bank-DvcV85sa.cjs';
3
- import './qr-CF-YeXU1.cjs';
1
+ import { S as Statement } from './nwc-h_-cGvMb.cjs';
2
+ export { B as BankAgentConfig, a as BankOrder, b as BankTransfer, c as BankTransferParams, d as BankVerifyConfig, C as Credit, e as bankAgent } from './nwc-h_-cGvMb.cjs';
4
3
  import './types-DYZ9EkmJ.cjs';
4
+ import './qr-CF-YeXU1.cjs';
5
5
 
6
6
  /** A Fio account to read credits from, as its own API describes one */
7
7
  interface FioConfig {
package/dist/bank.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- import { S as Statement } from './bank-BgA45yOU.js';
2
- export { B as BankAgentConfig, a as BankOrder, b as BankTransfer, c as BankTransferParams, d as BankVerifyConfig, C as Credit, e as bankAgent } from './bank-BgA45yOU.js';
3
- import './qr-CF-YeXU1.js';
1
+ import { S as Statement } from './nwc-BivXGSPY.js';
2
+ export { B as BankAgentConfig, a as BankOrder, b as BankTransfer, c as BankTransferParams, d as BankVerifyConfig, C as Credit, e as bankAgent } from './nwc-BivXGSPY.js';
4
3
  import './types-DYZ9EkmJ.js';
4
+ import './qr-CF-YeXU1.js';
5
5
 
6
6
  /** A Fio account to read credits from, as its own API describes one */
7
7
  interface FioConfig {
package/dist/bank.js CHANGED
@@ -121,6 +121,14 @@ var INITIAL_STATE = new Uint32Array([
121
121
  1541459225
122
122
  ]);
123
123
 
124
+ // ../core/signature.ts
125
+ import {
126
+ parseDictionary,
127
+ serializeDictionary,
128
+ serializeInnerList,
129
+ serializeString
130
+ } from "structured-headers";
131
+
124
132
  // ../core/sealed.ts
125
133
  var MIN_SECRET_CHARS = 32;
126
134
  function refuseAWeakSecret(secret) {