bsv-x402 0.1.1 → 0.2.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,751 @@ 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
+ createX402Fetch: () => createX402Fetch,
23
30
  parseChallenge: () => parseChallenge,
31
+ resolveSitePolicy: () => resolveSitePolicy,
32
+ resolveSpendLimits: () => resolveSpendLimits,
24
33
  x402Fetch: () => x402Fetch
25
34
  });
26
35
  module.exports = __toCommonJS(index_exports);
27
36
 
28
37
  // src/challenge.ts
29
38
  function parseChallenge(header) {
30
- return JSON.parse(header);
39
+ let parsed;
40
+ try {
41
+ parsed = JSON.parse(header);
42
+ } catch {
43
+ throw new Error("X402-Challenge: invalid JSON");
44
+ }
45
+ if (typeof parsed !== "object" || parsed === null) {
46
+ throw new Error("X402-Challenge: expected object");
47
+ }
48
+ const obj = parsed;
49
+ if (typeof obj.nonce !== "string" || obj.nonce.length === 0) {
50
+ throw new Error("X402-Challenge: missing or invalid nonce");
51
+ }
52
+ if (typeof obj.payee !== "string" || obj.payee.length === 0) {
53
+ throw new Error("X402-Challenge: missing or invalid payee");
54
+ }
55
+ if (typeof obj.network !== "string" || obj.network.length === 0) {
56
+ throw new Error("X402-Challenge: missing or invalid network");
57
+ }
58
+ if (typeof obj.amount !== "number" || !Number.isFinite(obj.amount) || !Number.isInteger(obj.amount) || obj.amount <= 0) {
59
+ throw new Error("X402-Challenge: amount must be a positive integer");
60
+ }
61
+ return {
62
+ nonce: obj.nonce,
63
+ payee: obj.payee,
64
+ amount: obj.amount,
65
+ network: obj.network
66
+ };
67
+ }
68
+
69
+ // src/limits.ts
70
+ var BFG_DAILY_CEILING_SATOSHIS = 1e10;
71
+ var BFG_PER_TX_CEILING_SATOSHIS = 1e9;
72
+ var WINDOW_MS = {
73
+ minute: 6e4,
74
+ hour: 36e5,
75
+ day: 864e5,
76
+ week: 6048e5
77
+ };
78
+ function windowToMs(window2) {
79
+ return WINDOW_MS[window2];
80
+ }
81
+ function makeLimits(windows, perTxMaxSatoshis, opts = {}) {
82
+ return {
83
+ windows,
84
+ perTxMaxSatoshis,
85
+ yellowLightThreshold: 0.8,
86
+ requirePerSitePrompt: false,
87
+ sitePolicies: {},
88
+ require2fa: {
89
+ onCircuitBreakerReset: true,
90
+ onTierChange: true,
91
+ onHighValueTx: false,
92
+ highValueThreshold: 0,
93
+ onNewSiteApproval: false
94
+ },
95
+ ...opts
96
+ };
31
97
  }
