thunder-bridge 0.8.9 → 1.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
@@ -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,11 @@ 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
+ **Webhooks** - [`src/webhook.ts`](src/webhook.ts): `parseSettlementRequest`,
173
+ `parseSettlement`, `isProvablySettled`, `parseWebhookRequest`, `parseWebhook`,
174
+ `parseWatchedWebhookRequest`, `parseWatchedWebhook`, `verifyWebhookSignature`,
175
+ `answerWebhookChallengeRequest`, `answerWebhookChallenge`. See
176
+ [Webhooks](#webhooks).
174
177
 
175
178
  **Errors** - [`src/errors.ts`](src/errors.ts): `ProblemError`,
176
179
  `NoWalletAvailableError`, `GatewayCheatError`, `UnverifiedRecipientError`,
@@ -387,15 +390,17 @@ try {
387
390
 
388
391
  ## Webhooks
389
392
 
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.
393
+ Pass `webhookUrl` when you create a payment, or on any rail. There is no webhook
394
+ secret: a gateway holds nothing of yours, and sending one is refused rather than
395
+ ignored. Every delivery is signed `ed25519=<signature>` with the key the gateway
396
+ publishes at `/webhook-key`, over `<x-timestamp>.<raw body>` rather than the body
397
+ alone, so a captured delivery cannot be replayed at you later.
398
+
399
+ The body is a `Settlement`: the id, the status, the payment hash, the preimage and
400
+ the time. Enough to act on and to check, and no more, so a retry is the same size
401
+ whatever you put in your own record. Read `sealed` back by id when you want it.
402
+ Retries widen until the payment itself runs out, never sooner than an hour. An
403
+ invoice that expires fires nothing.
399
404
 
400
405
  Delivery is at-least-once, so deduplicate on `id`.
401
406
 
@@ -409,85 +414,78 @@ settlement, and it leaves the body unread either way.
409
414
  ```ts
410
415
  import {
411
416
  answerWebhookChallengeRequest,
412
- parseWebhookRequest,
413
- proveSettlement,
417
+ isProvablySettled,
418
+ parseSettlementRequest,
414
419
  } from "thunder-bridge";
415
420
 
421
+ const signs = { publicKey: await gateway.webhookKey() };
422
+
416
423
  app.post("/hooks/paid", async (context) => {
417
- const challenge = await answerWebhookChallengeRequest(context.req.raw, secret);
424
+ const challenge = await answerWebhookChallengeRequest(context.req.raw, signs);
418
425
  if (challenge) return challenge;
419
426
 
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);
427
+ const settled = await parseSettlementRequest(context.req.raw, signs);
428
+ if (settled === null) return context.text("bad signature", 401);
429
+ if (!isProvablySettled(settled)) return context.text("no preimage that hashes to it", 402);
425
430
 
426
- await fulfil(payment.id);
431
+ await fulfil(settled.id, settled.preimage);
427
432
  return context.text("ok");
428
433
  });
429
434
  ```
430
435
 
431
- ### A watched payment sends a different body
432
-
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.
438
-
439
- ```ts
440
- import { parseWatchedWebhookRequest } from "thunder-bridge";
436
+ `isProvablySettled` answers the only question that matters about a delivery: it says
437
+ paid and it carries a preimage that hashes to the payment hash the same body names.
438
+ Ask the recipient's own server with `proveSettlement` when the payment is one you
439
+ minted through the gateway and you want the proof to come from somewhere other than
440
+ the delivery.
441
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);
442
+ ### Every rail sends the same body
445
443
 
446
- await fulfil(settled.id, settled.preimage);
447
- return context.text("ok");
448
- });
449
- ```
444
+ `bankRail` and `blindLightningRail` used to need a parser of their own, because their
445
+ webhook carried no address, no amount and no invoice while a minted one did. A
446
+ delivery is a `Settlement` on every rail now, so `parseSettlementRequest` is the only
447
+ one to reach for. `parseWatchedWebhookRequest` is still there for reading the shape a
448
+ socket frame and `getWatched` hand back, which is a payment rather than a delivery.
450
449
 
451
450
  Give each rail its own path, as above, and neither endpoint has to guess which body
452
451
  it was handed. Both events also carry `kind`, `"minted"` or `"watched"`, so a single
453
452
  path serving a trigger that both rails settle on can branch on the field instead of
454
453
  on which fields are missing.
455
454
 
456
- ### Or hand the gateway no secret at all
455
+ ### The gateway holds nothing of yours
457
456
 
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.
457
+ There is nothing to hand it. A delivery is signed with the gateway's own key,
458
+ `x-signature: ed25519=<signature>` over `<x-timestamp>.<raw body>`. Fetch the public
459
+ half once and keep it.
463
460
 
464
461
  ```ts
465
- const publicKey = await gateway.webhookKey();
462
+ const signs = { publicKey: await gateway.webhookKey() };
466
463
 
467
464
  app.post("/hooks/paid", async (context) => {
468
- const challenge = await answerWebhookChallengeRequest(context.req.raw, { publicKey });
465
+ const challenge = await answerWebhookChallengeRequest(context.req.raw, signs);
469
466
  if (challenge) return challenge;
470
467
 
471
- const payment = await parseWebhookRequest(context.req.raw, { publicKey });
472
- if (payment === null) return context.text("bad signature", 401);
468
+ const settled = await parseSettlementRequest(context.req.raw, signs);
469
+ if (settled === null) return context.text("bad signature", 401);
473
470
  ...
474
471
  });
475
472
  ```
476
473
 
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.
474
+ Answering echoes the nonce and nothing else, because there is nothing to sign it with.
475
+ Holding the URL the gateway challenged is the whole proof.
479
476
 
480
477
  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.
478
+ signs alike and an operator rotating that key changes this one too. A signature that
479
+ stops verifying is therefore a reason to read `/webhook-key` again before it is a
480
+ reason to distrust the gateway. A `sha256=` signature is refused outright: that scheme
481
+ is gone.
482
+
483
+ `parseSettlementRequest` refuses anything more than five minutes out of date,
484
+ adjustable with `toleranceSecs`. The signature proves the delivery came from the
485
+ gateway. It does not prove the payment happened, because the gateway holds the key
486
+ that signs it either way. The proof is the preimage, checked by `isProvablySettled`
487
+ against the hash in the same body, or `proveSettlement` against the recipient's own
488
+ server when you want the answer from somewhere else entirely.
491
489
 
492
490
  For a framework that hands you the raw body and headers separately, use
493
491
  `parseWebhook`. The body must be the bytes as received, so mount a raw body parser