thunder-bridge 0.8.10 → 1.1.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
@@ -79,7 +79,9 @@ them and this table does not repeat them.
79
79
 
80
80
  | Export | What it does |
81
81
  |---|---|
82
- | `new ThunderBridge(baseUrl, options?)` | a gateway handle. `{ verify: false }` turns off the automatic proof, `{ token }` makes the instance yours |
82
+ | `new ThunderBridge(baseUrl, options?)` | a gateway handle. `{ secret }` is your rail secret and makes every call speak as you, `{ verify: false }` turns off the automatic proof, `{ token }` makes the instance yours |
83
+ | `new Gateways(baseUrls, options?)` | the same payment watched at several gateways, so any one of them is replaceable. `onRefused` says which of them would not take it |
84
+ | `gateway.nameFor(paymentHash)` | what this payment is called, worked out before any gateway has heard of it, and the same at all of them |
83
85
  | `gateway.createPayment(params, options?)` | mint an invoice on the first address that can prove one, and prove it before returning |
84
86
  | `gateway.createQuote(params)` | ask which address would take an amount without minting anything |
85
87
  | `gateway.getPayment(id)` | read a payment back, `null` when the gateway never heard of it |
@@ -167,10 +169,17 @@ const toPay = invoiceToSvg(payment.bolt11, { size: 320, color: "#1a1a2e" });
167
169
  const tipJar = lnurlToSvg("https://agora.gripe/tip");
168
170
  ```
169
171
 
170
- **Webhooks** - [`src/webhook.ts`](src/webhook.ts): `parseWebhookRequest`,
171
- `parseWebhook`, `parseWatchedWebhookRequest`, `parseWatchedWebhook`,
172
- `verifyWebhookSignature`, `answerWebhookChallengeRequest`,
173
- `answerWebhookChallenge`. See [Webhooks](#webhooks).
172
+ **Minting your own invoice** - [`src/rail.ts`](src/rail.ts): `invoiceFrom`, from
173
+ `thunder-bridge/server`. A gateway that does not mint is one that never sees an
174
+ address or an amount, so this is how a client gets a provable invoice itself and hands
175
+ the gateway only a hash, a url and an expiry. Server side, because it resolves
176
+ hostnames and refuses a private one.
177
+
178
+ **Webhooks** - [`src/webhook.ts`](src/webhook.ts): `parseSettlementRequest`,
179
+ `parseSettlement`, `isProvablySettled`, `parseWebhookRequest`, `parseWebhook`,
180
+ `parseWatchedWebhookRequest`, `parseWatchedWebhook`, `verifyWebhookSignature`,
181
+ `answerWebhookChallengeRequest`, `answerWebhookChallenge`. See
182
+ [Webhooks](#webhooks).
174
183
 
175
184
  **Errors** - [`src/errors.ts`](src/errors.ts): `ProblemError`,
176
185
  `NoWalletAvailableError`, `GatewayCheatError`, `UnverifiedRecipientError`,
@@ -387,15 +396,17 @@ try {
387
396
 
388
397
  ## Webhooks
389
398
 
390
- Pass `webhookUrl` and optionally `webhookSecret` when you create a payment, or on
391
- any rail. Once it reaches `paid` the gateway POSTs the same JSON the API returns,
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.
399
+ Pass `webhookUrl` when you create a payment, or on any rail. There is no webhook
400
+ 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.
404
+
405
+ The body is a `Settlement`: the id, the status, the payment hash, the preimage and
406
+ the time. Enough to act on and to check, and no more, so a retry is the same size
407
+ whatever you put in your own record. Read `sealed` back by id when you want it.
408
+ Retries widen until the payment itself runs out, never sooner than an hour. An
409
+ invoice that expires fires nothing.
399
410
 
400
411
  Delivery is at-least-once, so deduplicate on `id`.
401
412
 
@@ -409,85 +420,78 @@ settlement, and it leaves the body unread either way.
409
420
  ```ts
410
421
  import {
411
422
  answerWebhookChallengeRequest,
412
- parseWebhookRequest,
413
- proveSettlement,
423
+ isProvablySettled,
424
+ parseSettlementRequest,
414
425
  } from "thunder-bridge";
415
426
 
427
+ const signs = { publicKey: await gateway.webhookKey() };
428
+
416
429
  app.post("/hooks/paid", async (context) => {
417
- const challenge = await answerWebhookChallengeRequest(context.req.raw, secret);
430
+ const challenge = await answerWebhookChallengeRequest(context.req.raw, signs);
418
431
  if (challenge) return challenge;
419
432
 
420
- const payment = await parseWebhookRequest(context.req.raw, secret);
421
- if (payment === null) return context.text("bad signature", 401);
422
-
423
- const preimage = await proveSettlement(payment, requestFor(payment.id));
424
- if (preimage === null) return context.text("the recipient has not seen it", 402);
433
+ const settled = await parseSettlementRequest(context.req.raw, signs);
434
+ if (settled === null) return context.text("bad signature", 401);
435
+ if (!isProvablySettled(settled)) return context.text("no preimage that hashes to it", 402);
425
436
 
426
- await fulfil(payment.id);
437
+ await fulfil(settled.id, settled.preimage);
427
438
  return context.text("ok");
428
439
  });