98
+ var TOO_YOUNG_TO_DIE = {
99
+ interactive: makeLimits(
100
+ [{ window: "day", maxSatoshis: 1e8, maxTransactions: Infinity }],
101
+ 1e8
102
+ ),
103
+ programmatic: makeLimits(
104
+ [{ window: "day", maxSatoshis: 1e8, maxTransactions: Infinity }],
105
+ 1e6
106
+ )
107
+ };
108
+ var HEY_NOT_TOO_ROUGH = {
109
+ interactive: makeLimits(
110
+ [
111
+ { window: "day", maxSatoshis: 1e8, maxTransactions: 100 },
112
+ { window: "week", maxSatoshis: 5e8, maxTransactions: 500 }
113
+ ],
114
+ 1e7,
115
+ {
116
+ requirePerSitePrompt: true,
117
+ require2fa: {
118
+ onCircuitBreakerReset: true,
119
+ onTierChange: true,
120
+ onHighValueTx: true,
121
+ highValueThreshold: 5e7,
122
+ onNewSiteApproval: true
123
+ }
124
+ }
125
+ ),
126
+ programmatic: makeLimits(
127
+ [
128
+ { window: "day", maxSatoshis: 1e8, maxTransactions: 1e4 },
129
+ { window: "week", maxSatoshis: 5e8, maxTransactions: 5e4 }
130
+ ],
131
+ 1e5,
132
+ {
133
+ requirePerSitePrompt: true,
134
+ require2fa: {
135
+ onCircuitBreakerReset: true,
136
+ onTierChange: true,
137
+ onHighValueTx: true,
138
+ highValueThreshold: 5e7,
139
+ onNewSiteApproval: true
140
+ }
141
+ }
142
+ )
143
+ };
144
+ var HURT_ME_PLENTY = {
145
+ interactive: makeLimits(
146
+ [
147
+ { window: "minute", maxSatoshis: 5e6, maxTransactions: 10 },
148
+ { window: "hour", maxSatoshis: 5e7, maxTransactions: 60 },
149
+ { window: "day", maxSatoshis: 2e8, maxTransactions: 200 },
150
+ { window: "week", maxSatoshis: 1e9, maxTransactions: 1e3 }
151
+ ],
152
+ 2e7,
153
+ {
154
+ requirePerSitePrompt: true,
155
+ require2fa: {
156
+ onCircuitBreakerReset: true,
157
+ onTierChange: true,
158
+ onHighValueTx: true,
159
+ highValueThreshold: 1e8,
160
+ onNewSiteApproval: true
161
+ }
162
+ }
163
+ ),
164
+ programmatic: makeLimits(
165
+ [
166
+ { window: "minute", maxSatoshis: 5e6, maxTransactions: 1e3 },
167
+ { window: "hour", maxSatoshis: 5e7, maxTransactions: 6e3 },
168
+ { window: "day", maxSatoshis: 2e8, maxTransactions: 2e4 },
169
+ { window: "week", maxSatoshis: 1e9, maxTransactions: 1e5 }
170
+ ],
171
+ 2e5,
172
+ {
173
+ requirePerSitePrompt: true,
174
+ require2fa: {
175
+ onCircuitBreakerReset: true,
176
+ onTierChange: true,
177
+ onHighValueTx: true,
178
+ highValueThreshold: 1e8,
179
+ onNewSiteApproval: true
180
+ }
181
+ }
182
+ )
183
+ };
184
+ var ULTRA_VIOLENCE = {
185
+ interactive: makeLimits(
186
+ [...HURT_ME_PLENTY.interactive.windows],
187
+ HURT_ME_PLENTY.interactive.perTxMaxSatoshis,
188
+ {
189
+ requirePerSitePrompt: true,
190
+ require2fa: {
191
+ onCircuitBreakerReset: true,
192
+ onTierChange: true,
193
+ onHighValueTx: false,
194
+ highValueThreshold: 0,
195
+ onNewSiteApproval: true
196
+ }
197
+ }
198
+ ),
199
+ programmatic: makeLimits(
200
+ [...HURT_ME_PLENTY.programmatic.windows],
201
+ HURT_ME_PLENTY.programmatic.perTxMaxSatoshis,
202
+ {
203
+ requirePerSitePrompt: true,
204
+ require2fa: {
205
+ onCircuitBreakerReset: true,
206
+ onTierChange: true,
207
+ onHighValueTx: false,
208
+ highValueThreshold: 0,
209
+ onNewSiteApproval: true
210
+ }
211
+ }
212
+ )
213
+ };
214
+ var NIGHTMARE = {
215
+ interactive: makeLimits([], BFG_PER_TX_CEILING_SATOSHIS),
216
+ programmatic: makeLimits([], BFG_PER_TX_CEILING_SATOSHIS)
217
+ };
218
+ var TIER_PRESETS = {
219
+ "I'm Too Young to Die": TOO_YOUNG_TO_DIE,
220
+ "Hey, Not Too Rough": HEY_NOT_TOO_ROUGH,
221
+ "Hurt Me Plenty": HURT_ME_PLENTY,
222
+ "Ultra-Violence": ULTRA_VIOLENCE,
223
+ "Nightmare!": NIGHTMARE
224
+ };
225
+ function cloneSpendLimits(preset) {
226
+ return {
227
+ ...preset,
228
+ windows: preset.windows.map((w) => ({ ...w })),
229
+ require2fa: { ...preset.require2fa },
230
+ sitePolicies: { ...preset.sitePolicies }
231
+ };
232
+ }
233
+ function resolveSpendLimits(tier = "Hey, Not Too Rough", mode = "interactive", overrides) {
234
+ const preset = TIER_PRESETS[tier][mode];
235
+ const base = cloneSpendLimits(preset);
236
+ if (!overrides) return base;
237
+ return {
238
+ ...base,
239
+ ...overrides,
240
+ // Deep-merge require2fa if provided
241
+ require2fa: overrides.require2fa ? { ...base.require2fa, ...overrides.require2fa } : base.require2fa,
242
+ // Deep-merge sitePolicies if provided
243
+ sitePolicies: overrides.sitePolicies ? { ...base.sitePolicies, ...overrides.sitePolicies } : base.sitePolicies
244
+ };
245
+ }
246
+ var RateLimiter = class {
247
+ constructor(limits, state, now) {
248
+ this.limits = limits;
249
+ this.entries = state?.entries ?? [];
250
+ this.broken = state?.circuitBroken ?? false;
251
+ this.now = now ?? Date.now;
252
+ }
253
+ check(challenge, origin) {
254
+ if (this.broken) {
255
+ return { action: "block", reason: "Circuit breaker tripped \u2014 call resetLimits() to clear", severity: "trip" };
256
+ }
257
+ if (!Number.isFinite(challenge.amount) || !Number.isInteger(challenge.amount) || challenge.amount <= 0) {
258
+ return { action: "block", reason: "Invalid transaction amount rejected", severity: "reject" };
259
+ }
260
+ if (challenge.amount > BFG_PER_TX_CEILING_SATOSHIS) {
261
+ return { action: "block", reason: `Exceeds BFG per-tx ceiling (${BFG_PER_TX_CEILING_SATOSHIS} sats)`, severity: "reject" };
262
+ }
263
+ const dayAgo = this.now() - WINDOW_MS.day;
264
+ const dailyTotal = this.sumSatoshis(dayAgo);
265
+ if (dailyTotal + challenge.amount > BFG_DAILY_CEILING_SATOSHIS) {
266
+ return { action: "block", reason: `Exceeds BFG daily ceiling (${BFG_DAILY_CEILING_SATOSHIS} sats)`, severity: "trip" };
267
+ }
268
+ if (challenge.amount > this.limits.perTxMaxSatoshis) {
269
+ return { action: "block", reason: `Exceeds per-tx limit (${this.limits.perTxMaxSatoshis} sats)`, severity: "reject" };
270
+ }
271
+ const isCustomSite = this.hasCustomPolicy(origin);
272
+ const effectiveLimits = this.effectiveWindows(origin);
273
+ const effectivePerTx = this.effectivePerTxMax(origin);
274
+ if (effectivePerTx !== void 0 && challenge.amount > effectivePerTx) {
275
+ return { action: "block", reason: `Exceeds per-tx limit for ${origin} (${effectivePerTx} sats)`, severity: "reject" };
276
+ }
277
+ let yellowLight;
278
+ for (const wl of effectiveLimits) {
279
+ const cutoff = this.now() - windowToMs(wl.window);
280
+ const windowEntries = this.entriesInWindow(cutoff, isCustomSite ? origin : void 0);
281
+ const totalSats = windowEntries.reduce((sum, e) => sum + e.satoshis, 0);
282
+ const totalTx = windowEntries.length;
283
+ if (totalSats + challenge.amount > wl.maxSatoshis) {
284
+ return { action: "block", reason: `Exceeds ${wl.window} sats limit (${wl.maxSatoshis})`, severity: "window" };
285
+ }
286
+ if (totalTx + 1 > wl.maxTransactions) {
287
+ return { action: "block", reason: `Exceeds ${wl.window} tx count limit (${wl.maxTransactions})`, severity: "window" };
288
+ }
289
+ if (this.limits.yellowLightThreshold < 1 && !yellowLight && totalSats + challenge.amount > wl.maxSatoshis * this.limits.yellowLightThreshold) {
290
+ yellowLight = {
291
+ origin,
292
+ currentSpend: totalSats,
293
+ limit: wl.maxSatoshis,
294
+ window: wl.window,
295
+ challenge
296
+ };
297
+ }
298
+ }
299
+ if (yellowLight) {
300
+ return { action: "yellow-light", detail: yellowLight };
301
+ }
302
+ return { action: "allow" };
303
+ }
304
+ record(entry) {
305
+ this.entries.push(entry);
306
+ this.prune();
307
+ }
308
+ trip() {
309
+ this.broken = true;
310
+ }
311
+ reset() {
312
+ this.broken = false;
313
+ }
314
+ isBroken() {
315
+ return this.broken;
316
+ }
317
+ getState() {
318
+ return {
319
+ entries: [...this.entries],
320
+ circuitBroken: this.broken,
321
+ hmac: ""
322
+ };
323
+ }
324
+ hasCustomPolicy(origin) {
325
+ const policy = this.limits.sitePolicies[origin];
326
+ return policy?.action === "custom" && !!policy.limits;
327
+ }
328
+ effectiveWindows(origin) {
329
+ const policy = this.limits.sitePolicies[origin];
330
+ if (policy?.action === "custom" && policy.limits) {
331
+ return policy.limits;
332
+ }
333
+ return this.limits.windows;
334
+ }
335
+ effectivePerTxMax(origin) {
336
+ const policy = this.limits.sitePolicies[origin];
337
+ if (policy?.action === "custom" && policy.perTxMaxSatoshis !== void 0) {
338
+ return policy.perTxMaxSatoshis;
339
+ }
340
+ return void 0;
341
+ }
342
+ entriesInWindow(cutoff, filterOrigin) {
343
+ return this.entries.filter(
344
+ (e) => e.timestamp >= cutoff && (filterOrigin === void 0 || e.origin === filterOrigin)
345
+ );
346
+ }
347
+ sumSatoshis(cutoff) {
348
+ return this.entries.filter((e) => e.timestamp >= cutoff).reduce((sum, e) => sum + e.satoshis, 0);
349
+ }
350
+ prune() {
351
+ let longestWindow = WINDOW_MS.day;
352
+ for (const wl of this.limits.windows) {
353
+ longestWindow = Math.max(longestWindow, windowToMs(wl.window));
354
+ }
355
+ if (this.limits.sitePolicies) {
356
+ for (const policy of Object.values(this.limits.sitePolicies)) {
357
+ if (policy?.action === "custom" && Array.isArray(policy.limits)) {
358
+ for (const wl of policy.limits) {
359
+ longestWindow = Math.max(longestWindow, windowToMs(wl.window));
360
+ }
361
+ }
362
+ }
363
+ }
364
+ const cutoff = this.now() - longestWindow;
365
+ this.entries = this.entries.filter((e) => e.timestamp >= cutoff);
366
+ }
367
+ };
368
+
369
+ // src/site-policy.ts
370
+ var defaultSitePrompt = async (origin) => {
371
+ if (typeof globalThis.confirm !== "function") return "global";
372
+ const allow = globalThis.confirm(
373
+ `First payment to ${origin}.
374
+
375
+ Use your global spending limits for this site?
376
+
377
+ OK = Use global limits
378
+ Cancel = Block this site`
379
+ );
380
+ return allow ? "global" : "block";
381
+ };
382
+ async function resolveSitePolicy(origin, limits, twoFactorProvider, promptFn = defaultSitePrompt) {
383
+ const existing = limits.sitePolicies[origin];
384
+ if (existing) return existing;
385
+ if (!limits.requirePerSitePrompt) {
386
+ return { origin, action: "global" };
387
+ }
388
+ if (limits.require2fa.onNewSiteApproval) {
389
+ if (!twoFactorProvider) {
390
+ return { origin, action: "block" };
391
+ }
392
+ const verified = await twoFactorProvider.verify({
393
+ type: "new-site-approval",
394
+ origin
395
+ });
396
+ if (!verified) {
397
+ return { origin, action: "block" };
398
+ }
399
+ }
400
+ const action = await promptFn(origin);
401
+ return { origin, action };
402
+ }
403
+
404
+ // src/storage.ts
405
+ var STATE_KEY = "x402:limit-state";
406
+ var POLICIES_KEY = "x402:site-policies";
407
+ async function computeHmac(data, key) {
408
+ const cryptoKey = await crypto.subtle.importKey(
409
+ "raw",
410
+ key,
411
+ { name: "HMAC", hash: "SHA-256" },
412
+ false,
413
+ ["sign"]
414
+ );
415
+ const encoded = new TextEncoder().encode(data);
416
+ const sig = await crypto.subtle.sign("HMAC", cryptoKey, encoded);
417
+ return Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
418
+ }
419
+ function serializeForHmac(state) {
420
+ return JSON.stringify({
421
+ entries: state.entries,
422
+ circuitBroken: state.circuitBroken
423
+ });
424
+ }
425
+ var LocalStorageAdapter = class {
426
+ constructor(keyDeriver, storage) {
427
+ this.keyDeriver = keyDeriver;
428
+ this.storage = storage ?? globalThis.localStorage;
429
+ }
430
+ async load() {
431
+ const raw = this.storage.getItem(STATE_KEY);
432
+ if (!raw) return null;
433
+ let state;
434
+ try {
435
+ state = JSON.parse(raw);
436
+ } catch {
437
+ console.warn("x402: limit state JSON parse failed \u2014 treating as tampered");
438
+ return { entries: [], circuitBroken: true, hmac: "" };
439
+ }
440
+ if (this.keyDeriver) {
441
+ if (!state.hmac) {
442
+ console.warn("x402: limit state missing HMAC \u2014 treating as tampered");
443
+ return { entries: [], circuitBroken: true, hmac: "" };
444
+ }
445
+ const key = await this.keyDeriver();
446
+ const expected = await computeHmac(serializeForHmac(state), key);
447
+ if (expected !== state.hmac) {
448
+ console.warn("x402: limit state HMAC mismatch \u2014 state may have been tampered with");
449
+ return { entries: [], circuitBroken: true, hmac: "" };
450
+ }
451
+ }
452
+ state.entries = (Array.isArray(state.entries) ? state.entries : []).filter(
453
+ (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
454
+ );
455
+ return state;
456
+ }
457
+ async save(state) {
458
+ if (this.keyDeriver) {
459
+ const key = await this.keyDeriver();
460
+ state.hmac = await computeHmac(serializeForHmac(state), key);
461
+ }
462
+ this.storage.setItem(STATE_KEY, JSON.stringify(state));
463
+ }
464
+ async loadSitePolicies() {
465
+ const raw = this.storage.getItem(POLICIES_KEY);
466
+ if (!raw) return {};
467
+ try {
468
+ return JSON.parse(raw);
469
+ } catch {
470
+ return {};
471
+ }
472
+ }
473
+ async saveSitePolicies(policies) {
474
+ this.storage.setItem(POLICIES_KEY, JSON.stringify(policies));
475
+ }
476
+ };
32
477
 
33
478
  // src/x402-fetch.ts
479
+ var BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
480
+ function base58DecodeCheck(address) {
481
+ let leadingZeros = 0;
482
+ for (const c of address) {
483
+ if (c === "1") leadingZeros++;
484
+ else break;
485
+ }
486
+ let n = BigInt(0);
487
+ for (const c of address) {
488
+ const i = BASE58_ALPHABET.indexOf(c);
489
+ if (i < 0) throw new Error(`Invalid Base58 character: ${c}`);
490
+ n = n * 58n + BigInt(i);
491
+ }
492
+ const hexFromBigint = n === 0n ? "" : n.toString(16);
493
+ const paddedHex = hexFromBigint.length % 2 ? "0" + hexFromBigint : hexFromBigint;
494
+ const bigintBytes = [];
495
+ for (let i = 0; i < paddedHex.length; i += 2) {
496
+ bigintBytes.push(parseInt(paddedHex.slice(i, i + 2), 16));
497
+ }
498
+ const allBytes = new Uint8Array(leadingZeros + bigintBytes.length);
499
+ allBytes.set(bigintBytes, leadingZeros);
500
+ if (allBytes.length !== 25) {
501
+ throw new Error(`Invalid address length: expected 25 bytes, got ${allBytes.length}`);
502
+ }
503
+ const body = allBytes.slice(0, 21);
504
+ const checksum = allBytes.slice(21);
505
+ const version = allBytes[0];
506
+ if (version !== 0 && version !== 111) {
507
+ throw new Error(`Unsupported address version: 0x${version.toString(16).padStart(2, "0")}`);
508
+ }
509
+ return { version, payload: body.slice(1) };
510
+ }
511
+ function payeeAddressToLockingScript(address) {
512
+ const { payload } = base58DecodeCheck(address);
513
+ if (payload.length !== 20) {
514
+ throw new Error(`Invalid pubkey hash length: expected 20 bytes, got ${payload.length}`);
515
+ }
516
+ const pubkeyHash = Array.from(payload).map((b) => b.toString(16).padStart(2, "0")).join("");
517
+ return `76a914${pubkeyHash}88ac`;
518
+ }
519
+ async function defaultConstructProof(challenge) {
520
+ const cwi = globalThis.CWI;
521
+ if (!cwi || typeof cwi.createAction !== "function") {
522
+ throw new Error(
523
+ "No BRC-100 wallet detected. Install a CWI-compliant browser extension or provide a custom proofConstructor in X402Config."
524
+ );
525
+ }
526
+ const result = await cwi.createAction({
527
+ description: `x402 payment: ${challenge.amount} sats to ${challenge.payee}`,
528
+ outputs: [{
529
+ satoshis: challenge.amount,
530
+ lockingScript: payeeAddressToLockingScript(challenge.payee),
531
+ description: `Payment to ${challenge.payee}`
532
+ }],
533
+ labels: ["x402-payment"],
534
+ options: {
535
+ returnTXIDOnly: false,
536
+ noSend: false
537
+ }
538
+ });
539
+ if (!result || !result.txid) {
540
+ throw new Error("Wallet declined payment or returned invalid result");
541
+ }
542
+ if (!result.rawTx || typeof result.rawTx !== "string" || result.rawTx.length === 0) {
543
+ throw new Error("Wallet did not return raw transaction");
544
+ }
545
+ return {
546
+ txid: result.txid,
547
+ rawTx: result.rawTx
548
+ };
549
+ }
550
+ function createMutex() {
551
+ let chain = Promise.resolve();
552
+ return (fn) => {
553
+ const result = chain.then(fn, fn);
554
+ chain = result.then(() => {
555
+ }, () => {
556
+ });
557
+ return result;
558
+ };
559
+ }
560
+ function createX402Fetch(config = {}) {
561
+ const tier = config.tier ?? "Hey, Not Too Rough";
562
+ const mode = config.mode ?? "interactive";
563
+ if (tier === "Nightmare!" && config.nightmareConfirmation !== "NIGHTMARE") {
564
+ throw new Error('Nightmare! tier requires nightmareConfirmation: "NIGHTMARE"');
565
+ }
566
+ const limits = resolveSpendLimits(tier, mode, config.limits);
567
+ const storage = config.storage ?? new LocalStorageAdapter();
568
+ const twoFactor = config.twoFactorProvider;
569
+ const constructProof = config.proofConstructor ?? defaultConstructProof;
570
+ const nowFn = config.now ?? Date.now;
571
+ const mutex = createMutex();
572
+ const needs2fa = limits.require2fa;
573
+ if (!twoFactor && (needs2fa.onCircuitBreakerReset || needs2fa.onHighValueTx || needs2fa.onNewSiteApproval || needs2fa.onTierChange)) {
574
+ console.warn("x402: tier requires 2FA but no twoFactorProvider configured \u2014 2FA-gated actions will be blocked");
575
+ }
576
+ let limiter;
577
+ let initialised = false;
578
+ async function ensureInitialised() {
579
+ if (limiter && initialised) return limiter;
580
+ const state = await storage.load();
581
+ limiter = new RateLimiter(limits, state ?? void 0, nowFn);
582
+ const policies = await storage.loadSitePolicies();
583
+ Object.assign(limits.sitePolicies, policies);
584
+ initialised = true;
585
+ return limiter;
586
+ }
587
+ async function persist(rl) {
588
+ await storage.save(rl.getState());
589
+ await storage.saveSitePolicies(limits.sitePolicies);
590
+ }
591
+ const fetchFn = async function x402Fetch2(input, init) {
592
+ const response = await fetch(input, init);
593
+ if (response.status !== 402) return response;
594
+ const challengeHeader = response.headers.get("X402-Challenge");
595
+ if (!challengeHeader) return response;
596
+ let challenge;
597
+ try {
598
+ challenge = parseChallenge(challengeHeader);
599
+ } catch {
600
+ return response;
601
+ }
602
+ const origin = extractOrigin(input);
603
+ return mutex(async () => {
604
+ const rl = await ensureInitialised();
605
+ const sitePolicy = await resolveSitePolicy(origin, limits, twoFactor);
606
+ if (sitePolicy.action === "block") return response;
607
+ if (!limits.sitePolicies[origin]) {
608
+ limits.sitePolicies[origin] = sitePolicy;
609
+ await storage.saveSitePolicies(limits.sitePolicies);
610
+ }
611
+ const result = rl.check(challenge, origin);
612
+ if (result.action === "block") {
613
+ if (result.severity === "trip") {
614
+ rl.trip();
615
+ await persist(rl);
616
+ config.onLimitReached?.(result.reason);
617
+ return response;
618
+ }
619
+ if (result.severity === "window" && twoFactor) {
620
+ config.onLimitReached?.(result.reason);
621
+ const override = await twoFactor.verify({
622
+ type: "limit-override",
623
+ amount: challenge.amount,
624
+ origin,
625
+ reason: result.reason
626
+ });
627
+ if (override) {
628
+ } else {
629
+ return response;
630
+ }
631
+ } else {
632
+ config.onLimitReached?.(result.reason);
633
+ return response;
634
+ }
635
+ }
636
+ if (result.action === "yellow-light") {
637
+ const proceed = config.onYellowLight ? await config.onYellowLight(result.detail) : false;
638
+ if (!proceed) return response;
639
+ }
640
+ if (limits.require2fa.onHighValueTx && challenge.amount > limits.require2fa.highValueThreshold) {
641
+ if (!twoFactor) return response;
642
+ const verified = await twoFactor.verify({
643
+ type: "high-value-tx",
644
+ amount: challenge.amount,
645
+ origin
646
+ });
647
+ if (!verified) return response;
648
+ }
649
+ const proof = await constructProof(challenge);
650
+ rl.record({
651
+ timestamp: nowFn(),
652
+ origin,
653
+ satoshis: challenge.amount,
654
+ txid: proof.txid
655
+ });
656
+ await persist(rl);
657
+ const headers = new Headers(init?.headers);
658
+ headers.set("X402-Proof", JSON.stringify(proof));
659
+ return fetch(input, { ...init, headers });
660
+ });
661
+ };
662
+ fetchFn.resetLimits = async () => {
663
+ const rl = await ensureInitialised();
664
+ if (limits.require2fa.onCircuitBreakerReset) {
665
+ if (!twoFactor) throw new Error("2FA required for circuit breaker reset but no twoFactorProvider configured");
666
+ const verified = await twoFactor.verify({ type: "circuit-breaker-reset" });
667
+ if (!verified) throw new Error("2FA verification failed for circuit breaker reset");
668
+ }
669
+ rl.reset();
670
+ await persist(rl);
671
+ };
672
+ fetchFn.getState = () => {
673
+ if (!limiter) return { entries: [], circuitBroken: false };
674
+ const state = limiter.getState();
675
+ return { entries: state.entries, circuitBroken: state.circuitBroken };
676
+ };
677
+ return fetchFn;
678
+ }
679
+ var singleton;
34
680
  async function x402Fetch(input, init) {
35
- const response = await fetch(input, init);
36
- if (response.status !== 402) {
37
- return response;
681
+ if (!singleton) singleton = createX402Fetch();
682
+ return singleton(input, init);
683
+ }
684
+ function resolveRelativeUrl(url) {
685
+ const loc = globalThis.location;
686
+ if (loc?.href) {
687
+ return new URL(url, loc.href).origin;
688
+ }
689
+ return "unknown";
690
+ }
691
+ function extractOrigin(input) {
692
+ if (input instanceof URL) return input.origin;
693
+ if (typeof input === "string") {
694
+ try {
695
+ return new URL(input).origin;
696
+ } catch {
697
+ try {
698
+ return resolveRelativeUrl(input);
699
+ } catch {
700
+ return "unknown";
701
+ }
702
+ }
38
703
  }
39
- const challengeHeader = response.headers.get("X402-Challenge");
40
- if (!challengeHeader) {
41
- return response;
704
+ try {
705
+ return new URL(input.url).origin;
706
+ } catch {
707
+ try {
708
+ return resolveRelativeUrl(input.url);
709
+ } catch {
710
+ return "unknown";
711
+ }
42
712
  }
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
713
  }
49
- async function constructProof(challenge) {
50
- throw new Error("Not implemented \u2014 requires BRC-100 wallet (window.CWI)");
714
+
715
+ // src/two-factor.ts
716
+ var WalletTwoFactorProvider = class {
717
+ async verify(action) {
718
+ if (typeof window === "undefined" || !window.CWI) {
719
+ return this.promptFallback(action);
720
+ }
721
+ const challengeData = `x402-2fa:${JSON.stringify(action)}:${Date.now()}`;
722
+ try {
723
+ const sig = await window.CWI.createSignature({
724
+ data: new TextEncoder().encode(challengeData),
725
+ protocolID: [1, "x402-2fa"],
726
+ keyID: "spending-limits"
727
+ });
728
+ return sig !== null;
729
+ } catch {
730
+ return false;
731
+ }
732
+ }
733
+ promptFallback(action) {
734
+ if (typeof window === "undefined" || !window.prompt) return false;
735
+ const message = describeAction(action);
736
+ const result = window.prompt(`${message}
737
+
738
+ Type CONFIRM to proceed:`);
739
+ return result === "CONFIRM";
740
+ }
741
+ };
742
+ function describeAction(action) {
743
+ switch (action.type) {
744
+ case "circuit-breaker-reset":
745
+ return "Reset spending circuit breaker? This re-enables automated payments.";
746
+ case "tier-change":
747
+ return `Change spending tier from "${action.from}" to "${action.to}"?`;
748
+ case "high-value-tx":
749
+ return `Approve high-value payment of ${action.amount} sats to ${action.origin}?`;
750
+ case "new-site-approval":
751
+ return `Allow automated payments to ${action.origin}?`;
752
+ case "limit-override":
753
+ return `Spending limit reached: ${action.reason}
754
+ Allow this payment of ${action.amount} sats to ${action.origin}?`;
755
+ }
51
756
  }
52
757
  // Annotate the CommonJS export names for ESM import in node:
53
758
  0 && (module.exports = {
759
+ BFG_DAILY_CEILING_SATOSHIS,
760
+ BFG_PER_TX_CEILING_SATOSHIS,
761
+ LocalStorageAdapter,
762
+ RateLimiter,
763
+ TIER_PRESETS,
764
+ WalletTwoFactorProvider,
765
+ createX402Fetch,
54
766
  parseChallenge,
767
+ resolveSitePolicy,
768
+ resolveSpendLimits,
55
769
  x402Fetch
56
770
  });