bsv-x402 0.1.1 → 0.3.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/dist/index.cjs CHANGED
@@ -20,37 +20,1338 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ BFG_DAILY_CEILING_SATOSHIS: () => BFG_DAILY_CEILING_SATOSHIS,
24
+ BFG_PER_TX_CEILING_SATOSHIS: () => BFG_PER_TX_CEILING_SATOSHIS,
25
+ LocalStorageAdapter: () => LocalStorageAdapter,
26
+ RateLimiter: () => RateLimiter,
27
+ TIER_PRESETS: () => TIER_PRESETS,
28
+ WalletTwoFactorProvider: () => WalletTwoFactorProvider,
29
+ constructBrc105Proof: () => constructBrc105Proof,
30
+ createX402Fetch: () => createX402Fetch,
31
+ parseBrc105Challenge: () => parseBrc105Challenge,
23
32
  parseChallenge: () => parseChallenge,
33
+ resolveSitePolicy: () => resolveSitePolicy,
34
+ resolveSpendLimits: () => resolveSpendLimits,
24
35
  x402Fetch: () => x402Fetch
25
36
  });
26
37
  module.exports = __toCommonJS(index_exports);
27
38
 
39
+ // src/brc105-challenge.ts
40
+ function parseBrc105Challenge(response) {
41
+ const version = response.headers.get("x-bsv-payment-version");
42
+ if (version === null) {
43
+ throw new Error("BRC-105: missing x-bsv-payment-version header");
44
+ }
45
+ if (version !== "1.0") {
46
+ throw new Error(`BRC-105: unsupported version "${version}", expected "1.0"`);
47
+ }
48
+ const satoshisRaw = response.headers.get("x-bsv-payment-satoshis-required");
49
+ if (satoshisRaw === null) {
50
+ throw new Error("BRC-105: missing x-bsv-payment-satoshis-required header");
51
+ }
52
+ const satoshisRequired = Number(satoshisRaw);
53
+ if (!Number.isFinite(satoshisRequired) || !Number.isInteger(satoshisRequired) || satoshisRequired <= 0) {
54
+ throw new Error("BRC-105: satoshis-required must be a positive integer");
55
+ }
56
+ const serverIdentityKey = response.headers.get("x-bsv-auth-identity-key");
57
+ if (serverIdentityKey === null || serverIdentityKey.length === 0) {
58
+ throw new Error("BRC-105: missing or empty x-bsv-auth-identity-key header");
59
+ }
60
+ if (!/^0[23][0-9a-fA-F]{64}$/.test(serverIdentityKey)) {
61
+ throw new Error("BRC-105: x-bsv-auth-identity-key must be a 33-byte compressed public key (hex)");
62
+ }
63
+ const derivationPrefix = response.headers.get("x-bsv-payment-derivation-prefix");
64
+ if (derivationPrefix === null || derivationPrefix.length === 0) {
65
+ throw new Error("BRC-105: missing or empty x-bsv-payment-derivation-prefix header");
66
+ }
67
+ return {
68
+ version,
69
+ satoshisRequired,
70
+ serverIdentityKey,
71
+ derivationPrefix
72
+ };
73
+ }
74
+
75
+ // src/brc105-proof.ts
76
+ function bytesToHex(bytes) {
77
+ return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
78
+ }
79
+ function hexToBytes(hex) {
80
+ if (hex.length % 2 !== 0) throw new Error("Hex string must have even length");
81
+ const bytes = new Uint8Array(hex.length / 2);
82
+ for (let i = 0; i < hex.length; i += 2) {
83
+ bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);
84
+ }
85
+ return bytes;
86
+ }
87
+ function bytesToBase64(bytes) {
88
+ let binary = "";
89
+ for (const b of bytes) binary += String.fromCharCode(b);
90
+ return btoa(binary);
91
+ }
92
+ function numberArrayToBase64(arr) {
93
+ return bytesToBase64(new Uint8Array(arr));
94
+ }
95
+ var RIPEMD160_CONSTANTS = {
96
+ // Round constants for left and right paths
97
+ KL: [0, 1518500249, 1859775393, 2400959708, 2840853838],
98
+ KR: [1352829926, 1548603684, 1836072691, 2053994217, 0],
99
+ // Message schedule (word index per step)
100
+ RL: [
101
+ 0,
102
+ 1,
103
+ 2,
104
+ 3,
105
+ 4,
106
+ 5,
107
+ 6,
108
+ 7,
109
+ 8,
110
+ 9,
111
+ 10,
112
+ 11,
113
+ 12,
114
+ 13,
115
+ 14,
116
+ 15,
117
+ 7,
118
+ 4,
119
+ 13,
120
+ 1,
121
+ 10,
122
+ 6,
123
+ 15,
124
+ 3,
125
+ 12,
126
+ 0,
127
+ 9,
128
+ 5,
129
+ 2,
130
+ 14,
131
+ 11,
132
+ 8,
133
+ 3,
134
+ 10,
135
+ 14,
136
+ 4,
137
+ 9,
138
+ 15,
139
+ 8,
140
+ 1,
141
+ 2,
142
+ 7,
143
+ 0,
144
+ 6,
145
+ 13,
146
+ 11,
147
+ 5,
148
+ 12,
149
+ 1,
150
+ 9,
151
+ 11,
152
+ 10,
153
+ 0,
154
+ 8,
155
+ 12,
156
+ 4,
157
+ 13,
158
+ 3,
159
+ 7,
160
+ 15,
161
+ 14,
162
+ 5,
163
+ 6,
164
+ 2,
165
+ 4,
166
+ 0,
167
+ 5,
168
+ 9,
169
+ 7,
170
+ 12,
171
+ 2,
172
+ 10,
173
+ 14,
174
+ 1,
175
+ 3,
176
+ 8,
177
+ 11,
178
+ 6,
179
+ 15,
180
+ 13
181
+ ],
182
+ RR: [
183
+ 5,
184
+ 14,
185
+ 7,
186
+ 0,
187
+ 9,
188
+ 2,
189
+ 11,
190
+ 4,
191
+ 13,
192
+ 6,
193
+ 15,
194
+ 8,
195
+ 1,
196
+ 10,
197
+ 3,
198
+ 12,
199
+ 6,
200
+ 11,
201
+ 3,
202
+ 7,
203
+ 0,
204
+ 13,
205
+ 5,
206
+ 10,
207
+ 14,
208
+ 15,
209
+ 8,
210
+ 12,
211
+ 4,
212
+ 9,
213
+ 1,
214
+ 2,
215
+ 15,
216
+ 5,
217
+ 1,
218
+ 3,
219
+ 7,
220
+ 14,
221
+ 6,
222
+ 9,
223
+ 11,
224
+ 8,
225
+ 12,
226
+ 2,
227
+ 10,
228
+ 0,
229
+ 4,
230
+ 13,
231
+ 8,
232
+ 6,
233
+ 4,
234
+ 1,
235
+ 3,
236
+ 11,
237
+ 15,
238
+ 0,
239
+ 5,
240
+ 12,
241
+ 2,
242
+ 13,
243
+ 9,
244
+ 7,
245
+ 10,
246
+ 14,
247
+ 12,
248
+ 15,
249
+ 10,
250
+ 4,
251
+ 1,
252
+ 5,
253
+ 8,
254
+ 7,
255
+ 6,
256
+ 2,
257
+ 13,
258
+ 14,
259
+ 0,
260
+ 3,
261
+ 9,
262
+ 11
263
+ ],
264
+ // Rotation amounts
265
+ SL: [
266
+ 11,
267
+ 14,
268
+ 15,
269
+ 12,
270
+ 5,
271
+ 8,
272
+ 7,
273
+ 9,
274
+ 11,
275
+ 13,
276
+ 14,
277
+ 15,
278
+ 6,
279
+ 7,
280
+ 9,
281
+ 8,
282
+ 7,
283
+ 6,
284
+ 8,
285
+ 13,
286
+ 11,
287
+ 9,
288
+ 7,
289
+ 15,
290
+ 7,
291
+ 12,
292
+ 15,
293
+ 9,
294
+ 11,
295
+ 7,
296
+ 13,
297
+ 12,
298
+ 11,
299
+ 13,
300
+ 6,
301
+ 7,
302
+ 14,
303
+ 9,
304
+ 13,
305
+ 15,
306
+ 14,
307
+ 8,
308
+ 13,
309
+ 6,
310
+ 5,
311
+ 12,
312
+ 7,
313
+ 5,
314
+ 11,
315
+ 12,
316
+ 14,
317
+ 15,
318
+ 14,
319
+ 15,
320
+ 9,
321
+ 8,
322
+ 9,
323
+ 14,
324
+ 5,
325
+ 6,
326
+ 8,
327
+ 6,
328
+ 5,
329
+ 12,
330
+ 9,
331
+ 15,
332
+ 5,
333
+ 11,
334
+ 6,
335
+ 8,
336
+ 13,
337
+ 12,
338
+ 5,
339
+ 12,
340
+ 13,
341
+ 14,
342
+ 11,
343
+ 8,
344
+ 5,
345
+ 6
346
+ ],
347
+ SR: [
348
+ 8,
349
+ 9,
350
+ 9,
351
+ 11,
352
+ 13,
353
+ 15,
354
+ 15,
355
+ 5,
356
+ 7,
357
+ 7,
358
+ 8,
359
+ 11,
360
+ 14,
361
+ 14,
362
+ 12,
363
+ 6,
364
+ 9,
365
+ 13,
366
+ 15,
367
+ 7,
368
+ 12,
369
+ 8,
370
+ 9,
371
+ 11,
372
+ 7,
373
+ 7,
374
+ 12,
375
+ 7,
376
+ 6,
377
+ 15,
378
+ 13,
379
+ 11,
380
+ 9,
381
+ 7,
382
+ 15,
383
+ 11,
384
+ 8,
385
+ 6,
386
+ 6,
387
+ 14,
388
+ 12,
389
+ 13,
390
+ 5,
391
+ 14,
392
+ 13,
393
+ 13,
394
+ 7,
395
+ 5,
396
+ 15,
397
+ 5,
398
+ 8,
399
+ 11,
400
+ 14,
401
+ 14,
402
+ 6,
403
+ 14,
404
+ 6,
405
+ 9,
406
+ 12,
407
+ 9,
408
+ 12,
409
+ 5,
410
+ 15,
411
+ 8,
412
+ 8,
413
+ 5,
414
+ 12,
415
+ 9,
416
+ 12,
417
+ 5,
418
+ 14,
419
+ 6,
420
+ 8,
421
+ 13,
422
+ 6,
423
+ 5,
424
+ 15,
425
+ 13,
426
+ 11,
427
+ 11
428
+ ]
429
+ };
430
+ function rotl32(x, n) {
431
+ return (x << n | x >>> 32 - n) >>> 0;
432
+ }
433
+ function f(j, x, y, z) {
434
+ if (j < 16) return (x ^ y ^ z) >>> 0;
435
+ if (j < 32) return (x & y | ~x & z) >>> 0;
436
+ if (j < 48) return ((x | ~y) ^ z) >>> 0;
437
+ if (j < 64) return (x & z | y & ~z) >>> 0;
438
+ return (x ^ (y | ~z)) >>> 0;
439
+ }
440
+ function ripemd160(message) {
441
+ const msgLen = message.length;
442
+ const bitLen = msgLen * 8;
443
+ const padLen = (55 - msgLen % 64 + 64) % 64 + 1;
444
+ const padded = new Uint8Array(msgLen + padLen + 8);
445
+ padded.set(message);
446
+ padded[msgLen] = 128;
447
+ const view = new DataView(padded.buffer);
448
+ view.setUint32(padded.length - 8, bitLen >>> 0, true);
449
+ view.setUint32(padded.length - 4, bitLen / 4294967296 >>> 0, true);
450
+ let h0 = 1732584193;
451
+ let h1 = 4023233417;
452
+ let h2 = 2562383102;
453
+ let h3 = 271733878;
454
+ let h4 = 3285377520;
455
+ const { KL, KR, RL, RR, SL, SR } = RIPEMD160_CONSTANTS;
456
+ for (let offset = 0; offset < padded.length; offset += 64) {
457
+ const w = new Uint32Array(16);
458
+ for (let i = 0; i < 16; i++) {
459
+ w[i] = view.getUint32(offset + i * 4, true);
460
+ }
461
+ let al = h0, bl = h1, cl = h2, dl = h3, el = h4;
462
+ let ar = h0, br = h1, cr = h2, dr = h3, er = h4;
463
+ for (let j = 0; j < 80; j++) {
464
+ const round = j >>> 4;
465
+ let t2 = al + f(j, bl, cl, dl) + w[RL[j]] + KL[round] >>> 0;
466
+ t2 = rotl32(t2, SL[j]) + el >>> 0;
467
+ al = el;
468
+ el = dl;
469
+ dl = rotl32(cl, 10);
470
+ cl = bl;
471
+ bl = t2;
472
+ t2 = ar + f(79 - j, br, cr, dr) + w[RR[j]] + KR[round] >>> 0;
473
+ t2 = rotl32(t2, SR[j]) + er >>> 0;
474
+ ar = er;
475
+ er = dr;
476
+ dr = rotl32(cr, 10);
477
+ cr = br;
478
+ br = t2;
479
+ }
480
+ const t = h1 + cl + dr >>> 0;
481
+ h1 = h2 + dl + er >>> 0;
482
+ h2 = h3 + el + ar >>> 0;
483
+ h3 = h4 + al + br >>> 0;
484
+ h4 = h0 + bl + cr >>> 0;
485
+ h0 = t;
486
+ }
487
+ const digest = new Uint8Array(20);
488
+ const dv = new DataView(digest.buffer);
489
+ dv.setUint32(0, h0, true);
490
+ dv.setUint32(4, h1, true);
491
+ dv.setUint32(8, h2, true);
492
+ dv.setUint32(12, h3, true);
493
+ dv.setUint32(16, h4, true);
494
+ return digest;
495
+ }
496
+ async function hash160(data) {
497
+ const sha256 = new Uint8Array(await crypto.subtle.digest("SHA-256", data));
498
+ return ripemd160(sha256);
499
+ }
500
+ async function pubkeyToP2PKHLockingScript(pubkeyHex) {
501
+ const pubkeyBytes = hexToBytes(pubkeyHex);
502
+ const pubkeyHash = await hash160(pubkeyBytes);
503
+ return `76a914${bytesToHex(pubkeyHash)}88ac`;
504
+ }
505
+ async function createDerivationSuffix(wallet) {
506
+ const randomBytes = new Uint8Array(16);
507
+ crypto.getRandomValues(randomBytes);
508
+ const firstHalf = Array.from(randomBytes);
509
+ const keyID = new TextDecoder().decode(randomBytes);
510
+ const { hmac } = await wallet.createHmac({
511
+ data: firstHalf,
512
+ protocolID: [2, "server hmac"],
513
+ keyID,
514
+ counterparty: "self"
515
+ });
516
+ const nonceBytes = [...firstHalf, ...hmac];
517
+ return numberArrayToBase64(nonceBytes);
518
+ }
519
+ async function constructBrc105Proof(challenge, wallet) {
520
+ const derivationSuffix = await createDerivationSuffix(wallet);
521
+ const keyID = `${challenge.derivationPrefix} ${derivationSuffix}`;
522
+ const { publicKey: derivedPublicKey } = await wallet.getPublicKey({
523
+ protocolID: [2, "3241645161d8"],
524
+ keyID,
525
+ counterparty: challenge.serverIdentityKey
526
+ });
527
+ const lockingScript = await pubkeyToP2PKHLockingScript(derivedPublicKey);
528
+ const result = await wallet.createAction({
529
+ description: "BRC-105 payment",
530
+ outputs: [{
531
+ satoshis: challenge.satoshisRequired,
532
+ lockingScript,
533
+ description: "BRC-105 payment output",
534
+ customInstructions: JSON.stringify({
535
+ derivationPrefix: challenge.derivationPrefix,
536
+ derivationSuffix,
537
+ payee: challenge.serverIdentityKey
538
+ })
539
+ }],
540
+ options: {
541
+ randomizeOutputs: false
542
+ }
543
+ });
544
+ let transactionBase64;
545
+ if (result.tx && Array.isArray(result.tx) && result.tx.length > 0) {
546
+ transactionBase64 = numberArrayToBase64(result.tx);
547
+ } else if (result.rawTx && typeof result.rawTx === "string" && result.rawTx.length > 0) {
548
+ transactionBase64 = bytesToBase64(hexToBytes(result.rawTx));
549
+ } else {
550
+ throw new Error("Wallet returned no transaction data (neither tx nor rawTx)");
551
+ }
552
+ return {
553
+ derivationPrefix: challenge.derivationPrefix,
554
+ derivationSuffix,
555
+ transaction: transactionBase64,
556
+ txid: result.txid
557
+ };
558
+ }
559
+
28
560
  // src/challenge.ts
