bsv-x402 0.2.0 → 0.4.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/index.cjs CHANGED
@@ -26,7 +26,9 @@ __export(index_exports, {
26
26
  RateLimiter: () => RateLimiter,
27
27
  TIER_PRESETS: () => TIER_PRESETS,
28
28
  WalletTwoFactorProvider: () => WalletTwoFactorProvider,
29
+ constructBrc105Proof: () => constructBrc105Proof,
29
30
  createX402Fetch: () => createX402Fetch,
31
+ parseBrc105Challenge: () => parseBrc105Challenge,
30
32
  parseChallenge: () => parseChallenge,
31
33
  resolveSitePolicy: () => resolveSitePolicy,
32
34
  resolveSpendLimits: () => resolveSpendLimits,
@@ -34,6 +36,534 @@ __export(index_exports, {
34
36
  });
35
37
  module.exports = __toCommonJS(index_exports);
36
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 authIdentityKey = response.headers.get("x-bsv-auth-identity-key") || null;
57
+ const paymentIdentityKey = response.headers.get("x-bsv-payment-identity-key") || null;
58
+ const authenticated = authIdentityKey !== null && authIdentityKey.length > 0;
59
+ const serverIdentityKey = authIdentityKey || paymentIdentityKey;
60
+ if (serverIdentityKey === null || serverIdentityKey.length === 0) {
61
+ throw new Error("BRC-105: missing identity key (expected x-bsv-auth-identity-key or x-bsv-payment-identity-key)");
62
+ }
63
+ if (!/^0[23][0-9a-fA-F]{64}$/.test(serverIdentityKey)) {
64
+ throw new Error("BRC-105: identity key must be a 33-byte compressed public key (hex)");
65
+ }
66
+ const derivationPrefix = response.headers.get("x-bsv-payment-derivation-prefix");
67
+ if (derivationPrefix === null || derivationPrefix.length === 0) {
68
+ throw new Error("BRC-105: missing or empty x-bsv-payment-derivation-prefix header");
69
+ }
70
+ return {
71
+ version,
72
+ satoshisRequired,
73
+ serverIdentityKey,
74
+ derivationPrefix,
75
+ authenticated
76
+ };
77
+ }
78
+
79
+ // src/brc105-proof.ts
80
+ function bytesToHex(bytes) {
81
+ return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
82
+ }
83
+ function hexToBytes(hex) {
84
+ if (hex.length % 2 !== 0) throw new Error("Hex string must have even length");
85
+ const bytes = new Uint8Array(hex.length / 2);
86
+ for (let i = 0; i < hex.length; i += 2) {
87
+ bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);
88
+ }
89
+ return bytes;
90
+ }
91
+ function bytesToBase64(bytes) {
92
+ let binary = "";
93
+ for (const b of bytes) binary += String.fromCharCode(b);
94
+ return btoa(binary);
95
+ }
96
+ function numberArrayToBase64(arr) {
97
+ return bytesToBase64(new Uint8Array(arr));
98
+ }
99
+ var RIPEMD160_CONSTANTS = {
100
+ // Round constants for left and right paths
101
+ KL: [0, 1518500249, 1859775393, 2400959708, 2840853838],
102
+ KR: [1352829926, 1548603684, 1836072691, 2053994217, 0],
103
+ // Message schedule (word index per step)
104
+ RL: [
105
+ 0,
106
+ 1,
107
+ 2,
108
+ 3,
109
+ 4,
110
+ 5,
111
+ 6,
112
+ 7,
113
+ 8,
114
+ 9,
115
+ 10,
116
+ 11,
117
+ 12,
118
+ 13,
119
+ 14,
120
+ 15,
121
+ 7,
122
+ 4,
123
+ 13,
124
+ 1,
125
+ 10,
126
+ 6,
127
+ 15,
128
+ 3,
129
+ 12,
130
+ 0,
131
+ 9,
132
+ 5,
133
+ 2,
134
+ 14,
135
+ 11,
136
+ 8,
137
+ 3,
138
+ 10,
139
+ 14,
140
+ 4,
141
+ 9,
142
+ 15,
143
+ 8,
144
+ 1,
145
+ 2,
146
+ 7,
147
+ 0,
148
+ 6,
149
+ 13,
150
+ 11,
151
+ 5,
152
+ 12,
153
+ 1,
154
+ 9,
155
+ 11,
156
+ 10,
157
+ 0,
158
+ 8,
159
+ 12,
160
+ 4,
161
+ 13,
162
+ 3,
163
+ 7,
164
+ 15,
165
+ 14,
166
+ 5,
167
+ 6,
168
+ 2,
169
+ 4,
170
+ 0,
171
+ 5,
172
+ 9,
173
+ 7,
174
+ 12,
175
+ 2,
176
+ 10,
177
+ 14,
178
+ 1,
179
+ 3,
180
+ 8,
181
+ 11,
182
+ 6,
183
+ 15,
184
+ 13
185
+ ],
186
+ RR: [
187
+ 5,
188
+ 14,
189
+ 7,
190
+ 0,
191
+ 9,
192
+ 2,
193
+ 11,
194
+ 4,
195
+ 13,
196
+ 6,
197
+ 15,
198
+ 8,
199
+ 1,
200
+ 10,
201
+ 3,
202
+ 12,
203
+ 6,
204
+ 11,
205
+ 3,
206
+ 7,
207
+ 0,
208
+ 13,
209
+ 5,
210
+ 10,
211
+ 14,
212
+ 15,
213
+ 8,
214
+ 12,
215
+ 4,
216
+ 9,
217
+ 1,
218
+ 2,
219
+ 15,
220
+ 5,
221
+ 1,
222
+ 3,
223
+ 7,
224
+ 14,
225
+ 6,
226
+ 9,
227
+ 11,
228
+ 8,
229
+ 12,
230
+ 2,
231
+ 10,
232
+ 0,
233
+ 4,
234
+ 13,
235
+ 8,
236
+ 6,
237
+ 4,
238
+ 1,
239
+ 3,
240
+ 11,
241
+ 15,
242
+ 0,
243
+ 5,
244
+ 12,
245
+ 2,
246
+ 13,
247
+ 9,
248
+ 7,
249
+ 10,
250
+ 14,
251
+ 12,
252
+ 15,
253
+ 10,
254
+ 4,
255
+ 1,
256
+ 5,
257
+ 8,
258
+ 7,
259
+ 6,
260
+ 2,
261
+ 13,
262
+ 14,
263
+ 0,
264
+ 3,
265
+ 9,
266
+ 11
267
+ ],
268
+ // Rotation amounts
269
+ SL: [
270
+ 11,
271
+ 14,
272
+ 15,
273
+ 12,
274
+ 5,
275
+ 8,
276
+ 7,
277
+ 9,
278
+ 11,
279
+ 13,
280
+ 14,
281
+ 15,
282
+ 6,
283
+ 7,
284
+ 9,
285
+ 8,
286
+ 7,
287
+ 6,
288
+ 8,
289
+ 13,
290
+ 11,
291
+ 9,
292
+ 7,
293
+ 15,
294
+ 7,
295
+ 12,
296
+ 15,
297
+ 9,
298
+ 11,
299
+ 7,
300
+ 13,
301
+ 12,
302
+ 11,
303
+ 13,
304
+ 6,
305
+ 7,
306
+ 14,
307
+ 9,
308
+ 13,
309
+ 15,
310
+ 14,
311
+ 8,
312
+ 13,
313
+ 6,
314
+ 5,
315
+ 12,
316
+ 7,
317
+ 5,
318
+ 11,
319
+ 12,
320
+ 14,
321
+ 15,
322
+ 14,
323
+ 15,
324
+ 9,
325
+ 8,
326
+ 9,
327
+ 14,
328
+ 5,
329
+ 6,
330
+ 8,
331
+ 6,
332
+ 5,
333
+ 12,
334
+ 9,
335
+ 15,
336
+ 5,
337
+ 11,
338
+ 6,
339
+ 8,
340
+ 13,
341
+ 12,
342
+ 5,
343
+ 12,
344
+ 13,
345
+ 14,
346
+ 11,
347
+ 8,
348
+ 5,
349
+ 6
350
+ ],
351
+ SR: [
352
+ 8,
353
+ 9,
354
+ 9,
355
+ 11,
356
+ 13,
357
+ 15,
358
+ 15,
359
+ 5,
360
+ 7,
361
+ 7,
362
+ 8,
363
+ 11,
364
+ 14,
365
+ 14,
366
+ 12,
367
+ 6,
368
+ 9,
369
+ 13,
370
+ 15,
371
+ 7,
372
+ 12,
373
+ 8,
374
+ 9,
375
+ 11,
376
+ 7,
377
+ 7,
378
+ 12,
379
+ 7,
380
+ 6,
381
+ 15,
382
+ 13,
383
+ 11,
384
+ 9,
385
+ 7,
386
+ 15,
387
+ 11,
388
+ 8,
389
+ 6,
390
+ 6,
391
+ 14,
392
+ 12,
393
+ 13,
394
+ 5,
395
+ 14,
396
+ 13,
397
+ 13,
398
+ 7,
399
+ 5,
400
+ 15,
401
+ 5,
402
+ 8,
403
+ 11,
404
+ 14,
405
+ 14,
406
+ 6,
407
+ 14,
408
+ 6,
409
+ 9,
410
+ 12,
411
+ 9,
412
+ 12,
413
+ 5,
414
+ 15,
415
+ 8,
416
+ 8,
417
+ 5,
418
+ 12,
419
+ 9,
420
+ 12,
421
+ 5,
422
+ 14,
423
+ 6,
424
+ 8,
425
+ 13,
426
+ 6,
427
+ 5,
428
+ 15,
429
+ 13,
430
+ 11,
431
+ 11
432
+ ]
433
+ };
434
+ function rotl32(x, n) {
435
+ return (x << n | x >>> 32 - n) >>> 0;
436
+ }
437
+ function f(j, x, y, z) {
438
+ if (j < 16) return (x ^ y ^ z) >>> 0;
439
+ if (j < 32) return (x & y | ~x & z) >>> 0;
440
+ if (j < 48) return ((x | ~y) ^ z) >>> 0;
441
+ if (j < 64) return (x & z | y & ~z) >>> 0;
442
+ return (x ^ (y | ~z)) >>> 0;
443
+ }
444
+ function ripemd160(message) {
445
+ const msgLen = message.length;
446
+ const bitLen = msgLen * 8;
447
+ const padLen = (55 - msgLen % 64 + 64) % 64 + 1;
448
+ const padded = new Uint8Array(msgLen + padLen + 8);
449
+ padded.set(message);
450
+ padded[msgLen] = 128;
451
+ const view = new DataView(padded.buffer);
452
+ view.setUint32(padded.length - 8, bitLen >>> 0, true);
453
+ view.setUint32(padded.length - 4, bitLen / 4294967296 >>> 0, true);
454
+ let h0 = 1732584193;
455
+ let h1 = 4023233417;
456
+ let h2 = 2562383102;
457
+ let h3 = 271733878;
458
+ let h4 = 3285377520;
459
+ const { KL, KR, RL, RR, SL, SR } = RIPEMD160_CONSTANTS;
460
+ for (let offset = 0; offset < padded.length; offset += 64) {
461
+ const w = new Uint32Array(16);
462
+ for (let i = 0; i < 16; i++) {
463
+ w[i] = view.getUint32(offset + i * 4, true);
464
+ }
465
+ let al = h0, bl = h1, cl = h2, dl = h3, el = h4;
466
+ let ar = h0, br = h1, cr = h2, dr = h3, er = h4;
467
+ for (let j = 0; j < 80; j++) {
468
+ const round = j >>> 4;
469
+ let t2 = al + f(j, bl, cl, dl) + w[RL[j]] + KL[round] >>> 0;
470
+ t2 = rotl32(t2, SL[j]) + el >>> 0;
471
+ al = el;
472
+ el = dl;
473
+ dl = rotl32(cl, 10);
474
+ cl = bl;
475
+ bl = t2;
476
+ t2 = ar + f(79 - j, br, cr, dr) + w[RR[j]] + KR[round] >>> 0;
477
+ t2 = rotl32(t2, SR[j]) + er >>> 0;
478
+ ar = er;
479
+ er = dr;
480
+ dr = rotl32(cr, 10);
481
+ cr = br;
482
+ br = t2;
483
+ }
484
+ const t = h1 + cl + dr >>> 0;
485
+ h1 = h2 + dl + er >>> 0;
486
+ h2 = h3 + el + ar >>> 0;
487
+ h3 = h4 + al + br >>> 0;
488
+ h4 = h0 + bl + cr >>> 0;
489
+ h0 = t;
490
+ }
491
+ const digest = new Uint8Array(20);
492
+ const dv = new DataView(digest.buffer);
493
+ dv.setUint32(0, h0, true);
494
+ dv.setUint32(4, h1, true);
495
+ dv.setUint32(8, h2, true);
496
+ dv.setUint32(12, h3, true);
497
+ dv.setUint32(16, h4, true);
498
+ return digest;
499
+ }
500
+ async function hash160(data) {
501
+ const sha256 = new Uint8Array(await crypto.subtle.digest("SHA-256", data));
502
+ return ripemd160(sha256);
503
+ }
504
+ async function pubkeyToP2PKHLockingScript(pubkeyHex) {
505
+ const pubkeyBytes = hexToBytes(pubkeyHex);
506
+ const pubkeyHash = await hash160(pubkeyBytes);
507
+ return `76a914${bytesToHex(pubkeyHash)}88ac`;
508
+ }
509
+ async function createDerivationSuffix(wallet) {
510
+ const randomBytes = new Uint8Array(16);
511
+ crypto.getRandomValues(randomBytes);
512
+ const firstHalf = Array.from(randomBytes);
513
+ const keyID = new TextDecoder().decode(randomBytes);
514
+ const { hmac } = await wallet.createHmac({
515
+ data: firstHalf,
516
+ protocolID: [2, "server hmac"],
517
+ keyID,
518
+ counterparty: "self"
519
+ });
520
+ const nonceBytes = [...firstHalf, ...hmac];
521
+ return numberArrayToBase64(nonceBytes);
522
+ }
523
+ async function constructBrc105Proof(challenge, wallet, origin) {
524
+ const { publicKey: clientIdentityKey } = await wallet.getPublicKey({ identityKey: true });
525
+ const derivationSuffix = await createDerivationSuffix(wallet);
526
+ const keyID = `${challenge.derivationPrefix} ${derivationSuffix}`;
527
+ const { publicKey: derivedPublicKey } = await wallet.getPublicKey({
528
+ protocolID: [2, "3241645161d8"],
529
+ keyID,
530
+ counterparty: challenge.serverIdentityKey
531
+ });
532
+ const lockingScript = await pubkeyToP2PKHLockingScript(derivedPublicKey);
533
+ const description = origin ? `Payment for request to ${origin}` : "BRC-105 payment";
534
+ const result = await wallet.createAction({
535
+ description,
536
+ outputs: [{
537
+ satoshis: challenge.satoshisRequired,
538
+ lockingScript,
539
+ outputDescription: "HTTP request payment",
540
+ customInstructions: JSON.stringify({
541
+ derivationPrefix: challenge.derivationPrefix,
542
+ derivationSuffix,
543
+ payee: challenge.serverIdentityKey
544
+ })
545
+ }],
546
+ options: {
547
+ randomizeOutputs: false
548
+ }
549
+ });
550
+ let transactionBase64;
551
+ if (result.tx && Array.isArray(result.tx) && result.tx.length > 0) {
552
+ transactionBase64 = numberArrayToBase64(result.tx);
553
+ } else if (result.rawTx && typeof result.rawTx === "string" && result.rawTx.length > 0) {
554
+ transactionBase64 = bytesToBase64(hexToBytes(result.rawTx));
555
+ } else {
556
+ throw new Error("Wallet returned no transaction data (neither tx nor rawTx)");
557
+ }
558
+ return {
559
+ derivationPrefix: challenge.derivationPrefix,
560
+ derivationSuffix,
561
+ transaction: transactionBase64,
562
+ clientIdentityKey,
563
+ txid: result.txid
564
+ };
565
+ }
566
+
37
567
  // src/challenge.ts
