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 +66 -62
- package/dist/index.cjs +291 -118
- package/dist/index.d.cts +63 -17
- package/dist/index.d.ts +63 -17
- package/dist/index.js +287 -118
- package/dist/{rail-J8QoYbSr.d.cts → rail-Dp8bs6uZ.d.cts} +54 -6
- package/dist/{rail-J8QoYbSr.d.ts → rail-Dp8bs6uZ.d.ts} +54 -6
- package/dist/server.cjs +3 -2
- package/dist/server.d.cts +2 -2
- package/dist/server.d.ts +2 -2
- package/dist/server.js +2 -2
- package/openapi.yaml +43 -1
- package/package.json +1 -1
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
|
-
**
|
|
171
|
-
`
|
|
172
|
-
|
|
173
|
-
|
|
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`
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
delivery cannot be replayed at you later.
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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
|
-
|
|
413
|
-
|
|
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,
|
|
430
|
+
const challenge = await answerWebhookChallengeRequest(context.req.raw, signs);
|
|
418
431
|
if (challenge) return challenge;
|
|
419
432
|
|
|
420
|
-
const
|
|
421
|
-
if (
|
|
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(
|
|
437
|
+
await fulfil(settled.id, settled.preimage);
|
|
427
438
|
return context.text("ok");
|
|
428
439
|
});
|
|
429
440
|
```
|
|
430
441
|
|
|
431
|
-
|
|
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
|
-
|
|
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
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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
|
-
###
|
|
461
|
+
### The gateway holds nothing of yours
|
|
457
462
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
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
|
|
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,
|
|
471
|
+
const challenge = await answerWebhookChallengeRequest(context.req.raw, signs);
|
|
469
472
|
if (challenge) return challenge;
|
|
470
473
|
|
|
471
|
-
const
|
|
472
|
-
if (
|
|
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
|
|
478
|
-
|
|
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.
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
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
|