29
561
  function parseChallenge(header) {
30
- return JSON.parse(header);
562
+ let parsed;
563
+ try {
564
+ parsed = JSON.parse(header);
565
+ } catch {
566
+ throw new Error("X402-Challenge: invalid JSON");
567
+ }
568
+ if (typeof parsed !== "object" || parsed === null) {
569
+ throw new Error("X402-Challenge: expected object");
570
+ }
571
+ const obj = parsed;
572
+ if (typeof obj.nonce !== "string" || obj.nonce.length === 0) {
573
+ throw new Error("X402-Challenge: missing or invalid nonce");
574
+ }
575
+ if (typeof obj.payee !== "string" || obj.payee.length === 0) {
576
+ throw new Error("X402-Challenge: missing or invalid payee");
577
+ }
578
+ if (typeof obj.network !== "string" || obj.network.length === 0) {
579
+ throw new Error("X402-Challenge: missing or invalid network");
580
+ }
581
+ if (typeof obj.amount !== "number" || !Number.isFinite(obj.amount) || !Number.isInteger(obj.amount) || obj.amount <= 0) {
582
+ throw new Error("X402-Challenge: amount must be a positive integer");
583
+ }
584
+ return {
585
+ nonce: obj.nonce,
586
+ payee: obj.payee,
587
+ amount: obj.amount,
588
+ network: obj.network
589
+ };
590
+ }
591
+
592
+ // src/limits.ts
593
+ var BFG_DAILY_CEILING_SATOSHIS = 1e10;
594
+ var BFG_PER_TX_CEILING_SATOSHIS = 1e9;
595
+ var WINDOW_MS = {
596
+ minute: 6e4,
597
+ hour: 36e5,
598
+ day: 864e5,
599
+ week: 6048e5
600
+ };
601
+ function windowToMs(window2) {
602
+ return WINDOW_MS[window2];
603
+ }
604
+ function makeLimits(windows, perTxMaxSatoshis, opts = {}) {
605
+ return {
606
+ windows,
607
+ perTxMaxSatoshis,
608
+ yellowLightThreshold: 0.8,
609
+ requirePerSitePrompt: false,
610
+ sitePolicies: {},
611
+ require2fa: {
612
+ onCircuitBreakerReset: true,
613
+ onTierChange: true,
614
+ onHighValueTx: false,
615
+ highValueThreshold: 0,
616
+ onNewSiteApproval: false
617
+ },
618
+ ...opts
619
+ };
620
+ }
621
+ var TOO_YOUNG_TO_DIE = {
622
+ interactive: makeLimits(
623
+ [{ window: "day", maxSatoshis: 1e8, maxTransactions: Infinity }],
624
+ 1e8
625
+ ),
626
+ programmatic: makeLimits(
627
+ [{ window: "day", maxSatoshis: 1e8, maxTransactions: Infinity }],
628
+ 1e6
629
+ )
630
+ };
631
+ var HEY_NOT_TOO_ROUGH = {
632
+ interactive: makeLimits(
633
+ [
634
+ { window: "day", maxSatoshis: 1e8, maxTransactions: 100 },
635
+ { window: "week", maxSatoshis: 5e8, maxTransactions: 500 }
636
+ ],
637
+ 1e7,
638
+ {
639
+ requirePerSitePrompt: true,
640
+ require2fa: {
641
+ onCircuitBreakerReset: true,
642
+ onTierChange: true,
643
+ onHighValueTx: true,
644
+ highValueThreshold: 5e7,
645
+ onNewSiteApproval: true
646
+ }
647
+ }
648
+ ),
649
+ programmatic: makeLimits(
650
+ [
651
+ { window: "day", maxSatoshis: 1e8, maxTransactions: 1e4 },
652
+ { window: "week", maxSatoshis: 5e8, maxTransactions: 5e4 }
653
+ ],
654
+ 1e5,
655
+ {
656
+ requirePerSitePrompt: true,
657
+ require2fa: {
658
+ onCircuitBreakerReset: true,
659
+ onTierChange: true,
660
+ onHighValueTx: true,
661
+ highValueThreshold: 5e7,
662
+ onNewSiteApproval: true
663
+ }
664
+ }
665
+ )
666
+ };
667
+ var HURT_ME_PLENTY = {
668
+ interactive: makeLimits(
669
+ [
670
+ { window: "minute", maxSatoshis: 5e6, maxTransactions: 10 },
671
+ { window: "hour", maxSatoshis: 5e7, maxTransactions: 60 },
672
+ { window: "day", maxSatoshis: 2e8, maxTransactions: 200 },
673
+ { window: "week", maxSatoshis: 1e9, maxTransactions: 1e3 }
674
+ ],
675
+ 2e7,
676
+ {
677
+ requirePerSitePrompt: true,
678
+ require2fa: {
679
+ onCircuitBreakerReset: true,
680
+ onTierChange: true,
681
+ onHighValueTx: true,
682
+ highValueThreshold: 1e8,
683
+ onNewSiteApproval: true
684
+ }
685
+ }
686
+ ),
687
+ programmatic: makeLimits(
688
+ [
689
+ { window: "minute", maxSatoshis: 5e6, maxTransactions: 1e3 },
690
+ { window: "hour", maxSatoshis: 5e7, maxTransactions: 6e3 },
691
+ { window: "day", maxSatoshis: 2e8, maxTransactions: 2e4 },
692
+ { window: "week", maxSatoshis: 1e9, maxTransactions: 1e5 }
693
+ ],
694
+ 2e5,
695
+ {
696
+ requirePerSitePrompt: true,
697
+ require2fa: {
698
+ onCircuitBreakerReset: true,
699
+ onTierChange: true,
700
+ onHighValueTx: true,
701
+ highValueThreshold: 1e8,
702
+ onNewSiteApproval: true
703
+ }
704
+ }
705
+ )
706
+ };
707
+ var ULTRA_VIOLENCE = {
708
+ interactive: makeLimits(
709
+ [...HURT_ME_PLENTY.interactive.windows],
710
+ HURT_ME_PLENTY.interactive.perTxMaxSatoshis,
711
+ {
712
+ requirePerSitePrompt: true,
713
+ require2fa: {
714
+ onCircuitBreakerReset: true,
715
+ onTierChange: true,
716
+ onHighValueTx: false,
717
+ highValueThreshold: 0,
718
+ onNewSiteApproval: true
719
+ }
720
+ }
721
+ ),
722
+ programmatic: makeLimits(
723
+ [...HURT_ME_PLENTY.programmatic.windows],
724
+ HURT_ME_PLENTY.programmatic.perTxMaxSatoshis,
725
+ {
726
+ requirePerSitePrompt: true,
727
+ require2fa: {
728
+ onCircuitBreakerReset: true,
729
+ onTierChange: true,
730
+ onHighValueTx: false,
731
+ highValueThreshold: 0,
732
+ onNewSiteApproval: true
733
+ }
734
+ }
735
+ )
736
+ };
737
+ var NIGHTMARE = {
738
+ interactive: makeLimits([], BFG_PER_TX_CEILING_SATOSHIS),
739
+ programmatic: makeLimits([], BFG_PER_TX_CEILING_SATOSHIS)
740
+ };
741
+ var TIER_PRESETS = {
742
+ "I'm Too Young to Die": TOO_YOUNG_TO_DIE,
743
+ "Hey, Not Too Rough": HEY_NOT_TOO_ROUGH,
744
+ "Hurt Me Plenty": HURT_ME_PLENTY,
745
+ "Ultra-Violence": ULTRA_VIOLENCE,
746
+ "Nightmare!": NIGHTMARE
747
+ };
748
+ function cloneSpendLimits(preset) {
749
+ return {
750
+ ...preset,
751
+ windows: preset.windows.map((w) => ({ ...w })),
752
+ require2fa: { ...preset.require2fa },
753
+ sitePolicies: { ...preset.sitePolicies }
754
+ };
755
+ }
756
+ function resolveSpendLimits(tier = "Hey, Not Too Rough", mode = "interactive", overrides) {
757
+ const preset = TIER_PRESETS[tier][mode];
758
+ const base = cloneSpendLimits(preset);
759
+ if (!overrides) return base;
760
+ return {
761
+ ...base,
762
+ ...overrides,
763
+ // Deep-merge require2fa if provided
764
+ require2fa: overrides.require2fa ? { ...base.require2fa, ...overrides.require2fa } : base.require2fa,
765
+ // Deep-merge sitePolicies if provided
766
+ sitePolicies: overrides.sitePolicies ? { ...base.sitePolicies, ...overrides.sitePolicies } : base.sitePolicies
767
+ };
768
+ }
769
+ var RateLimiter = class {
770
+ constructor(limits, state, now) {
771
+ this.limits = limits;
772
+ this.entries = state?.entries ?? [];
773
+ this.broken = state?.circuitBroken ?? false;
774
+ this.now = now ?? Date.now;
775
+ }
776
+ check(request, origin) {
777
+ if (this.broken) {
778
+ return { action: "block", reason: "Circuit breaker tripped \u2014 call resetLimits() to clear", severity: "trip" };
779
+ }
780
+ const amount = request.amount;
781
+ if (!Number.isFinite(amount) || !Number.isInteger(amount) || amount <= 0) {
782
+ return { action: "block", reason: "Invalid transaction amount rejected", severity: "reject" };
783
+ }
784
+ if (amount > BFG_PER_TX_CEILING_SATOSHIS) {
785
+ return { action: "block", reason: `Exceeds BFG per-tx ceiling (${BFG_PER_TX_CEILING_SATOSHIS} sats)`, severity: "reject" };
786
+ }
787
+ const dayAgo = this.now() - WINDOW_MS.day;
788
+ const dailyTotal = this.sumSatoshis(dayAgo);
789
+ if (dailyTotal + amount > BFG_DAILY_CEILING_SATOSHIS) {
790
+ return { action: "block", reason: `Exceeds BFG daily ceiling (${BFG_DAILY_CEILING_SATOSHIS} sats)`, severity: "trip" };
791
+ }
792
+ if (amount > this.limits.perTxMaxSatoshis) {
793
+ return { action: "block", reason: `Exceeds per-tx limit (${this.limits.perTxMaxSatoshis} sats)`, severity: "reject" };
794
+ }
795
+ const isCustomSite = this.hasCustomPolicy(origin);
796
+ const effectiveLimits = this.effectiveWindows(origin);
797
+ const effectivePerTx = this.effectivePerTxMax(origin);
798
+ if (effectivePerTx !== void 0 && amount > effectivePerTx) {
799
+ return { action: "block", reason: `Exceeds per-tx limit for ${origin} (${effectivePerTx} sats)`, severity: "reject" };
800
+ }
801
+ let yellowLight;
802
+ for (const wl of effectiveLimits) {
803
+ const cutoff = this.now() - windowToMs(wl.window);
804
+ const windowEntries = this.entriesInWindow(cutoff, isCustomSite ? origin : void 0);
805
+ const totalSats = windowEntries.reduce((sum, e) => sum + e.satoshis, 0);
806
+ const totalTx = windowEntries.length;
807
+ if (totalSats + amount > wl.maxSatoshis) {
808
+ return { action: "block", reason: `Exceeds ${wl.window} sats limit (${wl.maxSatoshis})`, severity: "window" };
809
+ }
810
+ if (totalTx + 1 > wl.maxTransactions) {
811
+ return { action: "block", reason: `Exceeds ${wl.window} tx count limit (${wl.maxTransactions})`, severity: "window" };
812
+ }
813
+ if (this.limits.yellowLightThreshold < 1 && !yellowLight && totalSats + amount > wl.maxSatoshis * this.limits.yellowLightThreshold) {
814
+ yellowLight = {
815
+ origin,
816
+ currentSpend: totalSats,
817
+ limit: wl.maxSatoshis,
818
+ window: wl.window,
819
+ challenge: request
820
+ };
821
+ }
822
+ }
823
+ if (yellowLight) {
824
+ return { action: "yellow-light", detail: yellowLight };
825
+ }
826
+ return { action: "allow" };
827
+ }
828
+ record(entry) {
829
+ this.entries.push(entry);
830
+ this.prune();
831
+ }
832
+ trip() {
833
+ this.broken = true;
834
+ }
835
+ reset() {
836
+ this.broken = false;
837
+ }
838
+ isBroken() {
839
+ return this.broken;
840
+ }
841
+ getState() {
842
+ return {
843
+ entries: [...this.entries],
844
+ circuitBroken: this.broken,
845
+ hmac: ""
846
+ };
847
+ }
848
+ hasCustomPolicy(origin) {
849
+ const policy = this.limits.sitePolicies[origin];
850
+ return policy?.action === "custom" && !!policy.limits;
851
+ }
852
+ effectiveWindows(origin) {
853
+ const policy = this.limits.sitePolicies[origin];
854
+ if (policy?.action === "custom" && policy.limits) {
855
+ return policy.limits;
856
+ }
857
+ return this.limits.windows;
858
+ }
859
+ effectivePerTxMax(origin) {
860
+ const policy = this.limits.sitePolicies[origin];
861
+ if (policy?.action === "custom" && policy.perTxMaxSatoshis !== void 0) {
862
+ return policy.perTxMaxSatoshis;
863
+ }
864
+ return void 0;
865
+ }
866
+ entriesInWindow(cutoff, filterOrigin) {
867
+ return this.entries.filter(
868
+ (e) => e.timestamp >= cutoff && (filterOrigin === void 0 || e.origin === filterOrigin)
869
+ );
870
+ }
871
+ sumSatoshis(cutoff) {
872
+ return this.entries.filter((e) => e.timestamp >= cutoff).reduce((sum, e) => sum + e.satoshis, 0);
873
+ }
874
+ prune() {
875
+ let longestWindow = WINDOW_MS.day;
876
+ for (const wl of this.limits.windows) {
877
+ longestWindow = Math.max(longestWindow, windowToMs(wl.window));
878
+ }
879
+ if (this.limits.sitePolicies) {
880
+ for (const policy of Object.values(this.limits.sitePolicies)) {
881
+ if (policy?.action === "custom" && Array.isArray(policy.limits)) {
882
+ for (const wl of policy.limits) {
883
+ longestWindow = Math.max(longestWindow, windowToMs(wl.window));
884
+ }
885
+ }
886
+ }
887
+ }
888
+ const cutoff = this.now() - longestWindow;
889
+ this.entries = this.entries.filter((e) => e.timestamp >= cutoff);
890
+ }
891
+ };
892
+
893
+ // src/site-policy.ts
894
+ var defaultSitePrompt = async (origin) => {
895
+ if (typeof globalThis.confirm !== "function") return "global";
896
+ const allow = globalThis.confirm(
897
+ `First payment to ${origin}.
898
+
899
+ Use your global spending limits for this site?
900
+
901
+ OK = Use global limits
902
+ Cancel = Block this site`
903
+ );
904
+ return allow ? "global" : "block";
905
+ };
906
+ async function resolveSitePolicy(origin, limits, twoFactorProvider, promptFn = defaultSitePrompt) {
907
+ const existing = limits.sitePolicies[origin];
908
+ if (existing) return existing;
909
+ if (!limits.requirePerSitePrompt) {
910
+ return { origin, action: "global" };
911
+ }
912
+ if (limits.require2fa.onNewSiteApproval) {
913
+ if (!twoFactorProvider) {
914
+ return { origin, action: "block" };
915
+ }
916
+ const verified = await twoFactorProvider.verify({
917
+ type: "new-site-approval",
918
+ origin
919
+ });
920
+ if (!verified) {
921
+ return { origin, action: "block" };
922
+ }
923
+ }
924
+ const action = await promptFn(origin);
925
+ return { origin, action };
926
+ }
927
+
928
+ // src/storage.ts
929
+ var STATE_KEY = "x402:limit-state";
930
+ var POLICIES_KEY = "x402:site-policies";
931
+ async function computeHmac(data, key) {
932
+ const cryptoKey = await crypto.subtle.importKey(
933
+ "raw",
934
+ key,
935
+ { name: "HMAC", hash: "SHA-256" },
936
+ false,
937
+ ["sign"]
938
+ );
939
+ const encoded = new TextEncoder().encode(data);
940
+ const sig = await crypto.subtle.sign("HMAC", cryptoKey, encoded);
941
+ return Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
942
+ }
943
+ function serializeForHmac(state) {
944
+ return JSON.stringify({
945
+ entries: state.entries,
946
+ circuitBroken: state.circuitBroken
947
+ });
31
948
  }