429
440
  ```
430
441
 
431
- ### A watched payment sends a different body
442
+ `isProvablySettled` answers the only question that matters about a delivery: it says
443
+ paid and it carries a preimage that hashes to the payment hash the same body names.
444
+ Ask the recipient's own server with `proveSettlement` when the payment is one you
445
+ minted through the gateway and you want the proof to come from somewhere other than
446
+ the delivery.
432
447
 
433
- `bankRail` and `blindLightningRail` register a payment the gateway was told almost
434
- nothing about, so its webhook carries no address, no amount and no invoice. That is
435
- not a `Payment`, and `parseWebhook` answers `null` for it, which looks exactly like a
436
- bad signature. Use `parseWatchedWebhookRequest` there instead and you get a
437
- `TriggerEvent`, the shape `getWatched` hands back.
448
+ ### Every rail sends the same body
438
449
 
439
- ```ts
440
- import { parseWatchedWebhookRequest } from "thunder-bridge";
441
-
442
- app.post("/hooks/bank", async (context) => {
443
- const settled = await parseWatchedWebhookRequest(context.req.raw, secret);
444
- if (settled === null) return context.text("bad signature", 401);
445
-
446
- await fulfil(settled.id, settled.preimage);
447
- return context.text("ok");
448
- });
449
- ```
450
+ `bankRail` and `blindLightningRail` used to need a parser of their own, because their
451
+ webhook carried no address, no amount and no invoice while a minted one did. A
452
+ delivery is a `Settlement` on every rail now, so `parseSettlementRequest` is the only
453
+ one to reach for. `parseWatchedWebhookRequest` is still there for reading the shape a
454
+ socket frame and `getWatched` hand back, which is a payment rather than a delivery.
450
455
 
451
456
  Give each rail its own path, as above, and neither endpoint has to guess which body
452
457
  it was handed. Both events also carry `kind`, `"minted"` or `"watched"`, so a single
453
458
  path serving a trigger that both rails settle on can branch on the field instead of
454
459
  on which fields are missing.
455
460
 
456
- ### Or hand the gateway no secret at all
461
+ ### The gateway holds nothing of yours
457
462
 
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
+ There is nothing to hand it. A delivery is signed with the gateway's own key,
464
+ `x-signature: ed25519=<signature>` over `<x-timestamp>.<raw body>`. Fetch the public
465
+ half once and keep it.
463
466
 
464
467
  ```ts
465
- const publicKey = await gateway.webhookKey();
468
+ const signs = { publicKey: await gateway.webhookKey() };
466
469
 
467
470
  app.post("/hooks/paid", async (context) => {
468
- const challenge = await answerWebhookChallengeRequest(context.req.raw, { publicKey });
471
+ const challenge = await answerWebhookChallengeRequest(context.req.raw, signs);
469
472
  if (challenge) return challenge;
470
473
 
471
- const payment = await parseWebhookRequest(context.req.raw, { publicKey });
472
- if (payment === null) return context.text("bad signature", 401);
474
+ const settled = await parseSettlementRequest(context.req.raw, signs);
475
+ if (settled === null) return context.text("bad signature", 401);
473
476
  ...
474
477
  });
475
478
  ```
476
479
 
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.
480
+ Answering echoes the nonce and nothing else, because there is nothing to sign it with.
481
+ Holding the URL the gateway challenged is the whole proof.
479
482
 
480
483
  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.
484
-
485
- `parseWebhookRequest` refuses anything more than five minutes out of date,
486
- adjustable with `toleranceSecs`. The signature proves the body came from someone
487
- holding your secret. It does not prove the payment happened, since the gateway
488
- holds that secret too. The proof is `proveSettlement`, and it needs the request you
489
- originally sent, which is why `requestFor` above is your own lookup from a payment
490
- id back to the `CreatePaymentParams` you stored.
484
+ signs alike and an operator rotating that key changes this one too. A signature that
485
+ stops verifying is therefore a reason to read `/webhook-key` again before it is a
486
+ reason to distrust the gateway. A `sha256=` signature is refused outright: that scheme
487
+ is gone.
488
+
489
+ `parseSettlementRequest` refuses anything more than five minutes out of date,
490
+ adjustable with `toleranceSecs`. The signature proves the delivery came from the
491
+ gateway. It does not prove the payment happened, because the gateway holds the key
492
+ that signs it either way. The proof is the preimage, checked by `isProvablySettled`
493
+ against the hash in the same body, or `proveSettlement` against the recipient's own
494
+ server when you want the answer from somewhere else entirely.
491
495
 
492
496
  For a framework that hands you the raw body and headers separately, use
493
497
  `parseWebhook`. The body must be the bytes as received, so mount a raw body parser