bsv-x402 0.2.0 → 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 +629 -42
- package/dist/index.d.cts +91 -3
- package/dist/index.d.ts +91 -3
- package/dist/index.js +627 -42
- package/package.json +1 -1
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,527 @@ __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 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
|
+
|
|
37
560
|
// src/challenge.ts
|
|
38
561
|
function parseChallenge(header) {
|
|
39
562
|
let parsed;
|
|
@@ -250,28 +773,29 @@ var RateLimiter = class {
|
|
|
250
773
|
this.broken = state?.circuitBroken ?? false;
|
|
251
774
|
this.now = now ?? Date.now;
|
|
252
775
|
}
|
|
253
|
-
check(
|
|
776
|
+
check(request, origin) {
|
|
254
777
|
if (this.broken) {
|
|
255
778
|
return { action: "block", reason: "Circuit breaker tripped \u2014 call resetLimits() to clear", severity: "trip" };
|
|
256
779
|
}
|
|
257
|
-
|
|
780
|
+
const amount = request.amount;
|
|
781
|
+
if (!Number.isFinite(amount) || !Number.isInteger(amount) || amount <= 0) {
|
|
258
782
|
return { action: "block", reason: "Invalid transaction amount rejected", severity: "reject" };
|
|
259
783
|
}
|
|
260
|
-
if (
|
|
784
|
+
if (amount > BFG_PER_TX_CEILING_SATOSHIS) {
|
|
261
785
|
return { action: "block", reason: `Exceeds BFG per-tx ceiling (${BFG_PER_TX_CEILING_SATOSHIS} sats)`, severity: "reject" };
|
|
262
786
|
}
|
|
263
787
|
const dayAgo = this.now() - WINDOW_MS.day;
|
|
264
788
|
const dailyTotal = this.sumSatoshis(dayAgo);
|
|
265
|
-
if (dailyTotal +
|
|
789
|
+
if (dailyTotal + amount > BFG_DAILY_CEILING_SATOSHIS) {
|
|
266
790
|
return { action: "block", reason: `Exceeds BFG daily ceiling (${BFG_DAILY_CEILING_SATOSHIS} sats)`, severity: "trip" };
|
|
267
791
|
}
|
|
268
|
-
if (
|
|
792
|
+
if (amount > this.limits.perTxMaxSatoshis) {
|
|
269
793
|
return { action: "block", reason: `Exceeds per-tx limit (${this.limits.perTxMaxSatoshis} sats)`, severity: "reject" };
|
|
270
794
|
}
|
|
271
795
|
const isCustomSite = this.hasCustomPolicy(origin);
|
|
272
796
|
const effectiveLimits = this.effectiveWindows(origin);
|
|
273
797
|
const effectivePerTx = this.effectivePerTxMax(origin);
|
|
274
|
-
if (effectivePerTx !== void 0 &&
|
|
798
|
+
if (effectivePerTx !== void 0 && amount > effectivePerTx) {
|
|
275
799
|
return { action: "block", reason: `Exceeds per-tx limit for ${origin} (${effectivePerTx} sats)`, severity: "reject" };
|
|
276
800
|
}
|
|
277
801
|
let yellowLight;
|
|
@@ -280,19 +804,19 @@ var RateLimiter = class {
|
|
|
280
804
|
const windowEntries = this.entriesInWindow(cutoff, isCustomSite ? origin : void 0);
|
|
281
805
|
const totalSats = windowEntries.reduce((sum, e) => sum + e.satoshis, 0);
|
|
282
806
|
const totalTx = windowEntries.length;
|
|
283
|
-
if (totalSats +
|
|
807
|
+
if (totalSats + amount > wl.maxSatoshis) {
|
|
284
808
|
return { action: "block", reason: `Exceeds ${wl.window} sats limit (${wl.maxSatoshis})`, severity: "window" };
|
|
285
809
|
}
|
|
286
810
|
if (totalTx + 1 > wl.maxTransactions) {
|
|
287
811
|
return { action: "block", reason: `Exceeds ${wl.window} tx count limit (${wl.maxTransactions})`, severity: "window" };
|
|
288
812
|
}
|
|
289
|
-
if (this.limits.yellowLightThreshold < 1 && !yellowLight && totalSats +
|
|
813
|
+
if (this.limits.yellowLightThreshold < 1 && !yellowLight && totalSats + amount > wl.maxSatoshis * this.limits.yellowLightThreshold) {
|
|
290
814
|
yellowLight = {
|
|
291
815
|
origin,
|
|
292
816
|
currentSpend: totalSats,
|
|
293
817
|
limit: wl.maxSatoshis,
|
|
294
818
|
window: wl.window,
|
|
295
|
-
challenge
|
|
819
|
+
challenge: request
|
|
296
820
|
};
|
|
297
821
|
}
|
|
298
822
|
}
|
|
@@ -567,6 +1091,8 @@ function createX402Fetch(config = {}) {
|
|
|
567
1091
|
const storage = config.storage ?? new LocalStorageAdapter();
|
|
568
1092
|
const twoFactor = config.twoFactorProvider;
|
|
569
1093
|
const constructProof = config.proofConstructor ?? defaultConstructProof;
|
|
1094
|
+
const brc105ProofConstructor = config.brc105ProofConstructor;
|
|
1095
|
+
const brc105Wallet = config.brc105Wallet;
|
|
570
1096
|
const nowFn = config.now ?? Date.now;
|
|
571
1097
|
const mutex = createMutex();
|
|
572
1098
|
const needs2fa = limits.require2fa;
|
|
@@ -588,76 +1114,135 @@ function createX402Fetch(config = {}) {
|
|
|
588
1114
|
await storage.save(rl.getState());
|
|
589
1115
|
await storage.saveSitePolicies(limits.sitePolicies);
|
|
590
1116
|
}
|
|
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);
|
|
1117
|
+
async function handlePaymentFlow(originalResponse, input, init, origin, amount, protocol, buildProof, retryWithProof, makeLedgerEntry) {
|
|
603
1118
|
return mutex(async () => {
|
|
604
1119
|
const rl = await ensureInitialised();
|
|
605
1120
|
const sitePolicy = await resolveSitePolicy(origin, limits, twoFactor);
|
|
606
|
-
if (sitePolicy.action === "block") return
|
|
1121
|
+
if (sitePolicy.action === "block") return originalResponse;
|
|
607
1122
|
if (!limits.sitePolicies[origin]) {
|
|
608
1123
|
limits.sitePolicies[origin] = sitePolicy;
|
|
609
1124
|
await storage.saveSitePolicies(limits.sitePolicies);
|
|
610
1125
|
}
|
|
611
|
-
const
|
|
1126
|
+
const spendCheckable = { amount, origin, protocol };
|
|
1127
|
+
const result = rl.check(spendCheckable, origin);
|
|
612
1128
|
if (result.action === "block") {
|
|
613
1129
|
if (result.severity === "trip") {
|
|
614
1130
|
rl.trip();
|
|
615
1131
|
await persist(rl);
|
|
616
1132
|
config.onLimitReached?.(result.reason);
|
|
617
|
-
return
|
|
1133
|
+
return originalResponse;
|
|
618
1134
|
}
|
|
619
1135
|
if (result.severity === "window" && twoFactor) {
|
|
620
1136
|
config.onLimitReached?.(result.reason);
|
|
621
1137
|
const override = await twoFactor.verify({
|
|
622
1138
|
type: "limit-override",
|
|
623
|
-
amount
|
|
1139
|
+
amount,
|
|
624
1140
|
origin,
|
|
625
1141
|
reason: result.reason
|
|
626
1142
|
});
|
|
627
1143
|
if (override) {
|
|
628
1144
|
} else {
|
|
629
|
-
return
|
|
1145
|
+
return originalResponse;
|
|
630
1146
|
}
|
|
631
1147
|
} else {
|
|
632
1148
|
config.onLimitReached?.(result.reason);
|
|
633
|
-
return
|
|
1149
|
+
return originalResponse;
|
|
634
1150
|
}
|
|
635
1151
|
}
|
|
636
1152
|
if (result.action === "yellow-light") {
|
|
637
1153
|
const proceed = config.onYellowLight ? await config.onYellowLight(result.detail) : false;
|
|
638
|
-
if (!proceed) return
|
|
1154
|
+
if (!proceed) return originalResponse;
|
|
639
1155
|
}
|
|
640
|
-
if (limits.require2fa.onHighValueTx &&
|
|
641
|
-
if (!twoFactor) return
|
|
1156
|
+
if (limits.require2fa.onHighValueTx && amount > limits.require2fa.highValueThreshold) {
|
|
1157
|
+
if (!twoFactor) return originalResponse;
|
|
642
1158
|
const verified = await twoFactor.verify({
|
|
643
1159
|
type: "high-value-tx",
|
|
644
|
-
amount
|
|
1160
|
+
amount,
|
|
645
1161
|
origin
|
|
646
1162
|
});
|
|
647
|
-
if (!verified) return
|
|
1163
|
+
if (!verified) return originalResponse;
|
|
648
1164
|
}
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
1165
|
+
let proof;
|
|
1166
|
+
try {
|
|
1167
|
+
proof = await buildProof();
|
|
1168
|
+
} catch {
|
|
1169
|
+
return originalResponse;
|
|
1170
|
+
}
|
|
1171
|
+
rl.record(makeLedgerEntry(proof));
|
|
656
1172
|
await persist(rl);
|
|
657
|
-
|
|
658
|
-
headers.set("X402-Proof", JSON.stringify(proof));
|
|
659
|
-
return fetch(input, { ...init, headers });
|
|
1173
|
+
return retryWithProof(proof);
|
|
660
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
|
+
}
|
|
1245
|
+
return response;
|
|
661
1246
|
};
|
|
662
1247
|
fetchFn.resetLimits = async () => {
|
|
663
1248
|
const rl = await ensureInitialised();
|
|
@@ -762,7 +1347,9 @@ Allow this payment of ${action.amount} sats to ${action.origin}?`;
|
|
|
762
1347
|
RateLimiter,
|
|
763
1348
|
TIER_PRESETS,
|
|
764
1349
|
WalletTwoFactorProvider,
|
|
1350
|
+
constructBrc105Proof,
|
|
765
1351
|
createX402Fetch,
|
|
1352
|
+
parseBrc105Challenge,
|
|
766
1353
|
parseChallenge,
|
|
767
1354
|
resolveSitePolicy,
|
|
768
1355
|
resolveSpendLimits,
|