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 +651 -42
- package/dist/index.d.cts +98 -3
- package/dist/index.d.ts +98 -3
- package/dist/index.js +649 -42
- package/package.json +7 -3
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(
|
|
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
|
-
|
|
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 (
|
|
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 +
|
|
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 (
|
|
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 &&
|
|
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 +
|
|
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 +
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1146
|
+
amount,
|
|
624
1147
|
origin,
|
|
625
1148
|
reason: result.reason
|
|
626
1149
|
});
|
|
627
1150
|
if (override) {
|
|
628
1151
|
} else {
|
|
629
|
-
return
|
|
1152
|
+
return originalResponse;
|
|
630
1153
|
}
|
|
631
1154
|
} else {
|
|
632
1155
|
config.onLimitReached?.(result.reason);
|
|
633
|
-
return
|
|
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
|
|
1161
|
+
if (!proceed) return originalResponse;
|
|
639
1162
|
}
|
|
640
|
-
if (limits.require2fa.onHighValueTx &&
|
|
641
|
-
if (!twoFactor) return
|
|
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
|
|
1167
|
+
amount,
|
|
645
1168
|
origin
|
|
646
1169
|
});
|
|
647
|
-
if (!verified) return
|
|
1170
|
+
if (!verified) return originalResponse;
|
|
648
1171
|
}
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
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
|
|
658
|
-
|
|
659
|
-
|
|
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,
|