thunder-bridge 1.5.0 → 2.1.1

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/dist/nwc.cjs CHANGED
@@ -206,6 +206,7 @@ function rotate(value, by) {
206
206
 
207
207
  // ../core/bolt11.ts
208
208
  var BECH32_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
209
+ var BECH32_GENERATOR = [996825010, 642813549, 513874426, 1027748829, 705979059];
209
210
  var CHECKSUM_WORDS = 6;
210
211
  var TIMESTAMP_WORDS = 7;
211
212
  var SIGNATURE_WORDS = 104;
@@ -223,18 +224,27 @@ var HRP_MULTIPLIER_MSAT = {
223
224
  n: MSAT_PER_BTC / 1e9,
224
225
  p: MSAT_PER_BTC / 1e12
225
226
  };
226
- var HRP_AMOUNT = /^ln[a-z]+?(\d+)([munp]?)$/;
227
+ var HRP = /^ln(?:bcrt|bc|tbs|tb|sb)(?:(\d+)([munp]?))?$/;
227
228
  function decodeInvoice(bolt11) {
228
229
  const parts = splitBech32(bolt11);
229
230
  if (parts === null) {
230
- return { paymentHash: null, descriptionHash: null, amountMsat: null, expiresAt: null };
231
+ return {
232
+ paymentHash: null,
233
+ descriptionHash: null,
234
+ amountMsat: null,
235
+ issuedAt: null,
236
+ expiresAt: null
237
+ };
231
238
  }
232
239
  const tagged = taggedFields(parts.words);
240
+ const issuedAt = issuedAtOf(parts.words);
241
+ const expiry = tagged.get(EXPIRY_TAG);
233
242
  return {
234
243
  paymentHash: hexTag(tagged, PAYMENT_HASH_TAG, PAYMENT_HASH_WORDS),
235
244
  descriptionHash: hexTag(tagged, DESCRIPTION_HASH_TAG, DESCRIPTION_HASH_WORDS),
236
245
  amountMsat: amountFromHrp(parts.hrp),
237
- expiresAt: expiryOf(parts.words, tagged)
246
+ issuedAt,
247
+ expiresAt: issuedAt === null ? null : issuedAt + (expiry ? readNumber(expiry) : DEFAULT_EXPIRY_SECS)
238
248
  };
239
249
  }
240
250
  function preimageMatchesHash(preimage, paymentHash) {
@@ -245,7 +255,7 @@ function preimageMatchesHash(preimage, paymentHash) {
245
255
  }
246
256
  function splitBech32(bolt11) {
247
257
  const lower = bolt11.toLowerCase();
248
- if (isBolt12Encoding(lower)) {
258
+ if (isBolt12Encoding(lower) || bolt11 !== lower && bolt11 !== bolt11.toUpperCase()) {
249
259
  return null;
250
260
  }
251
261
  const separator = lower.lastIndexOf("1");
@@ -264,7 +274,28 @@ function splitBech32(bolt11) {
264
274
  }
265
275
  words.push(value);
266
276
  }
267
- return { hrp: lower.slice(0, separator), words: words.slice(0, -CHECKSUM_WORDS) };
277
+ const hrp = lower.slice(0, separator);
278
+ if (!HRP.test(hrp) || bech32Polymod([...expandedHrp(hrp), ...words]) !== 1) {
279
+ return null;
280
+ }
281
+ return { hrp, words: words.slice(0, -CHECKSUM_WORDS) };
282
+ }
283
+ function expandedHrp(hrp) {
284
+ const codes = [...hrp].map((char) => char.charCodeAt(0));
285
+ return [...codes.map((code) => code >> 5), 0, ...codes.map((code) => code & 31)];
286
+ }
287
+ function bech32Polymod(values) {
288
+ let checksum = 1;
289
+ for (const value of values) {
290
+ const top = checksum >> 25;
291
+ checksum = (checksum & 33554431) << 5 ^ value;
292
+ for (let bit = 0; bit < 5; bit++) {
293
+ if (top >> bit & 1) {
294
+ checksum ^= BECH32_GENERATOR[bit];
295
+ }
296
+ }
297
+ }
298
+ return checksum;
268
299
  }
269
300
  function isBolt12Encoding(lower) {
270
301
  return lower.startsWith("lno") || lower.startsWith("lni") || lower.startsWith("lnr");
@@ -294,19 +325,17 @@ function hexTag(fields, tag, expectedWords) {
294
325
  }
295
326
  return bytesToHex(wordsToBytes(words));
296
327
  }
297
- function expiryOf(words, fields) {
328
+ function issuedAtOf(words) {
298
329
  if (words.length < TIMESTAMP_WORDS + SIGNATURE_WORDS) {
299
330
  return null;
300
331
  }
301
- const expiry = fields.get(EXPIRY_TAG);
302
- return readNumber(words.slice(0, TIMESTAMP_WORDS)) + (expiry ? readNumber(expiry) : DEFAULT_EXPIRY_SECS);
332
+ return readNumber(words.slice(0, TIMESTAMP_WORDS));
303
333
  }
304
334
  function amountFromHrp(hrp) {
305
- const match = HRP_AMOUNT.exec(hrp);
306
- if (match === null) {
335
+ const [, digits, multiplier] = HRP.exec(hrp) ?? [];
336
+ if (digits === void 0 || multiplier === "p" && !digits.endsWith("0")) {
307
337
  return null;
308
338
  }
309
- const [, digits, multiplier] = match;
310
339
  return multiplier ? Number(digits) * HRP_MULTIPLIER_MSAT[multiplier] : Number(digits) * MSAT_PER_BTC;
311
340
  }
312
341
  function readNumber(words) {
@@ -337,27 +366,38 @@ var WalletRefused = class extends Error {
337
366
  };
338
367
 
339
368
  // ../core/sealed.ts
340
- var VERSION = "v1";
369
+ var VERSION = "v2";
370
+ var UNPADDED_VERSION = "v1";
341
371
  var IV_BYTES = 12;
342
372
  var MIN_SECRET_CHARS = 32;
343
373
  var MAX_PLAIN_BYTES = 3e3;
374
+ var SIZE_CLASSES = [256, 1024, MAX_PLAIN_BYTES];
375
+ var END_OF_TEXT = 128;
344
376
  var INFO = new TextEncoder().encode("thunder-bridge/sealed");
345
- async function seal(secret, plaintext) {
346
- const body = new TextEncoder().encode(plaintext);
347
- if (body.length > MAX_PLAIN_BYTES) {
348
- throw new Error(`sealed takes at most ${MAX_PLAIN_BYTES} bytes, this was ${body.length}`);
349
- }
377
+ async function seal(secret, plaintext, paymentHash) {
378
+ return await sealUnder(
379
+ secret,
380
+ plaintext,
381
+ crypto.getRandomValues(new Uint8Array(IV_BYTES)),
382
+ boundTo(paymentHash)
383
+ );
384
+ }
385
+ async function sealUnder(secret, plaintext, iv, additionalData) {
386
+ const body = padToClass(plaintext);
350
387
  const key = await keyFor(secret);
351
- const iv = crypto.getRandomValues(new Uint8Array(IV_BYTES));
352
- const cipher = new Uint8Array(await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, body));
388
+ const cipher = new Uint8Array(
389
+ await crypto.subtle.encrypt({ name: "AES-GCM", iv, additionalData }, key, body)
390
+ );
353
391
  const joined2 = new Uint8Array(iv.length + cipher.length);
354
392
  joined2.set(iv);
355
393
  joined2.set(cipher, iv.length);
356
394
  return `${VERSION}.${toBase64Url(joined2)}`;
357
395
  }
358
- async function unseal(secret, sealed) {
396
+ async function unseal(secret, sealed, paymentHash) {
359
397
  const key = await keyFor(secret);
360
- if (!sealed.startsWith(`${VERSION}.`)) {
398
+ const padded2 = sealed.startsWith(`${VERSION}.`);
399
+ const unbound = !padded2 && sealed.startsWith(`${UNPADDED_VERSION}.`);
400
+ if (!padded2 && (!unbound || paymentHash !== void 0)) {
361
401
  return null;
362
402
  }
363
403
  const joined2 = fromBase64Url(sealed.slice(VERSION.length + 1));
@@ -365,20 +405,50 @@ async function unseal(secret, sealed) {
365
405
  return null;
366
406
  }
367
407
  try {
368
- const body = await crypto.subtle.decrypt(
369
- { name: "AES-GCM", iv: joined2.slice(0, IV_BYTES) },
370
- key,
371
- joined2.slice(IV_BYTES)
408
+ const body = new Uint8Array(
409
+ await crypto.subtle.decrypt(
410
+ {
411
+ name: "AES-GCM",
412
+ iv: joined2.slice(0, IV_BYTES),
413
+ additionalData: padded2 ? boundTo(paymentHash) : void 0
414
+ },
415
+ key,
416
+ joined2.slice(IV_BYTES)
417
+ )
372
418
  );
373
- return new TextDecoder().decode(body);
419
+ const text = padded2 ? unpad(body) : body;
420
+ return text === null ? null : new TextDecoder().decode(text);
374
421
  } catch {
375
422
  return null;
376
423
  }
377
424
  }
378
- async function keyFor(secret) {
425
+ function refuseAWeakSecret(secret) {
379
426
  if (secret.length < MIN_SECRET_CHARS) {
380
427
  throw new Error(`the sealing secret needs ${MIN_SECRET_CHARS} characters of randomness`);
381
428
  }
429
+ }
430
+ function boundTo(paymentHash) {
431
+ return new TextEncoder().encode(
432
+ paymentHash === void 0 ? VERSION : `${VERSION}|${paymentHash.toLowerCase()}`
433
+ );
434
+ }
435
+ function padToClass(plaintext) {
436
+ const text = new TextEncoder().encode(plaintext);
437
+ const size = SIZE_CLASSES.find((limit) => text.length <= limit);
438
+ if (size === void 0) {
439
+ throw new Error(`sealed takes at most ${MAX_PLAIN_BYTES} bytes, this was ${text.length}`);
440
+ }
441
+ const body = new Uint8Array(size + 1);
442
+ body.set(text);
443
+ body[text.length] = END_OF_TEXT;
444
+ return body;
445
+ }
446
+ function unpad(body) {
447
+ const end = body.lastIndexOf(END_OF_TEXT);
448
+ return end === -1 ? null : body.subarray(0, end);
449
+ }
450
+ async function keyFor(secret) {
451
+ refuseAWeakSecret(secret);
382
452
  const material = await crypto.subtle.importKey(
383
453
  "raw",
384
454
  new TextEncoder().encode(secret),
@@ -421,7 +491,7 @@ function fromBase64Url(text) {
421
491
  var import_chacha = require("@noble/ciphers/chacha.js");
422
492
  var import_secp256k1 = require("@noble/curves/secp256k1.js");
423
493
  var import_hkdf = require("@noble/hashes/hkdf.js");
424
- var import_hmac = require("@noble/hashes/hmac.js");
494
+ var import_hmac2 = require("@noble/hashes/hmac.js");
425
495
  var import_sha2 = require("@noble/hashes/sha2.js");
426
496
  var NIP44_SALT = new TextEncoder().encode("nip44-v2");
427
497
  var NIP44_VERSION = 2;
@@ -479,7 +549,7 @@ function encryptNip44(conversationKey, plaintext) {
479
549
  function sealNip44(conversationKey, plaintext, nonce) {
480
550
  const keys = messageKeysFrom(conversationKey, nonce);
481
551
  const ciphertext = (0, import_chacha.chacha20)(keys.chachaKey, keys.chachaNonce, padded(plaintext));
482
- const mac = (0, import_hmac.hmac)(import_sha2.sha256, keys.hmacKey, joined(nonce, ciphertext));
552
+ const mac = (0, import_hmac2.hmac)(import_sha2.sha256, keys.hmacKey, joined(nonce, ciphertext));
483
553
  return toBase64(joined(Uint8Array.of(NIP44_VERSION), nonce, ciphertext, mac));
484
554
  }
485
555
  function decryptNip44(conversationKey, payload) {
@@ -497,7 +567,7 @@ function decryptNip44(conversationKey, payload) {
497
567
  const ciphertext = raw.subarray(1 + NONCE_BYTES, raw.length - MAC_BYTES);
498
568
  const mac = raw.subarray(raw.length - MAC_BYTES);
499
569
  const keys = messageKeysFrom(conversationKey, nonce);
500
- if (!sameBytes((0, import_hmac.hmac)(import_sha2.sha256, keys.hmacKey, joined(nonce, ciphertext)), mac)) {
570
+ if (!sameBytes((0, import_hmac2.hmac)(import_sha2.sha256, keys.hmacKey, joined(nonce, ciphertext)), mac)) {
501
571
  throw new Error("nip44 payload failed its mac, so it was not written with this key");
502
572
  }
503
573
  return unpadded((0, import_chacha.chacha20)(keys.chachaKey, keys.chachaNonce, ciphertext));
@@ -796,6 +866,15 @@ async function amountNow(amount) {
796
866
 
797
867
  // ../core/ed25519.ts
798
868
  var PKCS8_HEADER = hexToBytes("302e020100300506032b657004220420");
869
+ var SMALL_ORDER = [
870
+ "00".repeat(32),
871
+ `01${"00".repeat(31)}`,
872
+ "26e8958fc2b227b045c3f489f2ef98f0d5dfac05d3c63339b13802886d53fc05",
873
+ "c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac037a",
874
+ `ec${"ff".repeat(30)}7f`,
875
+ `ed${"ff".repeat(30)}7f`,
876
+ `ee${"ff".repeat(30)}7f`
877
+ ];
799
878
 
800
879
  // src/webhook.ts
801
880
  var VERIFY_CHALLENGE = "verify-challenge";
@@ -1126,11 +1205,16 @@ function nwcRail(gateway, config) {
1126
1205
  expiresAt: invoice.expiresAt,
1127
1206
  trigger: config.trigger,
1128
1207
  replay: config.replay,
1129
- sealed: await config.sealed?.(order),
1208
+ sealed: config.sealed ? await seal(
1209
+ config.sealed.secret,
1210
+ JSON.stringify(config.sealed.data(order)),
1211
+ invoice.paymentHash
1212
+ ) : void 0,
1130
1213
  webhookUrl: config.webhookUrl
1131
1214
  });
1132
1215
  return {
1133
1216
  id: watched.id,
1217
+ paymentHash: invoice.paymentHash,
1134
1218
  rail: config.name ?? "lightning",
1135
1219
  scan: invoice.bolt11,
1136
1220
  qr: toLightningUri(invoice.bolt11),
package/dist/nwc.d.cts CHANGED
@@ -1,7 +1,6 @@
1
- import { A as Amount, T as Ticker } from './types-BNPmVnA7.cjs';
2
- import { R as RailConfig, O as Order, T as ThunderBridge, a as Rail } from './client-CzGZcByI.cjs';
1
+ import { A as Amount, T as Ticker } from './types-DYZ9EkmJ.cjs';
2
+ import { R as RailConfig, O as Order, T as ThunderBridge, f as Rail } from './bank-CEIuNhV_.cjs';
3
3
  import './qr-CF-YeXU1.cjs';
4
- import './bank-B3mHISn1.cjs';
5
4
 
6
5
  /** A wallet reachable over NIP-47, as its `nostr+walletconnect://` URI describes it */
7
6
  interface NwcConnection {
@@ -104,7 +103,10 @@ interface NwcRailConfig extends RailConfig {
104
103
  /** What the payer's wallet shows, the order's reference by default */
105
104
  description?: (order: Order) => string;
106
105
  /** Sealed before the gateway sees it, the way the blind Lightning rail does */
107
- sealed?: (order: Order) => string | Promise<string>;
106
+ sealed?: {
107
+ secret: string;
108
+ data: (order: Order) => unknown;
109
+ };
108
110
  }
109
111
  /**
110
112
  * Sell for Lightning against a wallet of your own over NIP-47, for a wallet that
package/dist/nwc.d.ts CHANGED
@@ -1,7 +1,6 @@
1
- import { A as Amount, T as Ticker } from './types-BNPmVnA7.js';
2
- import { R as RailConfig, O as Order, T as ThunderBridge, a as Rail } from './client-Cc5gtjGV.js';
1
+ import { A as Amount, T as Ticker } from './types-DYZ9EkmJ.js';
2
+ import { R as RailConfig, O as Order, T as ThunderBridge, f as Rail } from './bank-M8flcMks.js';
3
3
  import './qr-CF-YeXU1.js';
4
- import './bank-B3mHISn1.js';
5
4
 
6
5
  /** A wallet reachable over NIP-47, as its `nostr+walletconnect://` URI describes it */
7
6
  interface NwcConnection {
@@ -104,7 +103,10 @@ interface NwcRailConfig extends RailConfig {
104
103
  /** What the payer's wallet shows, the order's reference by default */
105
104
  description?: (order: Order) => string;
106
105
  /** Sealed before the gateway sees it, the way the blind Lightning rail does */
107
- sealed?: (order: Order) => string | Promise<string>;
106
+ sealed?: {
107
+ secret: string;
108
+ data: (order: Order) => unknown;
109
+ };
108
110
  }
109
111
  /**
110
112
  * Sell for Lightning against a wallet of your own over NIP-47, for a wallet that
package/dist/nwc.js CHANGED
@@ -172,6 +172,7 @@ function rotate(value, by) {
172
172
 
173
173
  // ../core/bolt11.ts
174
174
  var BECH32_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
175
+ var BECH32_GENERATOR = [996825010, 642813549, 513874426, 1027748829, 705979059];
175
176
  var CHECKSUM_WORDS = 6;
176
177
  var TIMESTAMP_WORDS = 7;
177
178
  var SIGNATURE_WORDS = 104;
@@ -189,18 +190,27 @@ var HRP_MULTIPLIER_MSAT = {
189
190
  n: MSAT_PER_BTC / 1e9,
190
191
  p: MSAT_PER_BTC / 1e12
191
192
  };
192
- var HRP_AMOUNT = /^ln[a-z]+?(\d+)([munp]?)$/;
193
+ var HRP = /^ln(?:bcrt|bc|tbs|tb|sb)(?:(\d+)([munp]?))?$/;
193
194
  function decodeInvoice(bolt11) {
194
195
  const parts = splitBech32(bolt11);
195
196
  if (parts === null) {
196
- return { paymentHash: null, descriptionHash: null, amountMsat: null, expiresAt: null };
197
+ return {
198
+ paymentHash: null,
199
+ descriptionHash: null,
200
+ amountMsat: null,
201
+ issuedAt: null,
202
+ expiresAt: null
203
+ };
197
204
  }
198
205
  const tagged = taggedFields(parts.words);
206
+ const issuedAt = issuedAtOf(parts.words);
207
+ const expiry = tagged.get(EXPIRY_TAG);
199
208
  return {
200
209
  paymentHash: hexTag(tagged, PAYMENT_HASH_TAG, PAYMENT_HASH_WORDS),
201
210
  descriptionHash: hexTag(tagged, DESCRIPTION_HASH_TAG, DESCRIPTION_HASH_WORDS),
202
211
  amountMsat: amountFromHrp(parts.hrp),
203
- expiresAt: expiryOf(parts.words, tagged)
212
+ issuedAt,
213
+ expiresAt: issuedAt === null ? null : issuedAt + (expiry ? readNumber(expiry) : DEFAULT_EXPIRY_SECS)
204
214
  };
205
215
  }
206
216
  function preimageMatchesHash(preimage, paymentHash) {
@@ -211,7 +221,7 @@ function preimageMatchesHash(preimage, paymentHash) {
211
221
  }
212
222
  function splitBech32(bolt11) {
213
223
  const lower = bolt11.toLowerCase();
214
- if (isBolt12Encoding(lower)) {
224
+ if (isBolt12Encoding(lower) || bolt11 !== lower && bolt11 !== bolt11.toUpperCase()) {
215
225
  return null;
216
226
  }
217
227
  const separator = lower.lastIndexOf("1");
@@ -230,7 +240,28 @@ function splitBech32(bolt11) {
230
240
  }
231
241
  words.push(value);
232
242
  }
233
- return { hrp: lower.slice(0, separator), words: words.slice(0, -CHECKSUM_WORDS) };
243
+ const hrp = lower.slice(0, separator);
244
+ if (!HRP.test(hrp) || bech32Polymod([...expandedHrp(hrp), ...words]) !== 1) {
245
+ return null;
246
+ }
247
+ return { hrp, words: words.slice(0, -CHECKSUM_WORDS) };
248
+ }
249
+ function expandedHrp(hrp) {
250
+ const codes = [...hrp].map((char) => char.charCodeAt(0));
251
+ return [...codes.map((code) => code >> 5), 0, ...codes.map((code) => code & 31)];
252
+ }
253
+ function bech32Polymod(values) {
254
+ let checksum = 1;
255
+ for (const value of values) {
256
+ const top = checksum >> 25;
257
+ checksum = (checksum & 33554431) << 5 ^ value;
258
+ for (let bit = 0; bit < 5; bit++) {
259
+ if (top >> bit & 1) {
260
+ checksum ^= BECH32_GENERATOR[bit];
261
+ }
262
+ }
263
+ }
264
+ return checksum;
234
265
  }
235
266
  function isBolt12Encoding(lower) {
236
267
  return lower.startsWith("lno") || lower.startsWith("lni") || lower.startsWith("lnr");
@@ -260,19 +291,17 @@ function hexTag(fields, tag, expectedWords) {
260
291
  }
261
292
  return bytesToHex(wordsToBytes(words));
262
293
  }
263
- function expiryOf(words, fields) {
294
+ function issuedAtOf(words) {
264
295
  if (words.length < TIMESTAMP_WORDS + SIGNATURE_WORDS) {
265
296
  return null;
266
297
  }
267
- const expiry = fields.get(EXPIRY_TAG);
268
- return readNumber(words.slice(0, TIMESTAMP_WORDS)) + (expiry ? readNumber(expiry) : DEFAULT_EXPIRY_SECS);
298
+ return readNumber(words.slice(0, TIMESTAMP_WORDS));
269
299
  }
270
300
  function amountFromHrp(hrp) {
271
- const match = HRP_AMOUNT.exec(hrp);
272
- if (match === null) {
301
+ const [, digits, multiplier] = HRP.exec(hrp) ?? [];
302
+ if (digits === void 0 || multiplier === "p" && !digits.endsWith("0")) {
273
303
  return null;
274
304
  }
275
- const [, digits, multiplier] = match;
276
305
  return multiplier ? Number(digits) * HRP_MULTIPLIER_MSAT[multiplier] : Number(digits) * MSAT_PER_BTC;
277
306
  }
278
307
  function readNumber(words) {
@@ -303,27 +332,38 @@ var WalletRefused = class extends Error {
303
332
  };
304
333
 
305
334
  // ../core/sealed.ts
306
- var VERSION = "v1";
335
+ var VERSION = "v2";
336
+ var UNPADDED_VERSION = "v1";
307
337
  var IV_BYTES = 12;
308
338
  var MIN_SECRET_CHARS = 32;
309
339
  var MAX_PLAIN_BYTES = 3e3;
340
+ var SIZE_CLASSES = [256, 1024, MAX_PLAIN_BYTES];
341
+ var END_OF_TEXT = 128;
310
342
  var INFO = new TextEncoder().encode("thunder-bridge/sealed");
311
- async function seal(secret, plaintext) {
312
- const body = new TextEncoder().encode(plaintext);
313
- if (body.length > MAX_PLAIN_BYTES) {
314
- throw new Error(`sealed takes at most ${MAX_PLAIN_BYTES} bytes, this was ${body.length}`);
315
- }
343
+ async function seal(secret, plaintext, paymentHash) {
344
+ return await sealUnder(
345
+ secret,
346
+ plaintext,
347
+ crypto.getRandomValues(new Uint8Array(IV_BYTES)),
348
+ boundTo(paymentHash)
349
+ );
350
+ }
351
+ async function sealUnder(secret, plaintext, iv, additionalData) {
352
+ const body = padToClass(plaintext);
316
353
  const key = await keyFor(secret);
317
- const iv = crypto.getRandomValues(new Uint8Array(IV_BYTES));
318
- const cipher = new Uint8Array(await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, body));
354
+ const cipher = new Uint8Array(
355
+ await crypto.subtle.encrypt({ name: "AES-GCM", iv, additionalData }, key, body)
356
+ );
319
357
  const joined2 = new Uint8Array(iv.length + cipher.length);
320
358
  joined2.set(iv);
321
359
  joined2.set(cipher, iv.length);
322
360
  return `${VERSION}.${toBase64Url(joined2)}`;
323
361
  }
324
- async function unseal(secret, sealed) {
362
+ async function unseal(secret, sealed, paymentHash) {
325
363
  const key = await keyFor(secret);
326
- if (!sealed.startsWith(`${VERSION}.`)) {
364
+ const padded2 = sealed.startsWith(`${VERSION}.`);
365
+ const unbound = !padded2 && sealed.startsWith(`${UNPADDED_VERSION}.`);
366
+ if (!padded2 && (!unbound || paymentHash !== void 0)) {
327
367
  return null;
328
368
  }
329
369
  const joined2 = fromBase64Url(sealed.slice(VERSION.length + 1));
@@ -331,20 +371,50 @@ async function unseal(secret, sealed) {
331
371
  return null;
332
372
  }
333
373
  try {
334
- const body = await crypto.subtle.decrypt(
335
- { name: "AES-GCM", iv: joined2.slice(0, IV_BYTES) },
336
- key,
337
- joined2.slice(IV_BYTES)
374
+ const body = new Uint8Array(
375
+ await crypto.subtle.decrypt(
376
+ {
377
+ name: "AES-GCM",
378
+ iv: joined2.slice(0, IV_BYTES),
379
+ additionalData: padded2 ? boundTo(paymentHash) : void 0
380
+ },
381
+ key,
382
+ joined2.slice(IV_BYTES)
383
+ )
338
384
  );
339
- return new TextDecoder().decode(body);
385
+ const text = padded2 ? unpad(body) : body;
386
+ return text === null ? null : new TextDecoder().decode(text);
340
387
  } catch {
341
388
  return null;
342
389
  }
343
390
  }
344
- async function keyFor(secret) {
391
+ function refuseAWeakSecret(secret) {
345
392
  if (secret.length < MIN_SECRET_CHARS) {
346
393
  throw new Error(`the sealing secret needs ${MIN_SECRET_CHARS} characters of randomness`);
347
394
  }
395
+ }
396
+ function boundTo(paymentHash) {
397
+ return new TextEncoder().encode(
398
+ paymentHash === void 0 ? VERSION : `${VERSION}|${paymentHash.toLowerCase()}`
399
+ );
400
+ }
401
+ function padToClass(plaintext) {
402
+ const text = new TextEncoder().encode(plaintext);
403
+ const size = SIZE_CLASSES.find((limit) => text.length <= limit);
404
+ if (size === void 0) {
405
+ throw new Error(`sealed takes at most ${MAX_PLAIN_BYTES} bytes, this was ${text.length}`);
406
+ }
407
+ const body = new Uint8Array(size + 1);
408
+ body.set(text);
409
+ body[text.length] = END_OF_TEXT;
410
+ return body;
411
+ }
412
+ function unpad(body) {
413
+ const end = body.lastIndexOf(END_OF_TEXT);
414
+ return end === -1 ? null : body.subarray(0, end);
415
+ }
416
+ async function keyFor(secret) {
417
+ refuseAWeakSecret(secret);
348
418
  const material = await crypto.subtle.importKey(
349
419
  "raw",
350
420
  new TextEncoder().encode(secret),
@@ -762,6 +832,15 @@ async function amountNow(amount) {
762
832
 
763
833
  // ../core/ed25519.ts
764
834
  var PKCS8_HEADER = hexToBytes("302e020100300506032b657004220420");
835
+ var SMALL_ORDER = [
836
+ "00".repeat(32),
837
+ `01${"00".repeat(31)}`,
838
+ "26e8958fc2b227b045c3f489f2ef98f0d5dfac05d3c63339b13802886d53fc05",
839
+ "c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac037a",
840
+ `ec${"ff".repeat(30)}7f`,
841
+ `ed${"ff".repeat(30)}7f`,
842
+ `ee${"ff".repeat(30)}7f`
843
+ ];
765
844
 
766
845
  // src/webhook.ts
767
846
  var VERIFY_CHALLENGE = "verify-challenge";
@@ -1092,11 +1171,16 @@ function nwcRail(gateway, config) {
1092
1171
  expiresAt: invoice.expiresAt,
1093
1172
  trigger: config.trigger,
1094
1173
  replay: config.replay,
1095
- sealed: await config.sealed?.(order),
1174
+ sealed: config.sealed ? await seal(
1175
+ config.sealed.secret,
1176
+ JSON.stringify(config.sealed.data(order)),
1177
+ invoice.paymentHash
1178
+ ) : void 0,
1096
1179
  webhookUrl: config.webhookUrl
1097
1180
  });
1098
1181
  return {
1099
1182
  id: watched.id,
1183
+ paymentHash: invoice.paymentHash,
1100
1184
  rail: config.name ?? "lightning",
1101
1185
  scan: invoice.bolt11,
1102
1186
  qr: toLightningUri(invoice.bolt11),
package/dist/price.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- export { A as AmountError, a as AmountFault } from './errors-Dmh-Uoh8.cjs';
2
- export { M as MedianOptions, T as Ticker, b as bitstamp, c as coinbase, a as coinmate, k as kraken, m as medianOf, d as msatFor } from './types-BNPmVnA7.cjs';
1
+ export { A as AmountError, a as AmountFault } from './errors-DriqW-lp.cjs';
2
+ export { M as MedianOptions, T as Ticker, b as bitstamp, c as coinbase, a as coinmate, k as kraken, m as medianOf, d as msatFor } from './types-DYZ9EkmJ.cjs';
3
3
 
4
4
  /**
5
5
  * How many digits ISO 4217 gives the currency's minor unit, so 2 for a crown and a
package/dist/price.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- export { A as AmountError, a as AmountFault } from './errors-0vbVoISA.js';
2
- export { M as MedianOptions, T as Ticker, b as bitstamp, c as coinbase, a as coinmate, k as kraken, m as medianOf, d as msatFor } from './types-BNPmVnA7.js';
1
+ export { A as AmountError, a as AmountFault } from './errors-ZEUT-MhO.js';
2
+ export { M as MedianOptions, T as Ticker, b as bitstamp, c as coinbase, a as coinmate, k as kraken, m as medianOf, d as msatFor } from './types-DYZ9EkmJ.js';
3
3
 
4
4
  /**
5
5
  * How many digits ISO 4217 gives the currency's minor unit, so 2 for a crown and a
package/dist/qr.cjs CHANGED
@@ -111,6 +111,8 @@ var INITIAL_STATE = new Uint32Array([
111
111
  ]);
112
112
 
113
113
  // ../core/bolt11.ts
114
+ var BECH32_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
115
+ var BECH32_GENERATOR = [996825010, 642813549, 513874426, 1027748829, 705979059];
114
116
  var MSAT_PER_BTC = 1e11;
115
117
  var HRP_MULTIPLIER_MSAT = {
116
118
  m: MSAT_PER_BTC / 1e3,
@@ -118,10 +120,25 @@ var HRP_MULTIPLIER_MSAT = {
118
120
  n: MSAT_PER_BTC / 1e9,
119
121
  p: MSAT_PER_BTC / 1e12
120
122
  };
123
+ function expandedHrp(hrp) {
124
+ const codes = [...hrp].map((char) => char.charCodeAt(0));
125
+ return [...codes.map((code) => code >> 5), 0, ...codes.map((code) => code & 31)];
126
+ }
127
+ function bech32Polymod(values) {
128
+ let checksum = 1;
129
+ for (const value of values) {
130
+ const top = checksum >> 25;
131
+ checksum = (checksum & 33554431) << 5 ^ value;
132
+ for (let bit = 0; bit < 5; bit++) {
133
+ if (top >> bit & 1) {
134
+ checksum ^= BECH32_GENERATOR[bit];
135
+ }
136
+ }
137
+ }
138
+ return checksum;
139
+ }
121
140
 
122
141
  // ../core/lnurl.ts
123
- var BECH32_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
124
- var BECH32_GENERATOR = [996825010, 642813549, 513874426, 1027748829, 705979059];
125
142
  var CHECKSUM_SLOTS = [0, 1, 2, 3, 4, 5];
126
143
  var LNURL_HRP = "lnurl";
127
144
  function toLnurl(endpoint) {
@@ -158,30 +175,10 @@ function toWords(bytes) {
158
175
  return words;
159
176
  }
160
177
  function checksumWords(hrp, words) {
161
- const expanded = [...hrp].map((char) => char.charCodeAt(0));
162
- const values = [
163
- ...expanded.map((code) => code >> 5),
164
- 0,
165
- ...expanded.map((code) => code & 31),
166
- ...words,
167
- ...CHECKSUM_SLOTS.map(() => 0)
168
- ];
178
+ const values = [...expandedHrp(hrp), ...words, ...CHECKSUM_SLOTS.map(() => 0)];
169
179
  const polymod = bech32Polymod(values) ^ 1;
170
180
  return CHECKSUM_SLOTS.map((slot) => polymod >> 5 * (5 - slot) & 31);
171
181
  }
172
- function bech32Polymod(values) {
173
- let checksum = 1;
174
- for (const value of values) {
175
- const top = checksum >> 25;
176
- checksum = (checksum & 33554431) << 5 ^ value;
177
- for (let bit = 0; bit < 5; bit++) {
178
- if (top >> bit & 1) {
179
- checksum ^= BECH32_GENERATOR[bit];
180
- }
181
- }
182
- }
183
- return checksum;
184
- }
185
182
 
186
183
  // src/qr.ts
187
184
  var import_uqr = require("uqr");