thunder-bridge 0.8.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.
@@ -0,0 +1,435 @@
1
+ /** Where a payment stands, `paid` is the only status that carries a preimage */
2
+ type PaymentStatus = "pending" | "paid" | "expired";
3
+ /** A payment as the gateway reports it, every field is checkable against the recipient */
4
+ interface Payment {
5
+ id: string;
6
+ lnAddress: string;
7
+ amountMsat: number;
8
+ status: PaymentStatus;
9
+ paymentHash: string;
10
+ bolt11: string;
11
+ preimage: string | null;
12
+ expiresAt: number;
13
+ createdAt: number;
14
+ verifyUrl: string;
15
+ }
16
+ /**
17
+ * `lnAddresses` is a priority list, the gateway takes the first one that can
18
+ * issue a provable invoice for `amountMsat` and the rest are the fallback
19
+ */
20
+ interface CreatePaymentParams {
21
+ lnAddresses: string[];
22
+ amountMsat: number;
23
+ webhookUrl?: string;
24
+ webhookSecret?: string;
25
+ }
26
+ /**
27
+ * `lnAddresses` is the same priority list `createPayment` takes, and quoting it
28
+ * mints nothing and charges the recipient's wallet nothing
29
+ */
30
+ interface CreateQuoteParams {
31
+ lnAddresses: string[];
32
+ amountMsat: number;
33
+ }
34
+ /**
35
+ * Which address would serve an amount, and what the ones ahead of it refused.
36
+ * `feeMsat` is always zero, the payer pays the recipient's own invoice and the
37
+ * gateway is never in the money's path
38
+ */
39
+ interface Quote {
40
+ lnAddress: string;
41
+ amountMsat: number;
42
+ feeMsat: number;
43
+ minMsat: number;
44
+ maxMsat: number;
45
+ metadata: string;
46
+ refusals: WalletFailure[];
47
+ }
48
+ /**
49
+ * What the gateway reports about a payment it is watching, for a trigger stream
50
+ * or for one you minted yourself. `lnAddress` and `amountMsat` are null when the
51
+ * gateway was never told them, which is the point of `watchPayment`: what the
52
+ * watcher needs but the gateway should not know travels in `sealed` instead
53
+ */
54
+ interface TriggerEvent {
55
+ id: string;
56
+ paymentHash: string;
57
+ verifyUrl: string;
58
+ status: PaymentStatus;
59
+ preimage: string | null;
60
+ expiresAt: number;
61
+ createdAt: number;
62
+ sealed: string | null;
63
+ lnAddress: string | null;
64
+ amountMsat: number | null;
65
+ }
66
+ /**
67
+ * An invoice you obtained yourself, handed over to be watched. The gateway is
68
+ * given no address and no amount, so it cannot refuse one recipient rather than
69
+ * all of them
70
+ */
71
+ interface WatchPaymentParams {
72
+ paymentHash: string;
73
+ verifyUrl: string;
74
+ expiresAt: number;
75
+ trigger?: string;
76
+ sealed?: string;
77
+ }
78
+ /** Why one wallet in the list could not be used */
79
+ type WalletReason = "address-unusable" | "unreachable" | "amount-not-accepted" | "cannot-prove-delivery" | "invoice-refused";
80
+ interface WalletFailure {
81
+ address: string;
82
+ reason: WalletReason;
83
+ }
84
+
85
+ interface ThunderBridgeOptions {
86
+ /**
87
+ * Prove every payment against the recipient's own server before handing it
88
+ * back, and refuse a reported settlement whose preimage does not hash to the
89
+ * payment hash, defaults to true
90
+ */
91
+ verify?: boolean;
92
+ /**
93
+ * Sent as `Authorization: Bearer`, which a gateway started with
94
+ * `GATEWAY_TOKEN` requires on every call, the socket handshake included. No
95
+ * browser WebSocket can carry a header, so setting this also puts every socket
96
+ * through a ticket
97
+ */
98
+ token?: string;
99
+ }
100
+ interface WaitOptions {
101
+ /** Give up when this aborts, `AbortSignal.timeout(ms)` covers the usual case */
102
+ signal?: AbortSignal;
103
+ /**
104
+ * Mint a short-lived ticket and put that in the socket URL instead of the
105
+ * payment id. Implied by `token`. The id stays readable inside the ticket,
106
+ * what changes is that a URL out of a log stops opening anything after a
107
+ * minute
108
+ */
109
+ tickets?: boolean;
110
+ }
111
+ interface CreateOptions {
112
+ /**
113
+ * Makes the POST safe to retry. A repeat of a finished request replays its
114
+ * payment instead of asking a wallet for a second invoice, a repeat that
115
+ * arrives while the first is still resolving throws
116
+ * `IdempotencyConflictError`, and the key is held for 24 hours
117
+ */
118
+ idempotencyKey?: string;
119
+ /**
120
+ * Groups this payment with every other one carrying the same secret, so
121
+ * `followTrigger` can watch the place rather than the payment. Only its
122
+ * sha256 reaches the gateway, and the secret itself is what authorises the
123
+ * stream, so keep it apart from any URL a payer sees
124
+ */
125
+ trigger?: string;
126
+ }
127
+ interface FollowOptions {
128
+ /** Called for the recent settlements replayed on connect, then for each new one */
129
+ onPayment: (settled: TriggerEvent) => void;
130
+ /** Called when a connection drops or a frame is refused, the follow keeps going */
131
+ onError?: (error: unknown) => void;
132
+ /** Reconnect after a drop, defaults to true */
133
+ reconnect?: boolean;
134
+ /**
135
+ * The first wait after a drop, doubling up to 30 seconds and jittered so a
136
+ * fleet does not come back in lockstep, defaults to 3000. A connection that
137
+ * opens puts it back to the first wait
138
+ */
139
+ reconnectDelayMs?: number;
140
+ /**
141
+ * Mint a short-lived ticket and put that in the socket URL instead of the
142
+ * secret, one per connection. Keeps the secret out of access logs, at the cost
143
+ * of a POST before each connect. Implied by `token`. Leave it off for a
144
+ * microcontroller, where one hardcoded URL and a dumb reconnect loop is the
145
+ * whole point
146
+ */
147
+ tickets?: boolean;
148
+ }
149
+ /** Talks to a Thunder Bridge gateway and trusts it for nothing it can check itself */
150
+ declare class ThunderBridge {
151
+ private readonly baseUrl;
152
+ private readonly verify;
153
+ private readonly token;
154
+ constructor(baseUrl: string, options?: ThunderBridgeOptions);
155
+ /**
156
+ * Ask the gateway for an invoice payable to the first address on your list
157
+ * that can issue a provable one, throws `NoWalletAvailableError` when none can
158
+ * and `GatewayCheatError` when what comes back is not what you asked for
159
+ */
160
+ createPayment(params: CreatePaymentParams, options?: CreateOptions): Promise<Payment>;
161
+ /**
162
+ * Ask which address would serve an amount without minting anything, throws
163
+ * `NoWalletAvailableError` when none would. A quote is a probe and not a
164
+ * promise: the address it names can still be refused at create time, because
165
+ * whether a wallet returns a provable invoice cannot be known without asking
166
+ * it for one, and asking mints it
167
+ */
168
+ createQuote(params: CreateQuoteParams): Promise<Quote>;
169
+ /** Read a payment back, null when the gateway has never heard of it */
170
+ getPayment(id: string): Promise<Payment | null>;
171
+ /**
172
+ * List what this gateway is watching, newest first. Only a gateway started
173
+ * with `GATEWAY_TOKEN` serves this, because on a shared one it would hand
174
+ * every caller everyone else's payments, so a public gateway answers 404.
175
+ *
176
+ * `scanned` says how many settled records were looked at to build the page.
177
+ * Anything older than that window is not in the answer, and the list does not
178
+ * pretend otherwise
179
+ */
180
+ listPayments(limit?: number): Promise<{
181
+ payments: TriggerEvent[];
182
+ scanned: number;
183
+ }>;
184
+ /**
185
+ * Follow a payment over WebSocket until it is paid or expired, reconnecting
186
+ * through a drop. A payment that never answers gives up after a few tries, and
187
+ * one that has answered is followed until its own expiry, so the wait always
188
+ * ends by itself
189
+ */
190
+ waitForPayment(id: string, options?: WaitOptions): Promise<Payment>;
191
+ /**
192
+ * Hand over an invoice you obtained yourself so the gateway watches it without
193
+ * being told the address or the amount. It can then only refuse everyone
194
+ * rather than one recipient, which is what makes leaving it cheap. Anything
195
+ * the watcher needs goes in `sealed`, which the gateway cannot read
196
+ */
197
+ watchPayment(params: WatchPaymentParams): Promise<TriggerEvent>;
198
+ /**
199
+ * Follow every payment made to one trigger, replayed from the recent ones on
200
+ * connect and then live, reconnecting on its own until the returned function
201
+ * is called. A trigger has no terminal state, so this never resolves
202
+ */
203
+ followTrigger(secret: string, options: FollowOptions): () => void;
204
+ private needsTicket;
205
+ private wsTicket;
206
+ private sending;
207
+ private reading;
208
+ private proven;
209
+ private checked;
210
+ }
211
+
212
+ /**
213
+ * Encrypt what the watcher needs and the gateway must not have. The gateway
214
+ * stores the result and hands it back untouched, so anything readable you put
215
+ * in `sealed` is something you told it, which is what blind mode exists to avoid
216
+ */
217
+ declare function seal(secret: string, plaintext: string): Promise<string>;
218
+ /**
219
+ * Read a sealed blob back, null when it was sealed with another secret, edited
220
+ * on the way, or is not one of ours. A secret too short to be a key throws,
221
+ * because that is your bug rather than someone else's input
222
+ */
223
+ declare function unseal(secret: string, sealed: string): Promise<string | null>;
224
+
225
+ interface TriggerConfig {
226
+ /** The gateway that quotes the addresses and mints the invoice */
227
+ gateway: ThunderBridge;
228
+ /** Priority list, quoted at payRequest and then pinned for the callback */
229
+ lnAddresses: string[];
230
+ /**
231
+ * What this trigger costs right now, called once per payRequest. A plain
232
+ * function, so a fiat peg or a time of day rule is just code you write
233
+ */
234
+ amountMsat: () => number | Promise<number>;
235
+ /**
236
+ * Signs the callback URL. Without it anyone could call the callback and make
237
+ * this endpoint mint invoices on wallets of their choosing
238
+ */
239
+ secret: string;
240
+ /** Groups every payment here so `followTrigger` can watch the place, keep it off the QR */
241
+ watchSecret?: string;
242
+ /** Override when a proxy hides the public URL from the request, no trailing slash */
243
+ baseUrl?: string;
244
+ /**
245
+ * Resolve the address here and hand the gateway only a hash and a URL to poll,
246
+ * instead of asking it to mint. It then cannot tell who is being paid beyond
247
+ * the domain in the verify URL, nor how much at all, so the only refusal left
248
+ * to it is refusing everyone. Costs one more round trip and gives up the
249
+ * gateway's CORS proxying, which a server does not need anyway
250
+ */
251
+ blind?: boolean;
252
+ /**
253
+ * What the watcher needs and the gateway must not have. `data` returns it and
254
+ * `secret` encrypts it, so there is no way to hand the gateway something it
255
+ * can read. Needs 32 characters of randomness, not a passphrase, and every
256
+ * watcher of this trigger holds the same one
257
+ */
258
+ sealed?: {
259
+ secret: string;
260
+ data: (minted: Minted) => unknown;
261
+ };
262
+ }
263
+ interface Minted {
264
+ lnAddress: string;
265
+ amountMsat: number;
266
+ bolt11: string;
267
+ paymentHash: string;
268
+ verifyUrl: string;
269
+ expiresAt: number;
270
+ }
271
+ /**
272
+ * An LNURL-pay endpoint standing in front of a priority list of addresses, as a
273
+ * Fetch handler so it runs on Deno Deploy, Workers, Hono, Next and Node alike.
274
+ *
275
+ * It answers both halves of the flow on one path. A bare request is the
276
+ * payRequest and quotes the list, and a signed one is the callback and mints.
277
+ * The winner is chosen at payRequest and pinned into the callback URL because
278
+ * LUD-06 binds the invoice to the metadata already served: if the callback
279
+ * picked a different address the payer's wallet would refuse the invoice.
280
+ *
281
+ * Nothing is stored between the two, so this holds no state of its own.
282
+ */
283
+ declare function lnurlPayEndpoint(config: TriggerConfig): (request: Request) => Promise<Response>;
284
+
285
+ /**
286
+ * Prove the invoice really is the one the recipient issued for what you asked,
287
+ * before the payer ever sees it, both fetches go straight to the recipient's own
288
+ * server and none of them goes back to the gateway
289
+ *
290
+ * Throws `GatewayCheatError` when a check fails and `UnverifiedRecipientError`
291
+ * when the recipient could not be reached to run one
292
+ */
293
+ declare function proveOrigin(payment: Payment, request: CreatePaymentParams): Promise<void>;
294
+ /**
295
+ * Prove the money arrived by asking the recipient's own server, not the gateway,
296
+ * returns the preimage when the recipient says it settled and null when it says
297
+ * it has not, and runs the full origin proof first because a verify url the
298
+ * gateway made up would otherwise answer for itself
299
+ */
300
+ declare function proveSettlement(payment: Payment, request: CreatePaymentParams): Promise<string | null>;
301
+ /**
302
+ * True when the gateway's own report of a settlement is at least self-consistent,
303
+ * the preimage hashes to the payment hash the invoice itself carries, this is a
304
+ * sanity check and not a proof, only `proveSettlement` asks the recipient
305
+ */
306
+ declare function isProvablyPaid(payment: Payment): boolean;
307
+
308
+ /** What a BOLT11 invoice says about itself, every field null when it does not carry one */
309
+ interface Invoice {
310
+ paymentHash: string | null;
311
+ descriptionHash: string | null;
312
+ amountMsat: number | null;
313
+ expiresAt: number | null;
314
+ }
315
+ /**
316
+ * Read a BOLT11 invoice without trusting anyone for its contents, an
317
+ * undecodable string or a BOLT12 offer yields an invoice with every field null
318
+ */
319
+ declare function decodeInvoice(bolt11: string): Invoice;
320
+ /** True when `preimage` is the secret behind `paymentHash` */
321
+ declare function preimageMatchesHash(preimage: string, paymentHash: string): boolean;
322
+
323
+ interface QrOptions {
324
+ /** SVG width and height in pixels, defaults to 256 */
325
+ size?: number;
326
+ /** Dark module color, defaults to `#000` */
327
+ color?: string;
328
+ }
329
+ /** Render a BOLT11 invoice or a lightning address as an SVG QR code */
330
+ declare function invoiceToSvg(destination: string, options?: QrOptions): string;
331
+ /** SVG data URL for an `<img>` `src` */
332
+ declare function invoiceToDataUrl(destination: string, options?: QrOptions): string;
333
+ /**
334
+ * Render your own LNURL-pay endpoint, the URL `lnurlPayEndpoint` is mounted on,
335
+ * as the QR a payer scans. Nothing is minted and nothing expires, so this is the
336
+ * code a tip jar prints once and an overlay shows all stream
337
+ */
338
+ declare function lnurlToSvg(endpoint: string, options?: QrOptions): string;
339
+ /** SVG data URL of the endpoint's QR, for an `<img>` `src` */
340
+ declare function lnurlToDataUrl(endpoint: string, options?: QrOptions): string;
341
+
342
+ /**
343
+ * Bech32-encode a pay endpoint as the `LNURL1` string LUD-01 defines, uppercase
344
+ * because that is the form it asks a QR to carry. An onion endpoint is http
345
+ * rather than https, which LUD-17 spells out, so both are taken here
346
+ */
347
+ declare function toLnurl(endpoint: string): string;
348
+
349
+ /** How far the gateway's clock may drift from yours before a webhook is refused */
350
+ type WebhookOptions = {
351
+ toleranceSecs?: number;
352
+ };
353
+ /** Verify the `X-Signature` header against the raw body and the `X-Timestamp` that came with it */
354
+ declare function verifyWebhookSignature(body: string | Uint8Array, signature: string, secret: string, timestamp: string, options?: WebhookOptions): Promise<boolean>;
355
+ /** Verify and parse in one step, returns null on a bad signature or a body that is not a payment */
356
+ declare function parseWebhook(body: string | Uint8Array, signature: string, secret: string, timestamp: string, options?: WebhookOptions): Promise<Payment | null>;
357
+ /**
358
+ * Verify and parse from a Fetch API `Request` as used by Hono, Next, SvelteKit,
359
+ * Cloudflare Workers and Deno, WebCrypto only so it runs anywhere fetch does
360
+ */
361
+ declare function parseWebhookRequest(request: Request, secret: string, options?: WebhookOptions): Promise<Payment | null>;
362
+
363
+ /**
364
+ * The way a gateway was caught out, every code is a check that held against the
365
+ * recipient's own server and failed against what the gateway returned
366
+ */
367
+ type GatewayCheatCode = "address_not_requested" | "hash_mismatch" | "amount_mismatch" | "description_hash_mismatch" | "verify_url_foreign" | "invoice_not_issued" | "preimage_mismatch";
368
+ /**
369
+ * Thrown when the gateway demonstrably misbehaved, the invoice it returned is
370
+ * not the one the address you asked for issued, or a settlement it reported
371
+ * carries a preimage that does not hash to the payment hash
372
+ */
373
+ declare class GatewayCheatError extends Error {
374
+ readonly code: GatewayCheatCode;
375
+ readonly paymentId: string;
376
+ constructor(code: GatewayCheatCode, paymentId: string);
377
+ }
378
+ /**
379
+ * Thrown when the recipient's own server could not be reached to check the
380
+ * invoice against, a CORS-blocked browser or a provider that is down, this is
381
+ * not proof the gateway cheated and it is not proof it did not
382
+ */
383
+ declare class UnverifiedRecipientError extends Error {
384
+ readonly lnAddress: string;
385
+ readonly paymentId: string;
386
+ constructor(lnAddress: string, paymentId: string, cause: unknown);
387
+ }
388
+ /** An RFC 9457 problem document the gateway answered with */
389
+ declare class ProblemError extends Error {
390
+ readonly type: string;
391
+ readonly title: string;
392
+ readonly status: number;
393
+ readonly detail: string | null;
394
+ constructor(problem: {
395
+ type?: string;
396
+ title?: string;
397
+ status?: number;
398
+ detail?: string;
399
+ });
400
+ }
401
+ declare const NO_WALLET_AVAILABLE = "urn:problem-type:thunder-bridge-direct:no-wallet-available";
402
+ declare const REQUEST_IN_FLIGHT = "urn:problem-type:thunder-bridge-direct:request-in-flight";
403
+ declare const IDEMPOTENCY_KEY_REUSED = "urn:problem-type:thunder-bridge-direct:idempotency-key-reused";
404
+ declare const PAYMENT_ALREADY_WATCHED = "urn:problem-type:thunder-bridge-direct:payment-already-watched";
405
+ /**
406
+ * Why an `Idempotency-Key` was refused, `request-in-flight` is the benign one and
407
+ * `key-reused` means the same key was sent for a different request
408
+ */
409
+ type IdempotencyConflict = "request-in-flight" | "key-reused";
410
+ /**
411
+ * Thrown when an `Idempotency-Key` is held by another request. On
412
+ * `request-in-flight` the first attempt is still resolving, so wait and read the
413
+ * payment back rather than retrying. `key-reused` is a bug in the caller: the key
414
+ * is bound to the addresses, amount and webhook that claimed it
415
+ */
416
+ declare class IdempotencyConflictError extends ProblemError {
417
+ readonly conflict: IdempotencyConflict;
418
+ constructor(problem: {
419
+ type?: string;
420
+ title?: string;
421
+ status?: number;
422
+ detail?: string;
423
+ }, conflict: IdempotencyConflict);
424
+ }
425
+ /** Thrown when no wallet on your list could issue a provable invoice, `wallets` says why each refused */
426
+ declare class NoWalletAvailableError extends ProblemError {
427
+ readonly wallets: WalletFailure[];
428
+ constructor(problem: {
429
+ title?: string;
430
+ status?: number;
431
+ detail?: string;
432
+ }, wallets: WalletFailure[]);
433
+ }
434
+
435
+ export { type CreateOptions, type CreatePaymentParams, type CreateQuoteParams, type FollowOptions, type GatewayCheatCode, GatewayCheatError, IDEMPOTENCY_KEY_REUSED, type IdempotencyConflict, IdempotencyConflictError, type Invoice, type Minted, NO_WALLET_AVAILABLE, NoWalletAvailableError, PAYMENT_ALREADY_WATCHED, type Payment, type PaymentStatus, ProblemError, type QrOptions, type Quote, REQUEST_IN_FLIGHT, ThunderBridge, type ThunderBridgeOptions, type TriggerConfig, type TriggerEvent, UnverifiedRecipientError, type WaitOptions, type WalletFailure, type WalletReason, type WatchPaymentParams, type WebhookOptions, decodeInvoice, invoiceToDataUrl, invoiceToSvg, isProvablyPaid, lnurlPayEndpoint, lnurlToDataUrl, lnurlToSvg, parseWebhook, parseWebhookRequest, preimageMatchesHash, proveOrigin, proveSettlement, seal, toLnurl, unseal, verifyWebhookSignature };