949
+ var LocalStorageAdapter = class {
950
+ constructor(keyDeriver, storage) {
951
+ this.keyDeriver = keyDeriver;
952
+ this.storage = storage ?? globalThis.localStorage;
953
+ }
954
+ async load() {
955
+ const raw = this.storage.getItem(STATE_KEY);
956
+ if (!raw) return null;
957
+ let state;
958
+ try {
959
+ state = JSON.parse(raw);
960
+ } catch {
961
+ console.warn("x402: limit state JSON parse failed \u2014 treating as tampered");
962
+ return { entries: [], circuitBroken: true, hmac: "" };
963
+ }
964
+ if (this.keyDeriver) {
965
+ if (!state.hmac) {
966
+ console.warn("x402: limit state missing HMAC \u2014 treating as tampered");
967
+ return { entries: [], circuitBroken: true, hmac: "" };
968
+ }
969
+ const key = await this.keyDeriver();
970
+ const expected = await computeHmac(serializeForHmac(state), key);
971
+ if (expected !== state.hmac) {
972
+ console.warn("x402: limit state HMAC mismatch \u2014 state may have been tampered with");
973
+ return { entries: [], circuitBroken: true, hmac: "" };
974
+ }
975
+ }
976
+ state.entries = (Array.isArray(state.entries) ? state.entries : []).filter(
977
+ (e) => e != null && typeof e.origin === "string" && typeof e.txid === "string" && typeof e.satoshis === "number" && Number.isFinite(e.satoshis) && e.satoshis >= 0 && typeof e.timestamp === "number" && Number.isFinite(e.timestamp) && e.timestamp > 0
978
+ );
979
+ return state;
980
+ }
981
+ async save(state) {
982
+ if (this.keyDeriver) {
983
+ const key = await this.keyDeriver();
984
+ state.hmac = await computeHmac(serializeForHmac(state), key);
985
+ }
986
+ this.storage.setItem(STATE_KEY, JSON.stringify(state));
987
+ }
988
+ async loadSitePolicies() {
989
+ const raw = this.storage.getItem(POLICIES_KEY);
990
+ if (!raw) return {};
991
+ try {
992
+ return JSON.parse(raw);
993
+ } catch {
994
+ return {};
995
+ }
996
+ }
997
+ async saveSitePolicies(policies) {
998
+ this.storage.setItem(POLICIES_KEY, JSON.stringify(policies));
999
+ }
1000
+ };
32
1001
 