38
568
  function parseChallenge(header) {
39
569
  let parsed;
@@ -250,28 +780,29 @@ var RateLimiter = class {
250
780
  this.broken = state?.circuitBroken ?? false;
251
781
  this.now = now ?? Date.now;
252
782
  }
253
- check(challenge, origin) {
783
+ check(request, origin) {
254
784
  if (this.broken) {
255
785
  return { action: "block", reason: "Circuit breaker tripped \u2014 call resetLimits() to clear", severity: "trip" };
256
786
  }
257
- if (!Number.isFinite(challenge.amount) || !Number.isInteger(challenge.amount) || challenge.amount <= 0) {
787
+ const amount = request.amount;
788
+ if (!Number.isFinite(amount) || !Number.isInteger(amount) || amount <= 0) {
258
789
  return { action: "block", reason: "Invalid transaction amount rejected", severity: "reject" };
259
790
  }
260
- if (challenge.amount > BFG_PER_TX_CEILING_SATOSHIS) {
791
+ if (amount > BFG_PER_TX_CEILING_SATOSHIS) {
261
792
  return { action: "block", reason: `Exceeds BFG per-tx ceiling (${BFG_PER_TX_CEILING_SATOSHIS} sats)`, severity: "reject" };
262
793
  }
263
794
  const dayAgo = this.now() - WINDOW_MS.day;
264
795
  const dailyTotal = this.sumSatoshis(dayAgo);
265
- if (dailyTotal + challenge.amount > BFG_DAILY_CEILING_SATOSHIS) {
796
+ if (dailyTotal + amount > BFG_DAILY_CEILING_SATOSHIS) {
266
797
  return { action: "block", reason: `Exceeds BFG daily ceiling (${BFG_DAILY_CEILING_SATOSHIS} sats)`, severity: "trip" };
267
798
  }
268
- if (challenge.amount > this.limits.perTxMaxSatoshis) {
799
+ if (amount > this.limits.perTxMaxSatoshis) {
269
800
  return { action: "block", reason: `Exceeds per-tx limit (${this.limits.perTxMaxSatoshis} sats)`, severity: "reject" };
270
801
  }
271
802
  const isCustomSite = this.hasCustomPolicy(origin);
272
803
  const effectiveLimits = this.effectiveWindows(origin);
273
804
  const effectivePerTx = this.effectivePerTxMax(origin);
274
- if (effectivePerTx !== void 0 && challenge.amount > effectivePerTx) {
805
+ if (effectivePerTx !== void 0 && amount > effectivePerTx) {
275
806
  return { action: "block", reason: `Exceeds per-tx limit for ${origin} (${effectivePerTx} sats)`, severity: "reject" };
276
807
  }
277
808
  let yellowLight;
@@ -280,19 +811,19 @@ var RateLimiter = class {
280
811
  const windowEntries = this.entriesInWindow(cutoff, isCustomSite ? origin : void 0);
281
812
  const totalSats = windowEntries.reduce((sum, e) => sum + e.satoshis, 0);
282
813
  const totalTx = windowEntries.length;
283
- if (totalSats + challenge.amount > wl.maxSatoshis) {
814
+ if (totalSats + amount > wl.maxSatoshis) {
284
815
  return { action: "block", reason: `Exceeds ${wl.window} sats limit (${wl.maxSatoshis})`, severity: "window" };
285
816
  }
286
817
  if (totalTx + 1 > wl.maxTransactions) {
287
818
  return { action: "block", reason: `Exceeds ${wl.window} tx count limit (${wl.maxTransactions})`, severity: "window" };
288
819
  }
289
- if (this.limits.yellowLightThreshold < 1 && !yellowLight && totalSats + challenge.amount > wl.maxSatoshis * this.limits.yellowLightThreshold) {
820
+ if (this.limits.yellowLightThreshold < 1 && !yellowLight && totalSats + amount > wl.maxSatoshis * this.limits.yellowLightThreshold) {
290
821
  yellowLight = {
291
822
  origin,
292
823
  currentSpend: totalSats,
293
824
  limit: wl.maxSatoshis,
294
825
  window: wl.window,
295
- challenge
826
+ challenge: request
296
827
  };
297
828
  }
298
829
  }
@@ -567,6 +1098,8 @@ function createX402Fetch(config = {}) {
567
1098
  const storage = config.storage ?? new LocalStorageAdapter();
568
1099
  const twoFactor = config.twoFactorProvider;
569
1100
  const constructProof = config.proofConstructor ?? defaultConstructProof;
1101
+ const brc105ProofConstructor = config.brc105ProofConstructor;
1102
+ const brc105Wallet = config.brc105Wallet;
570
1103
  const nowFn = config.now ?? Date.now;
571
1104
  const mutex = createMutex();
572
1105
  const needs2fa = limits.require2fa;
@@ -588,76 +1121,150 @@ function createX402Fetch(config = {}) {
588
1121
  await storage.save(rl.getState());
589
1122
  await storage.saveSitePolicies(limits.sitePolicies);
590
1123
  }
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);
1124
+ async function handlePaymentFlow(originalResponse, input, init, origin, amount, protocol, buildProof, retryWithProof, makeLedgerEntry) {
603
1125
  return mutex(async () => {
604
1126
  const rl = await ensureInitialised();
605
1127
  const sitePolicy = await resolveSitePolicy(origin, limits, twoFactor);
606
- if (sitePolicy.action === "block") return response;
1128
+ if (sitePolicy.action === "block") return originalResponse;
607
1129
  if (!limits.sitePolicies[origin]) {
608
1130
  limits.sitePolicies[origin] = sitePolicy;
609
1131
  await storage.saveSitePolicies(limits.sitePolicies);
610
1132
  }
611
- const result = rl.check(challenge, origin);
1133
+ const spendCheckable = { amount, origin, protocol };
1134
+ const result = rl.check(spendCheckable, origin);
612
1135
  if (result.action === "block") {
613
1136
  if (result.severity === "trip") {
614
1137
  rl.trip();
615
1138
  await persist(rl);
616
1139
  config.onLimitReached?.(result.reason);
617
- return response;
1140
+ return originalResponse;
618
1141
  }
619
1142
  if (result.severity === "window" && twoFactor) {
620
1143
  config.onLimitReached?.(result.reason);
621
1144
  const override = await twoFactor.verify({
622
1145
  type: "limit-override",
623
- amount: challenge.amount,
1146
+ amount,
624
1147
  origin,
625
1148
  reason: result.reason
626
1149
  });
627
1150
  if (override) {
628
1151
  } else {
629
- return response;
1152
+ return originalResponse;
630
1153
  }
631
1154
  } else {
632
1155
  config.onLimitReached?.(result.reason);
633
- return response;
1156
+ return originalResponse;
634
1157
  }
635
1158
  }
636
1159
  if (result.action === "yellow-light") {
637
1160
  const proceed = config.onYellowLight ? await config.onYellowLight(result.detail) : false;
638
- if (!proceed) return response;
1161
+ if (!proceed) return originalResponse;
639
1162
  }
640
- if (limits.require2fa.onHighValueTx && challenge.amount > limits.require2fa.highValueThreshold) {
641
- if (!twoFactor) return response;
1163
+ if (limits.require2fa.onHighValueTx && amount > limits.require2fa.highValueThreshold) {
1164
+ if (!twoFactor) return originalResponse;
642
1165
  const verified = await twoFactor.verify({
643
1166
  type: "high-value-tx",
644
- amount: challenge.amount,
1167
+ amount,
645
1168
  origin
646
1169
  });
647
- if (!verified) return response;
1170
+ if (!verified) return originalResponse;
648
1171
  }
649
- const proof = await constructProof(challenge);
650
- rl.record({
651
- timestamp: nowFn(),
652
- origin,
653
- satoshis: challenge.amount,
654
- txid: proof.txid
655
- });
1172
+ let proof;
1173
+ try {
1174
+ proof = await buildProof();
1175
+ } catch (err) {
1176
+ console.error(`[x402] Proof construction failed (${protocol}):`, err);
1177
+ config.onProofError?.(err, protocol);
1178
+ return originalResponse;
1179
+ }
1180
+ rl.record(makeLedgerEntry(proof));
656
1181
  await persist(rl);
657
- const headers = new Headers(init?.headers);
658
- headers.set("X402-Proof", JSON.stringify(proof));
659
- return fetch(input, { ...init, headers });
1182
+ const maxAttempts = 3;
1183
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
1184
+ try {
1185
+ return await retryWithProof(proof);
1186
+ } catch (err) {
1187
+ if (attempt >= maxAttempts) {
1188
+ console.error(`[x402] Paid request failed after ${maxAttempts} attempts (${protocol}):`, err);
1189
+ return originalResponse;
1190
+ }
1191
+ await new Promise((r) => setTimeout(r, 250 * Math.pow(2, attempt)));
1192
+ }
1193
+ }
1194
+ return originalResponse;
660
1195
  });
1196
+ }
1197
+ const fetchFn = async function x402Fetch2(input, init) {
1198
+ const response = await fetch(input, init);
1199
+ if (response.status !== 402) return response;
1200
+ const origin = extractOrigin(input);
1201
+ const challengeHeader = response.headers.get("X402-Challenge");
1202
+ if (challengeHeader) {
1203
+ let challenge;
1204
+ try {
1205
+ challenge = parseChallenge(challengeHeader);
1206
+ } catch {
1207
+ return response;
1208
+ }
1209
+ return handlePaymentFlow(
1210
+ response,
1211
+ input,
1212
+ init,
1213
+ origin,
1214
+ challenge.amount,
1215
+ "x402",
1216
+ async () => constructProof(challenge),
1217
+ (proof) => {
1218
+ const headers = new Headers(init?.headers);
1219
+ headers.set("X402-Proof", JSON.stringify(proof));
1220
+ return fetch(input, { ...init, headers });
1221
+ },
1222
+ (proof) => ({
1223
+ timestamp: nowFn(),
1224
+ origin,
1225
+ satoshis: challenge.amount,
1226
+ txid: proof.txid
1227
+ })
1228
+ );
1229
+ }
1230
+ const brc105Version = response.headers.get("x-bsv-payment-version");
1231
+ if (brc105Version) {
1232
+ if (!brc105ProofConstructor && !brc105Wallet) return response;
1233
+ let brc105Challenge;
1234
+ try {
1235
+ brc105Challenge = parseBrc105Challenge(response);
1236
+ } catch {
1237
+ return response;
1238
+ }
1239
+ return handlePaymentFlow(
1240
+ response,
1241
+ input,
1242
+ init,
1243
+ origin,
1244
+ brc105Challenge.satoshisRequired,
1245
+ "brc105",
1246
+ async () => {
1247
+ if (brc105ProofConstructor) {
1248
+ return brc105ProofConstructor(brc105Challenge);
1249
+ }
1250
+ return constructBrc105Proof(brc105Challenge, brc105Wallet, origin);
1251
+ },
1252
+ (proof) => {
1253
+ const headers = new Headers(init?.headers);
1254
+ headers.set("x-bsv-payment", JSON.stringify(proof));
1255
+ headers.set("x-bsv-auth-identity-key", proof.clientIdentityKey);
1256
+ return fetch(input, { ...init, headers });
1257
+ },
1258
+ (proof) => ({
1259
+ timestamp: nowFn(),
1260
+ origin,
1261
+ satoshis: brc105Challenge.satoshisRequired,
1262
+ txid: proof.txid,
1263
+ protocol: "brc105"
1264
+ })
1265
+ );
1266
+ }
1267
+ return response;
661
1268
  };
662
1269
  fetchFn.resetLimits = async () => {
663
1270
  const rl = await ensureInitialised();
@@ -762,7 +1369,9 @@ Allow this payment of ${action.amount} sats to ${action.origin}?`;
762
1369
  RateLimiter,
763
1370
  TIER_PRESETS,
764
1371
  WalletTwoFactorProvider,
1372
+ constructBrc105Proof,
765
1373
  createX402Fetch,
1374
+ parseBrc105Challenge,
766
1375
  parseChallenge,
767
1376
  resolveSitePolicy,
768
1377
  resolveSpendLimits,