thunder-bridge 1.5.0 → 2.2.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
@@ -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` from `thunder-bridge/nwc` is the way round it: your own
37
+ If your recipient is on a refused name, `gateway.rails.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
@@ -137,16 +137,16 @@ preimage, and which side does the checking.
137
137
  | the gateway is told | a hash, an expiry and your URL, with the wallet's sealed inside |
138
138
  | the invoice is checked by | nobody needs to, you resolved the address yourself |
139
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.verify` endpoint, once `relayThrough` is set. Leave it off and the gateway polls the wallet directly, as on the minted rail |
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
141
  | `settled` comes from | your endpoint, which unseals, asks the wallet and relays the answer |
142
142
  | the pace is set by | you, `pollEverySecs` |
143
143
 
144
- | `nwcRail` | your own wallet mints it, over NIP-47 `make_invoice` |
144
+ | `rails.nwc` | your own wallet mints it, over NIP-47 `make_invoice` |
145
145
  |---|---|
146
146
  | the gateway is told | a hash and your URL, with the hash sealed inside |
147
147
  | the invoice is checked by | nobody, it is your wallet |
148
148
  | the gateway probes first | the same GET and signed nonce |
149
- | the gateway polls | your `nwcVerifyEndpoint` |
149
+ | the gateway polls | your `serve.nwcVerify` endpoint |
150
150
  | `settled` comes from | `lookup_invoice`, refused unless the wallet's own key signed it |
151
151
  | the pace is set by | you, `pollEverySecs` |
152
152
 
@@ -203,7 +203,7 @@ without saying whether either invoice was paid.
203
203
  concluding the invoice went unpaid. The body still reads `settled: false`, and the
204
204
  status is what separates "could not ask" from "asked, and no"
205
205
 
206
- **`nwcRail`**
206
+ **`rails.nwc`**
207
207
 
208
208
  - an NWC connection to your own wallet, and the nostr relays behind it
209
209
  - **scope the connection to `make_invoice` and `lookup_invoice`, never
@@ -252,21 +252,23 @@ Four sharp edges, worth reading before you build:
252
252
  metadata and answers the verify requests. Every check passes. This protects a
253
253
  payer against the operator, never against the recipient's own custodian.
254
254
  - **The two proof fetches vet the first hop and no further.** `proveOrigin` and
255
- `proveSettlement` use the runtime's default redirect handling, so a public https
256
- host answering `302` to a private address is followed there. `invoiceFrom` is not
257
- like this: it resolves through the outbound guard, which sets `redirect: "manual"`
258
- and re-vets every hop. Keep egress control outside this package if that matters.
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.
259
260
  - **A payment read cold is only as pinned as its creation.** `payment` checks
260
- the preimage against the `paymentHash` in the same record, and it was
261
- `proveOrigin` at creation, against the request you wrote, that tied that hash to
262
- an invoice the recipient issued. Store the request alongside the payment id, or a
263
- cold read is checking the gateway's numbers against each other and nothing more.
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.
264
266
  - **Availability is not provable, and an address is not a person.** Every check
265
267
  here is about an invoice you were given, none about one you were refused, and
266
268
  proving an invoice belongs to an address never proves the address belongs to
267
269
  whoever you think it does.
268
270
 
269
- `carriesProof` is the one to be careful with: it asks only whether a report holds
271
+ `agreesWithItself` is the one to be careful with: it asks only whether a report holds
270
272
  together, so a gateway that generates a preimage, hashes it and builds an invoice
271
273
  around that hash passes it. If a payment matters, ask the recipient with
272
274
  `prove` on the payment request or with `proveSettlement`. The full argument, including the five
@@ -291,7 +293,7 @@ const gateway = new ThunderBridge("https://public.thunder-bridge.agora.gripe", {
291
293
  });
292
294
 
293
295
  const asked = await gateway.requestPayment({ paidTo: "iamfatik@blink.sv", amount: sats(21) });
294
- const read = await gateway.payment(asked.id);
296
+ const read = await gateway.payment(asked);
295
297
  const quoted = await gateway.quote({ paidTo: "iamfatik@blink.sv", amount: sats(21) });
296
298
 
297
299
  const lnurl = gateway.serve.lnurlPay({
@@ -306,13 +308,16 @@ const rail = gateway.rails.lightning({ paidTo: "iamfatik@blink.sv", amount: () =
306
308
  `payments`, `settled`, `firstSettled`, `follow`, `ticket`, `nameFor` and
307
309
  `webhookKey`, all on the instance
308
310
  - **what you mount** is on `gateway.serve`: an LNURL-pay endpoint, the two ticket
309
- endpoints, the verify endpoints for Lightning and for a bank, the webhook route,
310
- and the readers under it
311
+ endpoints, the verify endpoints `lightningVerify`, `bankVerify` and `nwcVerify`, the
312
+ webhook route, and the readers under it
311
313
  - **one call per sale** is on `gateway.rails`: `lightning`, `blindLightning`,
312
- `bank`, and `transfer` for a bank transfer on its own
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`
313
317
  - **the proofs** are free functions, deliberately, because a proof you cannot run
314
318
  without the thing being audited is not a proof: `proveOrigin`, `proveSettlement`,
315
- `proveWrapped`, `carriesProof`, `decodeInvoice`, `preimageMatchesHash`
319
+ `proveWrapped`, `decodeInvoice`, `preimageMatchesHash`. `agreesWithItself` sits
320
+ beside them and proves less, as the table below says
316
321
  - **a payment reads without an assertion.** `Payment` is `MintedPayment |
317
322
  WatchedPayment`, so checking `kind` is what makes the address, the amount and the
318
323
  invoice non-null. The gateway writes those three together or writes none of them,
@@ -322,6 +327,20 @@ const rail = gateway.rails.lightning({ paidTo: "iamfatik@blink.sv", amount: () =
322
327
  What your service answers once those handlers are mounted is written out in
323
328
  [`openapi.yaml`](openapi.yaml), shipped with this package.
324
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 |
343
+
325
344
  ## Errors
326
345
 
327
346
  Every failure from the gateway is an RFC 9457 problem document. Branch on `type`,
@@ -398,10 +417,16 @@ try {
398
417
 
399
418
  Pass `webhookUrl` when you create a payment, or on any rail. There is no webhook
400
419
  secret: a gateway holds nothing of yours, and sending one is refused rather than
401
- ignored. Every delivery is signed `ed25519=<signature>` with the key the gateway
402
- publishes at `/webhook-key`, over `<x-timestamp>.<raw body>` rather than the body
403
- alone, so a captured delivery cannot be replayed at you later. Delivery is
404
- at-least-once, so deduplicate on `id`.
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`.
405
430
 
406
431
  Your handler answers one challenge before any of that. The gateway POSTs
407
432
  `{"type":"webhook-challenge","nonce":"..."}` to the URL while the create is still
@@ -429,7 +454,7 @@ that proves nothing gets a `202` and no callback, because acting on an unproven
429
454
  claim is the one thing this refuses to do. Pass `onUnproven` when an expiry is news
430
455
  you want.
431
456
 
432
- `onSettled` is handed a `Proven<Settlement>`, so the preimage is a `string` rather
457
+ `onSettled` is handed a `SelfConsistent<Settlement>`, so the preimage is a `string` rather
433
458
  than something to coerce: the check the route already ran is what narrows it.
434
459
 
435
460
  `gateway.serve.readSettlement` and `gateway.serve.readPayment` are the same checks