33
1002
  // src/x402-fetch.ts
34
- async function x402Fetch(input, init) {
35
- const response = await fetch(input, init);
36
- if (response.status !== 402) {
37
- return response;
1003
+ var BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
1004
+ function base58DecodeCheck(address) {
1005
+ let leadingZeros = 0;
1006
+ for (const c of address) {
1007
+ if (c === "1") leadingZeros++;
1008
+ else break;
1009
+ }
1010
+ let n = BigInt(0);
1011
+ for (const c of address) {
1012
+ const i = BASE58_ALPHABET.indexOf(c);
1013
+ if (i < 0) throw new Error(`Invalid Base58 character: ${c}`);
1014
+ n = n * 58n + BigInt(i);
1015
+ }
1016
+ const hexFromBigint = n === 0n ? "" : n.toString(16);
1017
+ const paddedHex = hexFromBigint.length % 2 ? "0" + hexFromBigint : hexFromBigint;
1018
+ const bigintBytes = [];
1019
+ for (let i = 0; i < paddedHex.length; i += 2) {
1020
+ bigintBytes.push(parseInt(paddedHex.slice(i, i + 2), 16));
1021
+ }
1022
+ const allBytes = new Uint8Array(leadingZeros + bigintBytes.length);
1023
+ allBytes.set(bigintBytes, leadingZeros);
1024
+ if (allBytes.length !== 25) {
1025
+ throw new Error(`Invalid address length: expected 25 bytes, got ${allBytes.length}`);
38
1026
  }
39
- const challengeHeader = response.headers.get("X402-Challenge");
40
- if (!challengeHeader) {
1027
+ const body = allBytes.slice(0, 21);
1028
+ const checksum = allBytes.slice(21);
1029
+ const version = allBytes[0];
1030
+ if (version !== 0 && version !== 111) {
1031
+ throw new Error(`Unsupported address version: 0x${version.toString(16).padStart(2, "0")}`);
1032
+ }
1033
+ return { version, payload: body.slice(1) };
1034
+ }
1035
+ function payeeAddressToLockingScript(address) {
1036
+ const { payload } = base58DecodeCheck(address);
1037
+ if (payload.length !== 20) {
1038
+ throw new Error(`Invalid pubkey hash length: expected 20 bytes, got ${payload.length}`);
1039
+ }
1040
+ const pubkeyHash = Array.from(payload).map((b) => b.toString(16).padStart(2, "0")).join("");
1041
+ return `76a914${pubkeyHash}88ac`;
1042
+ }
1043
+ async function defaultConstructProof(challenge) {
1044
+ const cwi = globalThis.CWI;
1045
+ if (!cwi || typeof cwi.createAction !== "function") {
1046
+ throw new Error(
1047
+ "No BRC-100 wallet detected. Install a CWI-compliant browser extension or provide a custom proofConstructor in X402Config."
1048
+ );
1049
+ }
1050
+ const result = await cwi.createAction({
1051
+ description: `x402 payment: ${challenge.amount} sats to ${challenge.payee}`,
1052
+ outputs: [{
1053
+ satoshis: challenge.amount,
1054
+ lockingScript: payeeAddressToLockingScript(challenge.payee),
1055
+ description: `Payment to ${challenge.payee}`
1056
+ }],
1057
+ labels: ["x402-payment"],
1058
+ options: {
1059
+ returnTXIDOnly: false,
1060
+ noSend: false
1061
+ }
1062
+ });
1063
+ if (!result || !result.txid) {
1064
+ throw new Error("Wallet declined payment or returned invalid result");
1065
+ }
1066
+ if (!result.rawTx || typeof result.rawTx !== "string" || result.rawTx.length === 0) {
1067
+ throw new Error("Wallet did not return raw transaction");
1068
+ }
1069
+ return {
1070
+ txid: result.txid,
1071
+ rawTx: result.rawTx
1072
+ };
1073
+ }
1074
+ function createMutex() {
1075
+ let chain = Promise.resolve();
1076
+ return (fn) => {
1077
+ const result = chain.then(fn, fn);
1078
+ chain = result.then(() => {
1079
+ }, () => {
1080
+ });
1081
+ return result;
1082
+ };
1083
+ }
1084
+ function createX402Fetch(config = {}) {
1085
+ const tier = config.tier ?? "Hey, Not Too Rough";
1086
+ const mode = config.mode ?? "interactive";
1087
+ if (tier === "Nightmare!" && config.nightmareConfirmation !== "NIGHTMARE") {
1088
+ throw new Error('Nightmare! tier requires nightmareConfirmation: "NIGHTMARE"');
1089
+ }
1090
+ const limits = resolveSpendLimits(tier, mode, config.limits);
1091
+ const storage = config.storage ?? new LocalStorageAdapter();
1092
+ const twoFactor = config.twoFactorProvider;
1093
+ const constructProof = config.proofConstructor ?? defaultConstructProof;
1094
+ const brc105ProofConstructor = config.brc105ProofConstructor;
1095
+ const brc105Wallet = config.brc105Wallet;
1096
+ const nowFn = config.now ?? Date.now;
1097
+ const mutex = createMutex();
1098
+ const needs2fa = limits.require2fa;
1099
+ if (!twoFactor && (needs2fa.onCircuitBreakerReset || needs2fa.onHighValueTx || needs2fa.onNewSiteApproval || needs2fa.onTierChange)) {
1100
+ console.warn("x402: tier requires 2FA but no twoFactorProvider configured \u2014 2FA-gated actions will be blocked");
1101
+ }
1102
+ let limiter;
1103
+ let initialised = false;
1104
+ async function ensureInitialised() {
1105
+ if (limiter && initialised) return limiter;
1106
+ const state = await storage.load();
1107
+ limiter = new RateLimiter(limits, state ?? void 0, nowFn);
1108
+ const policies = await storage.loadSitePolicies();
1109
+ Object.assign(limits.sitePolicies, policies);
1110
+ initialised = true;
1111
+ return limiter;
1112
+ }
1113
+ async function persist(rl) {
1114
+ await storage.save(rl.getState());
1115
+ await storage.saveSitePolicies(limits.sitePolicies);
1116
+ }
1117
+ async function handlePaymentFlow(originalResponse, input, init, origin, amount, protocol, buildProof, retryWithProof, makeLedgerEntry) {
1118
+ return mutex(async () => {
1119
+ const rl = await ensureInitialised();
1120
+ const sitePolicy = await resolveSitePolicy(origin, limits, twoFactor);
1121
+ if (sitePolicy.action === "block") return originalResponse;
1122
+ if (!limits.sitePolicies[origin]) {
1123
+ limits.sitePolicies[origin] = sitePolicy;
1124
+ await storage.saveSitePolicies(limits.sitePolicies);
1125
+ }
1126
+ const spendCheckable = { amount, origin, protocol };
1127
+ const result = rl.check(spendCheckable, origin);
1128
+ if (result.action === "block") {
1129
+ if (result.severity === "trip") {
1130
+ rl.trip();
1131
+ await persist(rl);
1132
+ config.onLimitReached?.(result.reason);
1133
+ return originalResponse;
1134
+ }
1135
+ if (result.severity === "window" && twoFactor) {
1136
+ config.onLimitReached?.(result.reason);
1137
+ const override = await twoFactor.verify({
1138
+ type: "limit-override",
1139
+ amount,
1140
+ origin,
1141
+ reason: result.reason
1142
+ });
1143
+ if (override) {
1144
+ } else {
1145
+ return originalResponse;
1146
+ }
1147
+ } else {
1148
+ config.onLimitReached?.(result.reason);
1149
+ return originalResponse;
1150
+ }
1151
+ }
1152
+ if (result.action === "yellow-light") {
1153
+ const proceed = config.onYellowLight ? await config.onYellowLight(result.detail) : false;
1154
+ if (!proceed) return originalResponse;
1155
+ }
1156
+ if (limits.require2fa.onHighValueTx && amount > limits.require2fa.highValueThreshold) {
1157
+ if (!twoFactor) return originalResponse;
1158
+ const verified = await twoFactor.verify({
1159
+ type: "high-value-tx",
1160
+ amount,
1161
+ origin
1162
+ });
1163
+ if (!verified) return originalResponse;
1164
+ }
1165
+ let proof;
1166
+ try {
1167
+ proof = await buildProof();
1168
+ } catch {
1169
+ return originalResponse;
1170
+ }
1171
+ rl.record(makeLedgerEntry(proof));
1172
+ await persist(rl);
1173
+ return retryWithProof(proof);
1174
+ });
1175
+ }
1176
+ const fetchFn = async function x402Fetch2(input, init) {
1177
+ const response = await fetch(input, init);
1178
+ if (response.status !== 402) return response;
1179
+ const origin = extractOrigin(input);
1180
+ const challengeHeader = response.headers.get("X402-Challenge");
1181
+ if (challengeHeader) {
1182
+ let challenge;
1183
+ try {
1184
+ challenge = parseChallenge(challengeHeader);
1185
+ } catch {
1186
+ return response;
1187
+ }
1188
+ return handlePaymentFlow(
1189
+ response,
1190
+ input,
1191
+ init,
1192
+ origin,
1193
+ challenge.amount,
1194
+ "x402",
1195
+ async () => constructProof(challenge),
1196
+ (proof) => {
1197
+ const headers = new Headers(init?.headers);
1198
+ headers.set("X402-Proof", JSON.stringify(proof));
1199
+ return fetch(input, { ...init, headers });
1200
+ },
1201
+ (proof) => ({
1202
+ timestamp: nowFn(),
1203
+ origin,
1204
+ satoshis: challenge.amount,
1205
+ txid: proof.txid
1206
+ })
1207
+ );
1208
+ }
1209
+ const brc105Version = response.headers.get("x-bsv-payment-version");
1210
+ if (brc105Version) {
1211
+ if (!brc105ProofConstructor && !brc105Wallet) return response;
1212
+ let brc105Challenge;
1213
+ try {
1214
+ brc105Challenge = parseBrc105Challenge(response);
1215
+ } catch {
1216
+ return response;
1217
+ }
1218
+ return handlePaymentFlow(
1219
+ response,
1220
+ input,
1221
+ init,
1222
+ origin,
1223
+ brc105Challenge.satoshisRequired,
1224
+ "brc105",
1225
+ async () => {
1226
+ if (brc105ProofConstructor) {
1227
+ return brc105ProofConstructor(brc105Challenge);
1228
+ }
1229
+ return constructBrc105Proof(brc105Challenge, brc105Wallet);
1230
+ },
1231
+ (proof) => {
1232
+ const headers = new Headers(init?.headers);
1233
+ headers.set("x-bsv-payment", JSON.stringify(proof));
1234
+ return fetch(input, { ...init, headers });
1235
+ },
1236
+ (proof) => ({
1237
+ timestamp: nowFn(),
1238
+ origin,
1239
+ satoshis: brc105Challenge.satoshisRequired,
1240
+ txid: proof.txid,
1241
+ protocol: "brc105"
1242
+ })
1243
+ );
1244
+ }
41
1245
  return response;
1246
+ };
1247
+ fetchFn.resetLimits = async () => {
1248
+ const rl = await ensureInitialised();
1249
+ if (limits.require2fa.onCircuitBreakerReset) {
1250
+ if (!twoFactor) throw new Error("2FA required for circuit breaker reset but no twoFactorProvider configured");
1251
+ const verified = await twoFactor.verify({ type: "circuit-breaker-reset" });
1252
+ if (!verified) throw new Error("2FA verification failed for circuit breaker reset");
1253
+ }
1254
+ rl.reset();
1255
+ await persist(rl);
1256
+ };
1257
+ fetchFn.getState = () => {
1258
+ if (!limiter) return { entries: [], circuitBroken: false };
1259
+ const state = limiter.getState();
1260
+ return { entries: state.entries, circuitBroken: state.circuitBroken };
1261
+ };
1262
+ return fetchFn;
1263
+ }
1264
+ var singleton;
1265
+ async function x402Fetch(input, init) {
1266
+ if (!singleton) singleton = createX402Fetch();
1267
+ return singleton(input, init);
1268
+ }
1269
+ function resolveRelativeUrl(url) {
1270
+ const loc = globalThis.location;
1271
+ if (loc?.href) {
1272
+ return new URL(url, loc.href).origin;
1273
+ }
1274
+ return "unknown";
1275
+ }
1276
+ function extractOrigin(input) {
1277
+ if (input instanceof URL) return input.origin;
1278
+ if (typeof input === "string") {
1279
+ try {
1280
+ return new URL(input).origin;
1281
+ } catch {
1282
+ try {
1283
+ return resolveRelativeUrl(input);
1284
+ } catch {
1285
+ return "unknown";
1286
+ }
1287
+ }
1288
+ }
1289
+ try {
1290
+ return new URL(input.url).origin;
1291
+ } catch {
1292
+ try {
1293
+ return resolveRelativeUrl(input.url);
1294
+ } catch {
1295
+ return "unknown";
1296
+ }
42
1297
  }
43
- const challenge = parseChallenge(challengeHeader);
44
- const proof = await constructProof(challenge);
45
- const headers = new Headers(init?.headers);
46
- headers.set("X402-Proof", JSON.stringify(proof));
47
- return fetch(input, { ...init, headers });
48
1298
  }
49
- async function constructProof(challenge) {
50
- throw new Error("Not implemented \u2014 requires BRC-100 wallet (window.CWI)");
1299
+
1300
+ // src/two-factor.ts
1301
+ var WalletTwoFactorProvider = class {
1302
+ async verify(action) {
1303
+ if (typeof window === "undefined" || !window.CWI) {
1304
+ return this.promptFallback(action);
1305
+ }
1306
+ const challengeData = `x402-2fa:${JSON.stringify(action)}:${Date.now()}`;
1307
+ try {
1308
+ const sig = await window.CWI.createSignature({
1309
+ data: new TextEncoder().encode(challengeData),
1310
+ protocolID: [1, "x402-2fa"],
1311
+ keyID: "spending-limits"
1312
+ });
1313
+ return sig !== null;
1314
+ } catch {
1315
+ return false;
1316
+ }
1317
+ }
1318
+ promptFallback(action) {
1319
+ if (typeof window === "undefined" || !window.prompt) return false;
1320
+ const message = describeAction(action);
1321
+ const result = window.prompt(`${message}
1322
+
1323
+ Type CONFIRM to proceed:`);
1324
+ return result === "CONFIRM";
1325
+ }
1326
+ };
1327
+ function describeAction(action) {
1328
+ switch (action.type) {
1329
+ case "circuit-breaker-reset":
1330
+ return "Reset spending circuit breaker? This re-enables automated payments.";
1331
+ case "tier-change":
1332
+ return `Change spending tier from "${action.from}" to "${action.to}"?`;
1333
+ case "high-value-tx":
1334
+ return `Approve high-value payment of ${action.amount} sats to ${action.origin}?`;
1335
+ case "new-site-approval":
1336
+ return `Allow automated payments to ${action.origin}?`;
1337
+ case "limit-override":
1338
+ return `Spending limit reached: ${action.reason}
1339
+ Allow this payment of ${action.amount} sats to ${action.origin}?`;
1340
+ }
51
1341
  }
52
1342
  // Annotate the CommonJS export names for ESM import in node:
53
1343
  0 && (module.exports = {
1344
+ BFG_DAILY_CEILING_SATOSHIS,
1345
+ BFG_PER_TX_CEILING_SATOSHIS,
1346
+ LocalStorageAdapter,
1347
+ RateLimiter,
1348
+ TIER_PRESETS,
1349
+ WalletTwoFactorProvider,
1350
+ constructBrc105Proof,
1351
+ createX402Fetch,
1352
+ parseBrc105Challenge,
54
1353
  parseChallenge,
1354
+ resolveSitePolicy,
1355
+ resolveSpendLimits,
55
1356
  x402Fetch
56
1357
  });