thunder-bridge 0.8.2 → 0.8.4

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,807 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/server.ts
31
+ var server_exports = {};
32
+ __export(server_exports, {
33
+ blindLightningRail: () => blindLightningRail,
34
+ lnurlPayEndpoint: () => lnurlPayEndpoint
35
+ });
36
+ module.exports = __toCommonJS(server_exports);
37
+
38
+ // ../core/bytes.ts
39
+ function bytesToHex(bytes) {
40
+ let hex = "";
41
+ for (const byte of bytes) hex += byte.toString(16).padStart(2, "0");
42
+ return hex;
43
+ }
44
+
45
+ // ../core/hmac.ts
46
+ async function hmacHex(secret, payload) {
47
+ const raw = typeof secret === "string" ? new TextEncoder().encode(secret) : secret;
48
+ const key = await crypto.subtle.importKey(
49
+ "raw",
50
+ asBuffer(raw),
51
+ { name: "HMAC", hash: "SHA-256" },
52
+ false,
53
+ ["sign"]
54
+ );
55
+ const message = typeof payload === "string" ? new TextEncoder().encode(payload) : payload;
56
+ return bytesToHex(new Uint8Array(await crypto.subtle.sign("HMAC", key, asBuffer(message))));
57
+ }
58
+ function equalInConstantTime(one, other) {
59
+ if (one.length !== other.length) return false;
60
+ let difference = 0;
61
+ for (let at = 0; at < one.length; at++) difference |= one.charCodeAt(at) ^ other.charCodeAt(at);
62
+ return difference === 0;
63
+ }
64
+ function asBuffer(bytes) {
65
+ return bytes instanceof Uint8Array && bytes.buffer instanceof ArrayBuffer ? bytes : new Uint8Array(bytes);
66
+ }
67
+
68
+ // ../core/sha256.ts
69
+ var ROUND_CONSTANTS = new Uint32Array([
70
+ 1116352408,
71
+ 1899447441,
72
+ 3049323471,
73
+ 3921009573,
74
+ 961987163,
75
+ 1508970993,
76
+ 2453635748,
77
+ 2870763221,
78
+ 3624381080,
79
+ 310598401,
80
+ 607225278,
81
+ 1426881987,
82
+ 1925078388,
83
+ 2162078206,
84
+ 2614888103,
85
+ 3248222580,
86
+ 3835390401,
87
+ 4022224774,
88
+ 264347078,
89
+ 604807628,
90
+ 770255983,
91
+ 1249150122,
92
+ 1555081692,
93
+ 1996064986,
94
+ 2554220882,
95
+ 2821834349,
96
+ 2952996808,
97
+ 3210313671,
98
+ 3336571891,
99
+ 3584528711,
100
+ 113926993,
101
+ 338241895,
102
+ 666307205,
103
+ 773529912,
104
+ 1294757372,
105
+ 1396182291,
106
+ 1695183700,
107
+ 1986661051,
108
+ 2177026350,
109
+ 2456956037,
110
+ 2730485921,
111
+ 2820302411,
112
+ 3259730800,
113
+ 3345764771,
114
+ 3516065817,
115
+ 3600352804,
116
+ 4094571909,
117
+ 275423344,
118
+ 430227734,
119
+ 506948616,
120
+ 659060556,
121
+ 883997877,
122
+ 958139571,
123
+ 1322822218,
124
+ 1537002063,
125
+ 1747873779,
126
+ 1955562222,
127
+ 2024104815,
128
+ 2227730452,
129
+ 2361852424,
130
+ 2428436474,
131
+ 2756734187,
132
+ 3204031479,
133
+ 3329325298
134
+ ]);
135
+ var INITIAL_STATE = new Uint32Array([
136
+ 1779033703,
137
+ 3144134277,
138
+ 1013904242,
139
+ 2773480762,
140
+ 1359893119,
141
+ 2600822924,
142
+ 528734635,
143
+ 1541459225
144
+ ]);
145
+ var BLOCK_BYTES = 64;
146
+ var LENGTH_BYTES = 8;
147
+ var SCHEDULE_WORDS = 64;
148
+ function sha256(bytes) {
149
+ const padded = withPadding(bytes);
150
+ const reader = new DataView(padded.buffer);
151
+ const state = Uint32Array.from(INITIAL_STATE);
152
+ const schedule = new Uint32Array(SCHEDULE_WORDS);
153
+ for (let block = 0; block < padded.length; block += BLOCK_BYTES) {
154
+ fillSchedule(schedule, reader, block);
155
+ compress(state, schedule);
156
+ }
157
+ return asBytes(state);
158
+ }
159
+ function sha256Hex(text2) {
160
+ return bytesToHex(sha256(new TextEncoder().encode(text2)));
161
+ }
162
+ function withPadding(bytes) {
163
+ const padded = new Uint8Array(
164
+ bytes.length + 1 + LENGTH_BYTES + BLOCK_BYTES - 1 & ~(BLOCK_BYTES - 1)
165
+ );
166
+ padded.set(bytes);
167
+ padded[bytes.length] = 128;
168
+ new DataView(padded.buffer).setUint32(padded.length - 4, bytes.length * 8, false);
169
+ return padded;
170
+ }
171
+ function fillSchedule(schedule, reader, block) {
172
+ for (let at = 0; at < 16; at++) schedule[at] = reader.getUint32(block + at * 4, false);
173
+ for (let at = 16; at < SCHEDULE_WORDS; at++) {
174
+ const recent = schedule[at - 15];
175
+ const distant = schedule[at - 2];
176
+ const mixedRecent = rotate(recent, 7) ^ rotate(recent, 18) ^ recent >>> 3;
177
+ const mixedDistant = rotate(distant, 17) ^ rotate(distant, 19) ^ distant >>> 10;
178
+ schedule[at] = schedule[at - 16] + mixedRecent + schedule[at - 7] + mixedDistant | 0;
179
+ }
180
+ }
181
+ function compress(state, schedule) {
182
+ let a = state[0];
183
+ let b = state[1];
184
+ let c = state[2];
185
+ let d = state[3];
186
+ let e = state[4];
187
+ let f = state[5];
188
+ let g = state[6];
189
+ let h = state[7];
190
+ for (let round = 0; round < SCHEDULE_WORDS; round++) {
191
+ const choose = e & f ^ ~e & g;
192
+ const majority = a & b ^ a & c ^ b & c;
193
+ const mixedE = rotate(e, 6) ^ rotate(e, 11) ^ rotate(e, 25);
194
+ const mixedA = rotate(a, 2) ^ rotate(a, 13) ^ rotate(a, 22);
195
+ const carried = h + mixedE + choose + ROUND_CONSTANTS[round] + schedule[round] | 0;
196
+ h = g;
197
+ g = f;
198
+ f = e;
199
+ e = d + carried | 0;
200
+ d = c;
201
+ c = b;
202
+ b = a;
203
+ a = carried + (mixedA + majority | 0) | 0;
204
+ }
205
+ const rolling = [a, b, c, d, e, f, g, h];
206
+ for (let at = 0; at < state.length; at++) state[at] = state[at] + rolling[at] | 0;
207
+ }
208
+ function asBytes(state) {
209
+ const digest = new Uint8Array(state.length * 4);
210
+ const writer = new DataView(digest.buffer);
211
+ for (let at = 0; at < state.length; at++) writer.setUint32(at * 4, state[at], false);
212
+ return digest;
213
+ }
214
+ function rotate(value, by) {
215
+ return value >>> by | value << 32 - by;
216
+ }
217
+
218
+ // ../core/bolt11.ts
219
+ var BECH32_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
220
+ var CHECKSUM_WORDS = 6;
221
+ var TIMESTAMP_WORDS = 7;
222
+ var SIGNATURE_WORDS = 104;
223
+ var TAGGED_FIELD_LENGTH_WORDS = 2;
224
+ var PAYMENT_HASH_TAG = 1;
225
+ var PAYMENT_HASH_WORDS = 52;
226
+ var EXPIRY_TAG = 6;
227
+ var DESCRIPTION_HASH_TAG = 23;
228
+ var DESCRIPTION_HASH_WORDS = 52;
229
+ var DEFAULT_EXPIRY_SECS = 3600;
230
+ var MSAT_PER_BTC = 1e11;
231
+ var HRP_MULTIPLIER_MSAT = {
232
+ m: MSAT_PER_BTC / 1e3,
233
+ u: MSAT_PER_BTC / 1e6,
234
+ n: MSAT_PER_BTC / 1e9,
235
+ p: MSAT_PER_BTC / 1e12
236
+ };
237
+ var HRP_AMOUNT = /^ln[a-z]+?(\d+)([munp]?)$/;
238
+ function decodeInvoice(bolt11) {
239
+ const parts = splitBech32(bolt11);
240
+ if (parts === null) {
241
+ return { paymentHash: null, descriptionHash: null, amountMsat: null, expiresAt: null };
242
+ }
243
+ const tagged = taggedFields(parts.words);
244
+ return {
245
+ paymentHash: hexTag(tagged, PAYMENT_HASH_TAG, PAYMENT_HASH_WORDS),
246
+ descriptionHash: hexTag(tagged, DESCRIPTION_HASH_TAG, DESCRIPTION_HASH_WORDS),
247
+ amountMsat: amountFromHrp(parts.hrp),
248
+ expiresAt: expiryOf(parts.words, tagged)
249
+ };
250
+ }
251
+ function splitBech32(bolt11) {
252
+ const lower = bolt11.toLowerCase();
253
+ if (isBolt12Encoding(lower)) return null;
254
+ const separator = lower.lastIndexOf("1");
255
+ if (separator < 1) return null;
256
+ const dataChars = lower.slice(separator + 1);
257
+ if (dataChars.length < CHECKSUM_WORDS) return null;
258
+ const words = [];
259
+ for (const char of dataChars) {
260
+ const value = BECH32_CHARSET.indexOf(char);
261
+ if (value === -1) return null;
262
+ words.push(value);
263
+ }
264
+ return { hrp: lower.slice(0, separator), words: words.slice(0, -CHECKSUM_WORDS) };
265
+ }
266
+ function isBolt12Encoding(lower) {
267
+ return lower.startsWith("lno") || lower.startsWith("lni") || lower.startsWith("lnr");
268
+ }
269
+ function taggedFields(words) {
270
+ const fields = /* @__PURE__ */ new Map();
271
+ const signatureStart = words.length - SIGNATURE_WORDS;
272
+ let cursor = TIMESTAMP_WORDS;
273
+ while (cursor + 1 + TAGGED_FIELD_LENGTH_WORDS <= signatureStart) {
274
+ const lengthWords = words[cursor + 1] * 32 + words[cursor + 2];
275
+ const dataStart = cursor + 1 + TAGGED_FIELD_LENGTH_WORDS;
276
+ const dataEnd = dataStart + lengthWords;
277
+ if (dataEnd > signatureStart) return fields;
278
+ if (!fields.has(words[cursor])) fields.set(words[cursor], words.slice(dataStart, dataEnd));
279
+ cursor = dataEnd;
280
+ }
281
+ return fields;
282
+ }
283
+ function hexTag(fields, tag, expectedWords) {
284
+ const words = fields.get(tag);
285
+ if (words === void 0 || words.length !== expectedWords) return null;
286
+ return bytesToHex(wordsToBytes(words));
287
+ }
288
+ function expiryOf(words, fields) {
289
+ if (words.length < TIMESTAMP_WORDS + SIGNATURE_WORDS) return null;
290
+ const expiry = fields.get(EXPIRY_TAG);
291
+ return readNumber(words.slice(0, TIMESTAMP_WORDS)) + (expiry ? readNumber(expiry) : DEFAULT_EXPIRY_SECS);
292
+ }
293
+ function amountFromHrp(hrp) {
294
+ const match = HRP_AMOUNT.exec(hrp);
295
+ if (match === null) return null;
296
+ const [, digits, multiplier] = match;
297
+ return multiplier ? Number(digits) * HRP_MULTIPLIER_MSAT[multiplier] : Number(digits) * MSAT_PER_BTC;
298
+ }
299
+ function readNumber(words) {
300
+ return words.reduce((total, word) => total * 32 + word, 0);
301
+ }
302
+ function wordsToBytes(words) {
303
+ const bytes = [];
304
+ let accumulator = 0;
305
+ let bits = 0;
306
+ for (const word of words) {
307
+ accumulator = accumulator << 5 | word;
308
+ bits += 5;
309
+ if (bits >= 8) {
310
+ bits -= 8;
311
+ bytes.push(accumulator >>> bits & 255);
312
+ }
313
+ }
314
+ return Uint8Array.from(bytes);
315
+ }
316
+
317
+ // ../core/url.ts
318
+ var PRIVATE_SUFFIXES = ["localhost", "local", "internal", "lan", "arpa", "test", "invalid"];
319
+ function publicHttps(raw) {
320
+ let url;
321
+ try {
322
+ url = new URL(raw);
323
+ } catch {
324
+ return false;
325
+ }
326
+ return url.protocol === "https:" && hostIsPublic(url.hostname.toLowerCase());
327
+ }
328
+ function sameOrigin(one, other) {
329
+ try {
330
+ return new URL(one).origin === new URL(other).origin;
331
+ } catch {
332
+ return false;
333
+ }
334
+ }
335
+ function publicAddress(literal) {
336
+ return literal.includes(":") ? ipv6IsGlobal(literal) : ipv4IsGlobal(literal);
337
+ }
338
+ function hostIsPublic(host) {
339
+ if (host.startsWith("[")) return publicAddress(host.slice(1, -1));
340
+ const name = host.replace(/\.$/, "");
341
+ if (/^\d+\.\d+\.\d+\.\d+$/.test(name)) return publicAddress(name);
342
+ const labels = name.split(".");
343
+ if (labels.length < 2 || labels.some((label) => label === "")) return false;
344
+ return !PRIVATE_SUFFIXES.includes(labels[labels.length - 1]);
345
+ }
346
+ function ipv4IsGlobal(literal) {
347
+ const parts = literal.split(".").map(Number);
348
+ if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) {
349
+ return false;
350
+ }
351
+ const [a, b] = parts;
352
+ if (a === 0 || a === 10 || a === 127 || a >= 240) return false;
353
+ if (a === 100 && b >= 64 && b < 128) return false;
354
+ if (a === 169 && b === 254) return false;
355
+ if (a === 172 && b >= 16 && b < 32) return false;
356
+ if (a === 192 && b === 168) return false;
357
+ return true;
358
+ }
359
+ function ipv6IsGlobal(literal) {
360
+ const groups = ipv6Groups(literal.toLowerCase());
361
+ if (groups.length !== 8 || groups.some(Number.isNaN)) return false;
362
+ const leading = groups.slice(0, 5).every((group) => group === 0);
363
+ if (leading && groups[5] === 0 && groups[6] === 0 && groups[7] <= 1) return false;
364
+ if (leading && groups[5] === 65535) {
365
+ const [high, low] = [groups[6], groups[7]];
366
+ return ipv4IsGlobal(`${high >> 8}.${high & 255}.${low >> 8}.${low & 255}`);
367
+ }
368
+ const first = groups[0];
369
+ if ((first & 65024) === 64512) return false;
370
+ if ((first & 65472) === 65152) return false;
371
+ return true;
372
+ }
373
+ function ipv6Groups(address) {
374
+ const [head = "", tail] = address.split("::");
375
+ const left = head.split(":").filter((group) => group !== "");
376
+ const right = (tail ?? "").split(":").filter((group) => group !== "");
377
+ const gap = tail === void 0 ? [] : Array(8 - left.length - right.length).fill("0");
378
+ return [...left, ...gap, ...right].map((group) => parseInt(group, 16));
379
+ }
380
+
381
+ // ../core/outbound.ts
382
+ var HTTP_TIMEOUT_MS = 15e3;
383
+ var LOOKUP_TIMEOUT_MS = 5e3;
384
+ var HOPS_FOLLOWED = 2;
385
+ var REDIRECTS = [301, 302, 303, 307, 308];
386
+ var KEEPS_THE_METHOD = [307, 308];
387
+ var CREDENTIALS = ["authorization", "cookie", "proxy-authorization"];
388
+ var BODY_LIMIT_BYTES = 262144;
389
+ async function ask(url, sent = {}) {
390
+ const capped = AbortSignal.timeout(HTTP_TIMEOUT_MS);
391
+ const signal = sent.deadline ? AbortSignal.any([sent.deadline, capped]) : capped;
392
+ let target = url;
393
+ let carried = sent;
394
+ for (let hop = 0; hop <= HOPS_FOLLOWED; hop += 1) {
395
+ await refuseUnlessPublic(target);
396
+ const response = await fetch(target, {
397
+ method: carried.method ?? "GET",
398
+ headers: carried.headers ?? {},
399
+ body: carried.body,
400
+ redirect: "manual",
401
+ signal
402
+ });
403
+ if (!REDIRECTS.includes(response.status)) return await answerOf(response);
404
+ const location = response.headers.get("location");
405
+ if (location === null) {
406
+ throw new Error(`${target} answered ${response.status} without saying where to`);
407
+ }
408
+ await response.body?.cancel();
409
+ const next = new URL(location, target).toString();
410
+ carried = KEEPS_THE_METHOD.includes(response.status) ? carried : withoutBody(carried);
411
+ carried = sameOrigin(target, next) ? carried : withoutCredentials(carried);
412
+ target = next;
413
+ }
414
+ throw new Error(`${url} redirected more than ${HOPS_FOLLOWED} times`);
415
+ }
416
+ async function resolvesNothingPrivate(url) {
417
+ const { lookup } = await import("dns/promises");
418
+ const { setTimeout: sleep } = await import("timers/promises");
419
+ const host = new URL(url).hostname.replace(/^\[|]$/g, "");
420
+ const found = await Promise.race([
421
+ lookup(host, { all: true, verbatim: true }).catch(() => []),
422
+ sleep(LOOKUP_TIMEOUT_MS, null, { ref: false })
423
+ ]);
424
+ return found !== null && found.every((one) => publicAddress(one.address));
425
+ }
426
+ async function refuseUnlessPublic(url) {
427
+ if (!publicHttps(url)) throw new Error(`${url} is not a public https URL`);
428
+ if (!await resolvesNothingPrivate(url)) throw new Error(`${url} resolves to an address we do not reach`);
429
+ }
430
+ async function answerOf(response) {
431
+ const { status, ok } = response;
432
+ const reader = response.body?.getReader();
433
+ if (!reader) return { status, ok, body: "", truncated: false };
434
+ const read = [];
435
+ let bytes = 0;
436
+ while (bytes <= BODY_LIMIT_BYTES) {
437
+ const { done, value } = await reader.read();
438
+ if (done) return { status, ok, body: text(read), truncated: false };
439
+ read.push(value);
440
+ bytes += value.length;
441
+ }
442
+ await reader.cancel();
443
+ return { status, ok, body: text(read), truncated: true };
444
+ }
445
+ function withoutBody(sent) {
446
+ const headers = { ...sent.headers };
447
+ delete headers["content-type"];
448
+ return { ...sent, method: "GET", headers, body: void 0 };
449
+ }
450
+ function withoutCredentials(sent) {
451
+ const headers = { ...sent.headers };
452
+ for (const named of CREDENTIALS) delete headers[named];
453
+ return { ...sent, headers };
454
+ }
455
+ function text(chunks) {
456
+ const decoder = new TextDecoder();
457
+ let decoded = "";
458
+ for (const chunk of chunks) decoded += decoder.decode(chunk, { stream: true });
459
+ return decoded + decoder.decode();
460
+ }
461
+
462
+ // ../core/refusal.ts
463
+ var WalletRefused = class extends Error {
464
+ reason;
465
+ constructor(reason, message) {
466
+ super(message);
467
+ this.reason = reason;
468
+ }
469
+ };
470
+ var NoWalletAvailable = class extends Error {
471
+ wallets;
472
+ constructor(wallets) {
473
+ super(wallets.map((wallet) => `${wallet.address} ${wallet.reason}`).join(", "));
474
+ this.wallets = wallets;
475
+ }
476
+ };
477
+
478
+ // ../core/lnurl.ts
479
+ var VERIFY_WITHOUT_PREIMAGE = ["zeuspay.com", "zeusnuts.com", "ecash.love"];
480
+ var RESOLVE_TIMEOUT_MS = 3e4;
481
+ async function resolve(addresses, amountMsat) {
482
+ const served = await firstThatServes(
483
+ addresses,
484
+ (address, deadline) => resolveAddress(address, amountMsat, deadline)
485
+ );
486
+ return served.won;
487
+ }
488
+ async function firstThatServes(addresses, attempt) {
489
+ const deadline = AbortSignal.timeout(RESOLVE_TIMEOUT_MS);
490
+ const refusals = [];
491
+ for (const address of addresses) {
492
+ if (deadline.aborted) break;
493
+ try {
494
+ return { won: await attempt(address, deadline), refusals };
495
+ } catch (refusal) {
496
+ if (!(refusal instanceof WalletRefused)) throw refusal;
497
+ console.warn(`wallet ${address} refused, ${refusal.reason}: ${refusal.message}`);
498
+ refusals.push({ address, reason: refusal.reason });
499
+ }
500
+ }
501
+ throw new NoWalletAvailable(refusals);
502
+ }
503
+ async function probe(address, amountMsat, deadline) {
504
+ const [user, domain] = splitAddress(address);
505
+ if (cannotReleaseAPreimage(domain)) {
506
+ throw new WalletRefused("cannot-prove-delivery", `${address} never releases a preimage`);
507
+ }
508
+ const pay = await answered(
509
+ `https://${domain}/.well-known/lnurlp/${user}`,
510
+ deadline
511
+ );
512
+ if (pay.tag !== "payRequest" || !pay.callback) {
513
+ throw new WalletRefused("unreachable", `${address} answered with no payRequest`);
514
+ }
515
+ if (amountMsat < pay.minSendable || amountMsat > pay.maxSendable) {
516
+ throw new WalletRefused(
517
+ "amount-not-accepted",
518
+ `${address} takes [${pay.minSendable}, ${pay.maxSendable}] msat, not ${amountMsat}`
519
+ );
520
+ }
521
+ return { address, pay };
522
+ }
523
+ async function resolveAddress(address, amountMsat, deadline) {
524
+ const { pay } = await probe(address, amountMsat, deadline);
525
+ const invoice = await answered(withAmount(pay.callback, amountMsat), deadline);
526
+ if (!invoice.pr) throw new WalletRefused("unreachable", `${address} returned no invoice`);
527
+ if (!invoice.verify || !publicHttps(invoice.verify)) {
528
+ throw new WalletRefused(
529
+ "cannot-prove-delivery",
530
+ `${address} publishes no usable LUD-21 verify URL`
531
+ );
532
+ }
533
+ const issued = decodeIssued(address, invoice.pr);
534
+ if (issued.amountMsat !== amountMsat) {
535
+ throw new WalletRefused(
536
+ "invoice-refused",
537
+ `${address} issued an invoice for ${issued.amountMsat} msat, not ${amountMsat}`
538
+ );
539
+ }
540
+ if (issued.descriptionHash !== sha256Hex(pay.metadata)) {
541
+ throw new WalletRefused(
542
+ "invoice-refused",
543
+ `${address} does not bind its invoice to the metadata it serves`
544
+ );
545
+ }
546
+ return {
547
+ address,
548
+ bolt11: invoice.pr,
549
+ verifyUrl: invoice.verify,
550
+ paymentHash: issued.paymentHash,
551
+ expiresAt: issued.expiresAt
552
+ };
553
+ }
554
+ async function answered(url, deadline) {
555
+ try {
556
+ return await fetchJson(url, deadline);
557
+ } catch (cause) {
558
+ throw new WalletRefused("unreachable", String(cause));
559
+ }
560
+ }
561
+ function decodeIssued(address, bolt11) {
562
+ const { paymentHash, descriptionHash, amountMsat, expiresAt } = decodeInvoice(bolt11);
563
+ if (paymentHash === null || expiresAt === null) {
564
+ throw new WalletRefused("invoice-refused", `${address} issued an invoice we cannot decode`);
565
+ }
566
+ return { paymentHash, descriptionHash, amountMsat, expiresAt };
567
+ }
568
+ function cannotReleaseAPreimage(host) {
569
+ const lowered = host.toLowerCase();
570
+ return VERIFY_WITHOUT_PREIMAGE.some(
571
+ (known) => lowered === known || lowered.endsWith(`.${known}`)
572
+ );
573
+ }
574
+ function withAmount(callback, amountMsat) {
575
+ const separator = callback.includes("?") ? "&" : "?";
576
+ return `${callback}${separator}amount=${amountMsat}`;
577
+ }
578
+ function splitAddress(address) {
579
+ const [user, domain, ...rest] = address.split("@");
580
+ if (!user || !domain || rest.length > 0 || !publicHttps(`https://${domain}/`)) {
581
+ throw new WalletRefused("address-unusable", `${address} is not a usable lightning address`);
582
+ }
583
+ return [user, domain];
584
+ }
585
+ async function fetchJson(url, deadline) {
586
+ const answer = await ask(url, { headers: { accept: "application/json" }, deadline });
587
+ if (!answer.ok) throw new Error(`${url} answered ${answer.status}`);
588
+ if (answer.truncated) {
589
+ throw new Error(`${url} answered with more than the ${BODY_LIMIT_BYTES} bytes we read`);
590
+ }
591
+ return JSON.parse(answer.body);
592
+ }
593
+
594
+ // ../core/sealed.ts
595
+ var VERSION = "v1";
596
+ var IV_BYTES = 12;
597
+ var MIN_SECRET_CHARS = 32;
598
+ var MAX_PLAIN_BYTES = 3e3;
599
+ var INFO = new TextEncoder().encode("thunder-bridge/sealed");
600
+ async function seal(secret, plaintext) {
601
+ const body = new TextEncoder().encode(plaintext);
602
+ if (body.length > MAX_PLAIN_BYTES) {
603
+ throw new Error(`sealed takes at most ${MAX_PLAIN_BYTES} bytes, this was ${body.length}`);
604
+ }
605
+ const key = await keyFor(secret);
606
+ const iv = crypto.getRandomValues(new Uint8Array(IV_BYTES));
607
+ const cipher = new Uint8Array(await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, body));
608
+ const joined = new Uint8Array(iv.length + cipher.length);
609
+ joined.set(iv);
610
+ joined.set(cipher, iv.length);
611
+ return `${VERSION}.${toBase64Url(joined)}`;
612
+ }
613
+ async function keyFor(secret) {
614
+ if (secret.length < MIN_SECRET_CHARS) {
615
+ throw new Error(`the sealing secret needs ${MIN_SECRET_CHARS} characters of randomness`);
616
+ }
617
+ const material = await crypto.subtle.importKey(
618
+ "raw",
619
+ new TextEncoder().encode(secret),
620
+ "HKDF",
621
+ false,
622
+ ["deriveKey"]
623
+ );
624
+ return crypto.subtle.deriveKey(
625
+ { name: "HKDF", hash: "SHA-256", salt: new Uint8Array(0), info: INFO },
626
+ material,
627
+ { name: "AES-GCM", length: 256 },
628
+ false,
629
+ ["encrypt", "decrypt"]
630
+ );
631
+ }
632
+ function toBase64Url(bytes) {
633
+ let binary = "";
634
+ for (const byte of bytes) binary += String.fromCharCode(byte);
635
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
636
+ }
637
+
638
+ // src/errors.ts
639
+ var ProblemError = class extends Error {
640
+ type;
641
+ title;
642
+ status;
643
+ detail;
644
+ constructor(problem) {
645
+ const title = problem.title ?? "Request failed";
646
+ super(problem.detail ? `${title}: ${problem.detail}` : title);
647
+ this.name = "ProblemError";
648
+ this.type = problem.type ?? "about:blank";
649
+ this.title = title;
650
+ this.status = problem.status ?? 0;
651
+ this.detail = problem.detail ?? null;
652
+ }
653
+ };
654
+ function isProblemType(problem, type) {
655
+ return problem.type === type;
656
+ }
657
+ var NO_WALLET_AVAILABLE = "urn:problem-type:thunder-bridge:no-wallet-available";
658
+ var PAYMENT_ALREADY_WATCHED = "urn:problem-type:thunder-bridge:payment-already-watched";
659
+ var NoWalletAvailableError = class extends ProblemError {
660
+ wallets;
661
+ constructor(problem, wallets) {
662
+ super({ ...problem, type: NO_WALLET_AVAILABLE });
663
+ this.name = "NoWalletAvailableError";
664
+ this.wallets = wallets;
665
+ }
666
+ };
667
+
668
+ // src/trigger.ts
669
+ var NONCE_BYTES = 16;
670
+ function lnurlPayEndpoint(config) {
671
+ return async (request) => {
672
+ const url = new URL(request.url);
673
+ const asked = url.searchParams.get("to");
674
+ try {
675
+ return asked === null ? await offer(config, url) : await mint(config, url, asked);
676
+ } catch (failure) {
677
+ return refuse(failure instanceof Error ? failure.message : "the trigger could not be served");
678
+ }
679
+ };
680
+ }
681
+ async function offer(config, url) {
682
+ const amountMsat = await config.amountMsat();
683
+ const quote = await config.gateway.createQuote({
684
+ lnAddresses: config.lnAddresses,
685
+ amountMsat
686
+ });
687
+ const nonce = randomNonce();
688
+ const callback = new URL(config.baseUrl ?? `${url.origin}${url.pathname}`);
689
+ callback.searchParams.set("to", quote.lnAddress);
690
+ callback.searchParams.set("msat", String(amountMsat));
691
+ callback.searchParams.set("n", nonce);
692
+ callback.searchParams.set("sig", await sign(config.secret, quote.lnAddress, amountMsat, nonce));
693
+ return Response.json({
694
+ tag: "payRequest",
695
+ callback: callback.toString(),
696
+ metadata: quote.metadata,
697
+ minSendable: amountMsat,
698
+ maxSendable: amountMsat
699
+ });
700
+ }
701
+ async function mint(config, url, address) {
702
+ const amountMsat = Number(url.searchParams.get("msat"));
703
+ const nonce = url.searchParams.get("n") ?? "";
704
+ const signature = url.searchParams.get("sig") ?? "";
705
+ const expected = await sign(config.secret, address, amountMsat, nonce);
706
+ if (!equalInConstantTime(signature, expected)) return refuse("this callback was not signed here");
707
+ const wanted = url.searchParams.get("amount");
708
+ if (wanted !== null && Number(wanted) !== amountMsat) {
709
+ return refuse(`this trigger costs exactly ${amountMsat} msat`);
710
+ }
711
+ const minted = config.blind ? await mintBlind(config, address, amountMsat) : await mintThroughGateway(config, address, amountMsat, nonce);
712
+ return Response.json({
713
+ status: "OK",
714
+ pr: minted.bolt11,
715
+ routes: [],
716
+ verify: minted.verifyUrl
717
+ });
718
+ }
719
+ async function mintThroughGateway(config, address, amountMsat, nonce) {
720
+ const payment = await config.gateway.createPayment(
721
+ { lnAddresses: [address], amountMsat },
722
+ { idempotencyKey: nonce, trigger: config.watchSecret }
723
+ );
724
+ return { bolt11: payment.bolt11, verifyUrl: payment.verifyUrl };
725
+ }
726
+ async function mintBlind(config, address, amountMsat) {
727
+ const resolved = await resolve([address], amountMsat);
728
+ const minted = { ...resolved, amountMsat, lnAddress: resolved.address };
729
+ const locked = config.sealed;
730
+ try {
731
+ await config.gateway.watchPayment({
732
+ paymentHash: resolved.paymentHash,
733
+ verifyUrl: resolved.verifyUrl,
734
+ expiresAt: resolved.expiresAt,
735
+ trigger: config.watchSecret,
736
+ sealed: locked ? await seal(locked.secret, JSON.stringify(locked.data(minted))) : void 0
737
+ });
738
+ } catch (refused) {
739
+ if (!alreadyWatched(refused)) throw refused;
740
+ }
741
+ return { bolt11: resolved.bolt11, verifyUrl: resolved.verifyUrl };
742
+ }
743
+ function alreadyWatched(refused) {
744
+ return refused instanceof ProblemError && isProblemType(refused, PAYMENT_ALREADY_WATCHED);
745
+ }
746
+ function sign(secret, address, amountMsat, nonce) {
747
+ return hmacHex(secret, `${address}|${amountMsat}|${nonce}`);
748
+ }
749
+ function randomNonce() {
750
+ const bytes = crypto.getRandomValues(new Uint8Array(NONCE_BYTES));
751
+ let hex = "";
752
+ for (const byte of bytes) hex += byte.toString(16).padStart(2, "0");
753
+ return hex;
754
+ }
755
+ function refuse(reason) {
756
+ return Response.json({ status: "ERROR", reason });
757
+ }
758
+
759
+ // src/qr.ts
760
+ var import_uqr = require("uqr");
761
+ function encodeForQr(destination) {
762
+ const scanned = isLnAddress(destination) ? destination : destination.toUpperCase();
763
+ return `LIGHTNING:${scanned}`;
764
+ }
765
+ function isLnAddress(destination) {
766
+ const at = destination.indexOf("@");
767
+ return at > 0 && destination.slice(at + 1).includes(".");
768
+ }
769
+
770
+ // src/rail.ts
771
+ var LIGHTNING = "lightning";
772
+ function blindLightningRail(config) {
773
+ return async (order) => {
774
+ const resolved = await invoiceFrom(config.lnAddresses, await config.amountMsat(order));
775
+ const watched = await config.gateway.watchPayment({
776
+ paymentHash: resolved.paymentHash,
777
+ verifyUrl: resolved.verifyUrl,
778
+ expiresAt: resolved.expiresAt,
779
+ trigger: config.trigger,
780
+ sealed: await config.sealed?.(order),
781
+ webhookUrl: config.webhookUrl,
782
+ webhookSecret: config.webhookSecret
783
+ });
784
+ return {
785
+ id: watched.id,
786
+ rail: config.name ?? LIGHTNING,
787
+ scan: resolved.bolt11,
788
+ qr: encodeForQr(resolved.bolt11),
789
+ expiresAt: resolved.expiresAt
790
+ };
791
+ };
792
+ }
793
+ async function invoiceFrom(lnAddresses, amountMsat) {
794
+ try {
795
+ return await resolve(lnAddresses, amountMsat);
796
+ } catch (refused) {
797
+ if (refused instanceof NoWalletAvailable) {
798
+ throw new NoWalletAvailableError({ title: refused.message }, refused.wallets);
799
+ }
800
+ throw refused;
801
+ }
802
+ }
803
+ // Annotate the CommonJS export names for ESM import in node:
804
+ 0 && (module.exports = {
805
+ blindLightningRail,
806
+ lnurlPayEndpoint
807
+ });