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.js
CHANGED
|
@@ -1,3 +1,524 @@
|
|
|
1
|
+
// src/brc105-challenge.ts
|
|
2
|
+
function parseBrc105Challenge(response) {
|
|
3
|
+
const version = response.headers.get("x-bsv-payment-version");
|
|
4
|
+
if (version === null) {
|
|
5
|
+
throw new Error("BRC-105: missing x-bsv-payment-version header");
|
|
6
|
+
}
|
|
7
|
+
if (version !== "1.0") {
|
|
8
|
+
throw new Error(`BRC-105: unsupported version "${version}", expected "1.0"`);
|
|
9
|
+
}
|
|
10
|
+
const satoshisRaw = response.headers.get("x-bsv-payment-satoshis-required");
|
|
11
|
+
if (satoshisRaw === null) {
|
|
12
|
+
throw new Error("BRC-105: missing x-bsv-payment-satoshis-required header");
|
|
13
|
+
}
|
|
14
|
+
const satoshisRequired = Number(satoshisRaw);
|
|
15
|
+
if (!Number.isFinite(satoshisRequired) || !Number.isInteger(satoshisRequired) || satoshisRequired <= 0) {
|
|
16
|
+
throw new Error("BRC-105: satoshis-required must be a positive integer");
|
|
17
|
+
}
|
|
18
|
+
const serverIdentityKey = response.headers.get("x-bsv-auth-identity-key");
|
|
19
|
+
if (serverIdentityKey === null || serverIdentityKey.length === 0) {
|
|
20
|
+
throw new Error("BRC-105: missing or empty x-bsv-auth-identity-key header");
|
|
21
|
+
}
|
|
22
|
+
if (!/^0[23][0-9a-fA-F]{64}$/.test(serverIdentityKey)) {
|
|
23
|
+
throw new Error("BRC-105: x-bsv-auth-identity-key must be a 33-byte compressed public key (hex)");
|
|
24
|
+
}
|
|
25
|
+
const derivationPrefix = response.headers.get("x-bsv-payment-derivation-prefix");
|
|
26
|
+
if (derivationPrefix === null || derivationPrefix.length === 0) {
|
|
27
|
+
throw new Error("BRC-105: missing or empty x-bsv-payment-derivation-prefix header");
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
version,
|
|
31
|
+
satoshisRequired,
|
|
32
|
+
serverIdentityKey,
|
|
33
|
+
derivationPrefix
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// src/brc105-proof.ts
|
|
38
|
+
function bytesToHex(bytes) {
|
|
39
|
+
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
40
|
+
}
|
|
41
|
+
function hexToBytes(hex) {
|
|
42
|
+
if (hex.length % 2 !== 0) throw new Error("Hex string must have even length");
|
|
43
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
44
|
+
for (let i = 0; i < hex.length; i += 2) {
|
|
45
|
+
bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);
|
|
46
|
+
}
|
|
47
|
+
return bytes;
|
|
48
|
+
}
|
|
49
|
+
function bytesToBase64(bytes) {
|
|
50
|
+
let binary = "";
|
|
51
|
+
for (const b of bytes) binary += String.fromCharCode(b);
|
|
52
|
+
return btoa(binary);
|
|
53
|
+
}
|
|
54
|
+
function numberArrayToBase64(arr) {
|
|
55
|
+
return bytesToBase64(new Uint8Array(arr));
|
|
56
|
+
}
|
|
57
|
+
var RIPEMD160_CONSTANTS = {
|
|
58
|
+
// Round constants for left and right paths
|
|
59
|
+
KL: [0, 1518500249, 1859775393, 2400959708, 2840853838],
|
|
60
|
+
KR: [1352829926, 1548603684, 1836072691, 2053994217, 0],
|
|
61
|
+
// Message schedule (word index per step)
|
|
62
|
+
RL: [
|
|
63
|
+
0,
|
|
64
|
+
1,
|
|
65
|
+
2,
|
|
66
|
+
3,
|
|
67
|
+
4,
|
|
68
|
+
5,
|
|
69
|
+
6,
|
|
70
|
+
7,
|
|
71
|
+
8,
|
|
72
|
+
9,
|
|
73
|
+
10,
|
|
74
|
+
11,
|
|
75
|
+
12,
|
|
76
|
+
13,
|
|
77
|
+
14,
|
|
78
|
+
15,
|
|
79
|
+
7,
|
|
80
|
+
4,
|
|
81
|
+
13,
|
|
82
|
+
1,
|
|
83
|
+
10,
|
|
84
|
+
6,
|
|
85
|
+
15,
|
|
86
|
+
3,
|
|
87
|
+
12,
|
|
88
|
+
0,
|
|
89
|
+
9,
|
|
90
|
+
5,
|
|
91
|
+
2,
|
|
92
|
+
14,
|
|
93
|
+
11,
|
|
94
|
+
8,
|
|
95
|
+
3,
|
|
96
|
+
10,
|
|
97
|
+
14,
|
|
98
|
+
4,
|
|
99
|
+
9,
|
|
100
|
+
15,
|
|
101
|
+
8,
|
|
102
|
+
1,
|
|
103
|
+
2,
|
|
104
|
+
7,
|
|
105
|
+
0,
|
|
106
|
+
6,
|
|
107
|
+
13,
|
|
108
|
+
11,
|
|
109
|
+
5,
|
|
110
|
+
12,
|
|
111
|
+
1,
|
|
112
|
+
9,
|
|
113
|
+
11,
|
|
114
|
+
10,
|
|
115
|
+
0,
|
|
116
|
+
8,
|
|
117
|
+
12,
|
|
118
|
+
4,
|
|
119
|
+
13,
|
|
120
|
+
3,
|
|
121
|
+
7,
|
|
122
|
+
15,
|
|
123
|
+
14,
|
|
124
|
+
5,
|
|
125
|
+
6,
|
|
126
|
+
2,
|
|
127
|
+
4,
|
|
128
|
+
0,
|
|
129
|
+
5,
|
|
130
|
+
9,
|
|
131
|
+
7,
|
|
132
|
+
12,
|
|
133
|
+
2,
|
|
134
|
+
10,
|
|
135
|
+
14,
|
|
136
|
+
1,
|
|
137
|
+
3,
|
|
138
|
+
8,
|
|
139
|
+
11,
|
|
140
|
+
6,
|
|
141
|
+
15,
|
|
142
|
+
13
|
|
143
|
+
],
|
|
144
|
+
RR: [
|
|
145
|
+
5,
|
|
146
|
+
14,
|
|
147
|
+
7,
|
|
148
|
+
0,
|
|
149
|
+
9,
|
|
150
|
+
2,
|
|
151
|
+
11,
|
|
152
|
+
4,
|
|
153
|
+
13,
|
|
154
|
+
6,
|
|
155
|
+
15,
|
|
156
|
+
8,
|
|
157
|
+
1,
|
|
158
|
+
10,
|
|
159
|
+
3,
|
|
160
|
+
12,
|
|
161
|
+
6,
|
|
162
|
+
11,
|
|
163
|
+
3,
|
|
164
|
+
7,
|
|
165
|
+
0,
|
|
166
|
+
13,
|
|
167
|
+
5,
|
|
168
|
+
10,
|
|
169
|
+
14,
|
|
170
|
+
15,
|
|
171
|
+
8,
|
|
172
|
+
12,
|
|
173
|
+
4,
|
|
174
|
+
9,
|
|
175
|
+
1,
|
|
176
|
+
2,
|
|
177
|
+
15,
|
|
178
|
+
5,
|
|
179
|
+
1,
|
|
180
|
+
3,
|
|
181
|
+
7,
|
|
182
|
+
14,
|
|
183
|
+
6,
|
|
184
|
+
9,
|
|
185
|
+
11,
|
|
186
|
+
8,
|
|
187
|
+
12,
|
|
188
|
+
2,
|
|
189
|
+
10,
|
|
190
|
+
0,
|
|
191
|
+
4,
|
|
192
|
+
13,
|
|
193
|
+
8,
|
|
194
|
+
6,
|
|
195
|
+
4,
|
|
196
|
+
1,
|
|
197
|
+
3,
|
|
198
|
+
11,
|
|
199
|
+
15,
|
|
200
|
+
0,
|
|
201
|
+
5,
|
|
202
|
+
12,
|
|
203
|
+
2,
|
|
204
|
+
13,
|
|
205
|
+
9,
|
|
206
|
+
7,
|
|
207
|
+
10,
|
|
208
|
+
14,
|
|
209
|
+
12,
|
|
210
|
+
15,
|
|
211
|
+
10,
|
|
212
|
+
4,
|
|
213
|
+
1,
|
|
214
|
+
5,
|
|
215
|
+
8,
|
|
216
|
+
7,
|
|
217
|
+
6,
|
|
218
|
+
2,
|
|
219
|
+
13,
|
|
220
|
+
14,
|
|
221
|
+
0,
|
|
222
|
+
3,
|
|
223
|
+
9,
|
|
224
|
+
11
|
|
225
|
+
],
|
|
226
|
+
// Rotation amounts
|
|
227
|
+
SL: [
|
|
228
|
+
11,
|
|
229
|
+
14,
|
|
230
|
+
15,
|
|
231
|
+
12,
|
|
232
|
+
5,
|
|
233
|
+
8,
|
|
234
|
+
7,
|
|
235
|
+
9,
|
|
236
|
+
11,
|
|
237
|
+
13,
|
|
238
|
+
14,
|
|
239
|
+
15,
|
|
240
|
+
6,
|
|
241
|
+
7,
|
|
242
|
+
9,
|
|
243
|
+
8,
|
|
244
|
+
7,
|
|
245
|
+
6,
|
|
246
|
+
8,
|
|
247
|
+
13,
|
|
248
|
+
11,
|
|
249
|
+
9,
|
|
250
|
+
7,
|
|
251
|
+
15,
|
|
252
|
+
7,
|
|
253
|
+
12,
|
|
254
|
+
15,
|
|
255
|
+
9,
|
|
256
|
+
11,
|
|
257
|
+
7,
|
|
258
|
+
13,
|
|
259
|
+
12,
|
|
260
|
+
11,
|
|
261
|
+
13,
|
|
262
|
+
6,
|
|
263
|
+
7,
|
|
264
|
+
14,
|
|
265
|
+
9,
|
|
266
|
+
13,
|
|
267
|
+
15,
|
|
268
|
+
14,
|
|
269
|
+
8,
|
|
270
|
+
13,
|
|
271
|
+
6,
|
|
272
|
+
5,
|
|
273
|
+
12,
|
|
274
|
+
7,
|
|
275
|
+
5,
|
|
276
|
+
11,
|
|
277
|
+
12,
|
|
278
|
+
14,
|
|
279
|
+
15,
|
|
280
|
+
14,
|
|
281
|
+
15,
|
|
282
|
+
9,
|
|
283
|
+
8,
|
|
284
|
+
9,
|
|
285
|
+
14,
|
|
286
|
+
5,
|
|
287
|
+
6,
|
|
288
|
+
8,
|
|
289
|
+
6,
|
|
290
|
+
5,
|
|
291
|
+
12,
|
|
292
|
+
9,
|
|
293
|
+
15,
|
|
294
|
+
5,
|
|
295
|
+
11,
|
|
296
|
+
6,
|
|
297
|
+
8,
|
|
298
|
+
13,
|
|
299
|
+
12,
|
|
300
|
+
5,
|
|
301
|
+
12,
|
|
302
|
+
13,
|
|
303
|
+
14,
|
|
304
|
+
11,
|
|
305
|
+
8,
|
|
306
|
+
5,
|
|
307
|
+
6
|
|
308
|
+
],
|
|
309
|
+
SR: [
|
|
310
|
+
8,
|
|
311
|
+
9,
|
|
312
|
+
9,
|
|
313
|
+
11,
|
|
314
|
+
13,
|
|
315
|
+
15,
|
|
316
|
+
15,
|
|
317
|
+
5,
|
|
318
|
+
7,
|
|
319
|
+
7,
|
|
320
|
+
8,
|
|
321
|
+
11,
|
|
322
|
+
14,
|
|
323
|
+
14,
|
|
324
|
+
12,
|
|
325
|
+
6,
|
|
326
|
+
9,
|
|
327
|
+
13,
|
|
328
|
+
15,
|
|
329
|
+
7,
|
|
330
|
+
12,
|
|
331
|
+
8,
|
|
332
|
+
9,
|
|
333
|
+
11,
|
|
334
|
+
7,
|
|
335
|
+
7,
|
|
336
|
+
12,
|
|
337
|
+
7,
|
|
338
|
+
6,
|
|
339
|
+
15,
|
|
340
|
+
13,
|
|
341
|
+
11,
|
|
342
|
+
9,
|
|
343
|
+
7,
|
|
344
|
+
15,
|
|
345
|
+
11,
|
|
346
|
+
8,
|
|
347
|
+
6,
|
|
348
|
+
6,
|
|
349
|
+
14,
|
|
350
|
+
12,
|
|
351
|
+
13,
|
|
352
|
+
5,
|
|
353
|
+
14,
|
|
354
|
+
13,
|
|
355
|
+
13,
|
|
356
|
+
7,
|
|
357
|
+
5,
|
|
358
|
+
15,
|
|
359
|
+
5,
|
|
360
|
+
8,
|
|
361
|
+
11,
|
|
362
|
+
14,
|
|
363
|
+
14,
|
|
364
|
+
6,
|
|
365
|
+
14,
|
|
366
|
+
6,
|
|
367
|
+
9,
|
|
368
|
+
12,
|
|
369
|
+
9,
|
|
370
|
+
12,
|
|
371
|
+
5,
|
|
372
|
+
15,
|
|
373
|
+
8,
|
|
374
|
+
8,
|
|
375
|
+
5,
|
|
376
|
+
12,
|
|
377
|
+
9,
|
|
378
|
+
12,
|
|
379
|
+
5,
|
|
380
|
+
14,
|
|
381
|
+
6,
|
|
382
|
+
8,
|
|
383
|
+
13,
|
|
384
|
+
6,
|
|
385
|
+
5,
|
|
386
|
+
15,
|
|
387
|
+
13,
|
|
388
|
+
11,
|
|
389
|
+
11
|
|
390
|
+
]
|
|
391
|
+
};
|
|
392
|
+
function rotl32(x, n) {
|
|
393
|
+
return (x << n | x >>> 32 - n) >>> 0;
|
|
394
|
+
}
|
|
395
|
+
function f(j, x, y, z) {
|
|
396
|
+
if (j < 16) return (x ^ y ^ z) >>> 0;
|
|
397
|
+
if (j < 32) return (x & y | ~x & z) >>> 0;
|
|
398
|
+
if (j < 48) return ((x | ~y) ^ z) >>> 0;
|
|
399
|
+
if (j < 64) return (x & z | y & ~z) >>> 0;
|
|
400
|
+
return (x ^ (y | ~z)) >>> 0;
|
|
401
|
+
}
|
|
402
|
+
function ripemd160(message) {
|
|
403
|
+
const msgLen = message.length;
|
|
404
|
+
const bitLen = msgLen * 8;
|
|
405
|
+
const padLen = (55 - msgLen % 64 + 64) % 64 + 1;
|
|
406
|
+
const padded = new Uint8Array(msgLen + padLen + 8);
|
|
407
|
+
padded.set(message);
|
|
408
|
+
padded[msgLen] = 128;
|
|
409
|
+
const view = new DataView(padded.buffer);
|
|
410
|
+
view.setUint32(padded.length - 8, bitLen >>> 0, true);
|
|
411
|
+
view.setUint32(padded.length - 4, bitLen / 4294967296 >>> 0, true);
|
|
412
|
+
let h0 = 1732584193;
|
|
413
|
+
let h1 = 4023233417;
|
|
414
|
+
let h2 = 2562383102;
|
|
415
|
+
let h3 = 271733878;
|
|
416
|
+
let h4 = 3285377520;
|
|
417
|
+
const { KL, KR, RL, RR, SL, SR } = RIPEMD160_CONSTANTS;
|
|
418
|
+
for (let offset = 0; offset < padded.length; offset += 64) {
|
|
419
|
+
const w = new Uint32Array(16);
|
|
420
|
+
for (let i = 0; i < 16; i++) {
|
|
421
|
+
w[i] = view.getUint32(offset + i * 4, true);
|
|
422
|
+
}
|
|
423
|
+
let al = h0, bl = h1, cl = h2, dl = h3, el = h4;
|
|
424
|
+
let ar = h0, br = h1, cr = h2, dr = h3, er = h4;
|
|
425
|
+
for (let j = 0; j < 80; j++) {
|
|
426
|
+
const round = j >>> 4;
|
|
427
|
+
let t2 = al + f(j, bl, cl, dl) + w[RL[j]] + KL[round] >>> 0;
|
|
428
|
+
t2 = rotl32(t2, SL[j]) + el >>> 0;
|
|
429
|
+
al = el;
|
|
430
|
+
el = dl;
|
|
431
|
+
dl = rotl32(cl, 10);
|
|
432
|
+
cl = bl;
|
|
433
|
+
bl = t2;
|
|
434
|
+
t2 = ar + f(79 - j, br, cr, dr) + w[RR[j]] + KR[round] >>> 0;
|
|
435
|
+
t2 = rotl32(t2, SR[j]) + er >>> 0;
|
|
436
|
+
ar = er;
|
|
437
|
+
er = dr;
|
|
438
|
+
dr = rotl32(cr, 10);
|
|
439
|
+
cr = br;
|
|
440
|
+
br = t2;
|
|
441
|
+
}
|
|
442
|
+
const t = h1 + cl + dr >>> 0;
|
|
443
|
+
h1 = h2 + dl + er >>> 0;
|
|
444
|
+
h2 = h3 + el + ar >>> 0;
|
|
445
|
+
h3 = h4 + al + br >>> 0;
|
|
446
|
+
h4 = h0 + bl + cr >>> 0;
|
|
447
|
+
h0 = t;
|
|
448
|
+
}
|
|
449
|
+
const digest = new Uint8Array(20);
|
|
450
|
+
const dv = new DataView(digest.buffer);
|
|
451
|
+
dv.setUint32(0, h0, true);
|
|
452
|
+
dv.setUint32(4, h1, true);
|
|
453
|
+
dv.setUint32(8, h2, true);
|
|
454
|
+
dv.setUint32(12, h3, true);
|
|
455
|
+
dv.setUint32(16, h4, true);
|
|
456
|
+
return digest;
|
|
457
|
+
}
|
|
458
|
+
async function hash160(data) {
|
|
459
|
+
const sha256 = new Uint8Array(await crypto.subtle.digest("SHA-256", data));
|
|
460
|
+
return ripemd160(sha256);
|
|
461
|
+
}
|
|
462
|
+
async function pubkeyToP2PKHLockingScript(pubkeyHex) {
|
|
463
|
+
const pubkeyBytes = hexToBytes(pubkeyHex);
|
|
464
|
+
const pubkeyHash = await hash160(pubkeyBytes);
|
|
465
|
+
return `76a914${bytesToHex(pubkeyHash)}88ac`;
|
|
466
|
+
}
|
|
467
|
+
async function createDerivationSuffix(wallet) {
|
|
468
|
+
const randomBytes = new Uint8Array(16);
|
|
469
|
+
crypto.getRandomValues(randomBytes);
|
|
470
|
+
const firstHalf = Array.from(randomBytes);
|
|
471
|
+
const keyID = new TextDecoder().decode(randomBytes);
|
|
472
|
+
const { hmac } = await wallet.createHmac({
|
|
473
|
+
data: firstHalf,
|
|
474
|
+
protocolID: [2, "server hmac"],
|
|
475
|
+
keyID,
|
|
476
|
+
counterparty: "self"
|
|
477
|
+
});
|
|
478
|
+
const nonceBytes = [...firstHalf, ...hmac];
|
|
479
|
+
return numberArrayToBase64(nonceBytes);
|
|
480
|
+
}
|
|
481
|
+
async function constructBrc105Proof(challenge, wallet) {
|
|
482
|
+
const derivationSuffix = await createDerivationSuffix(wallet);
|
|
483
|
+
const keyID = `${challenge.derivationPrefix} ${derivationSuffix}`;
|
|
484
|
+
const { publicKey: derivedPublicKey } = await wallet.getPublicKey({
|
|
485
|
+
protocolID: [2, "3241645161d8"],
|
|
486
|
+
keyID,
|
|
487
|
+
counterparty: challenge.serverIdentityKey
|
|
488
|
+
});
|
|
489
|
+
const lockingScript = await pubkeyToP2PKHLockingScript(derivedPublicKey);
|
|
490
|
+
const result = await wallet.createAction({
|
|
491
|
+
description: "BRC-105 payment",
|
|
492
|
+
outputs: [{
|
|
493
|
+
satoshis: challenge.satoshisRequired,
|
|
494
|
+
lockingScript,
|
|
495
|
+
description: "BRC-105 payment output",
|
|
496
|
+
customInstructions: JSON.stringify({
|
|
497
|
+
derivationPrefix: challenge.derivationPrefix,
|
|
498
|
+
derivationSuffix,
|
|
499
|
+
payee: challenge.serverIdentityKey
|
|
500
|
+
})
|
|
501
|
+
}],
|
|
502
|
+
options: {
|
|
503
|
+
randomizeOutputs: false
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
let transactionBase64;
|
|
507
|
+
if (result.tx && Array.isArray(result.tx) && result.tx.length > 0) {
|
|
508
|
+
transactionBase64 = numberArrayToBase64(result.tx);
|
|
509
|
+
} else if (result.rawTx && typeof result.rawTx === "string" && result.rawTx.length > 0) {
|
|
510
|
+
transactionBase64 = bytesToBase64(hexToBytes(result.rawTx));
|
|
511
|
+
} else {
|
|
512
|
+
throw new Error("Wallet returned no transaction data (neither tx nor rawTx)");
|
|
513
|
+
}
|
|
514
|
+
return {
|
|
515
|
+
derivationPrefix: challenge.derivationPrefix,
|
|
516
|
+
derivationSuffix,
|
|
517
|
+
transaction: transactionBase64,
|
|
518
|
+
txid: result.txid
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
|
|
1
522
|
// src/challenge.ts
|
|
2
523
|
function parseChallenge(header) {
|
|
3
524
|
let parsed;
|
|
@@ -214,28 +735,29 @@ var RateLimiter = class {
|
|
|
214
735
|
this.broken = state?.circuitBroken ?? false;
|
|
215
736
|
this.now = now ?? Date.now;
|
|
216
737
|
}
|
|
217
|
-
check(
|
|
738
|
+
check(request, origin) {
|
|
218
739
|
if (this.broken) {
|
|
219
740
|
return { action: "block", reason: "Circuit breaker tripped \u2014 call resetLimits() to clear", severity: "trip" };
|
|
220
741
|
}
|
|
221
|
-
|
|
742
|
+
const amount = request.amount;
|
|
743
|
+
if (!Number.isFinite(amount) || !Number.isInteger(amount) || amount <= 0) {
|
|
222
744
|
return { action: "block", reason: "Invalid transaction amount rejected", severity: "reject" };
|
|
223
745
|
}
|
|
224
|
-
if (
|
|
746
|
+
if (amount > BFG_PER_TX_CEILING_SATOSHIS) {
|
|
225
747
|
return { action: "block", reason: `Exceeds BFG per-tx ceiling (${BFG_PER_TX_CEILING_SATOSHIS} sats)`, severity: "reject" };
|
|
226
748
|
}
|
|
227
749
|
const dayAgo = this.now() - WINDOW_MS.day;
|
|
228
750
|
const dailyTotal = this.sumSatoshis(dayAgo);
|
|
229
|
-
if (dailyTotal +
|
|
751
|
+
if (dailyTotal + amount > BFG_DAILY_CEILING_SATOSHIS) {
|
|
230
752
|
return { action: "block", reason: `Exceeds BFG daily ceiling (${BFG_DAILY_CEILING_SATOSHIS} sats)`, severity: "trip" };
|
|
231
753
|
}
|
|
232
|
-
if (
|
|
754
|
+
if (amount > this.limits.perTxMaxSatoshis) {
|
|
233
755
|
return { action: "block", reason: `Exceeds per-tx limit (${this.limits.perTxMaxSatoshis} sats)`, severity: "reject" };
|
|
234
756
|
}
|
|
235
757
|
const isCustomSite = this.hasCustomPolicy(origin);
|
|
236
758
|
const effectiveLimits = this.effectiveWindows(origin);
|
|
237
759
|
const effectivePerTx = this.effectivePerTxMax(origin);
|
|
238
|
-
if (effectivePerTx !== void 0 &&
|
|
760
|
+
if (effectivePerTx !== void 0 && amount > effectivePerTx) {
|
|
239
761
|
return { action: "block", reason: `Exceeds per-tx limit for ${origin} (${effectivePerTx} sats)`, severity: "reject" };
|
|
240
762
|
}
|
|
241
763
|
let yellowLight;
|
|
@@ -244,19 +766,19 @@ var RateLimiter = class {
|
|
|
244
766
|
const windowEntries = this.entriesInWindow(cutoff, isCustomSite ? origin : void 0);
|
|
245
767
|
const totalSats = windowEntries.reduce((sum, e) => sum + e.satoshis, 0);
|
|
246
768
|
const totalTx = windowEntries.length;
|
|
247
|
-
if (totalSats +
|
|
769
|
+
if (totalSats + amount > wl.maxSatoshis) {
|
|
248
770
|
return { action: "block", reason: `Exceeds ${wl.window} sats limit (${wl.maxSatoshis})`, severity: "window" };
|
|
249
771
|
}
|
|
250
772
|
if (totalTx + 1 > wl.maxTransactions) {
|
|
251
773
|
return { action: "block", reason: `Exceeds ${wl.window} tx count limit (${wl.maxTransactions})`, severity: "window" };
|
|
252
774
|
}
|
|
253
|
-
if (this.limits.yellowLightThreshold < 1 && !yellowLight && totalSats +
|
|
775
|
+
if (this.limits.yellowLightThreshold < 1 && !yellowLight && totalSats + amount > wl.maxSatoshis * this.limits.yellowLightThreshold) {
|
|
254
776
|
yellowLight = {
|
|
255
777
|
origin,
|
|
256
778
|
currentSpend: totalSats,
|
|
257
779
|
limit: wl.maxSatoshis,
|
|
258
780
|
window: wl.window,
|
|
259
|
-
challenge
|
|
781
|
+
challenge: request
|
|
260
782
|
};
|
|
261
783
|
}
|
|
262
784
|
}
|
|
@@ -531,6 +1053,8 @@ function createX402Fetch(config = {}) {
|
|
|
531
1053
|
const storage = config.storage ?? new LocalStorageAdapter();
|
|
532
1054
|
const twoFactor = config.twoFactorProvider;
|
|
533
1055
|
const constructProof = config.proofConstructor ?? defaultConstructProof;
|
|
1056
|
+
const brc105ProofConstructor = config.brc105ProofConstructor;
|
|
1057
|
+
const brc105Wallet = config.brc105Wallet;
|
|
534
1058
|
const nowFn = config.now ?? Date.now;
|
|
535
1059
|
const mutex = createMutex();
|
|
536
1060
|
const needs2fa = limits.require2fa;
|
|
@@ -552,76 +1076,135 @@ function createX402Fetch(config = {}) {
|
|
|
552
1076
|
await storage.save(rl.getState());
|
|
553
1077
|
await storage.saveSitePolicies(limits.sitePolicies);
|
|
554
1078
|
}
|
|
555
|
-
|
|
556
|
-
const response = await fetch(input, init);
|
|
557
|
-
if (response.status !== 402) return response;
|
|
558
|
-
const challengeHeader = response.headers.get("X402-Challenge");
|
|
559
|
-
if (!challengeHeader) return response;
|
|
560
|
-
let challenge;
|
|
561
|
-
try {
|
|
562
|
-
challenge = parseChallenge(challengeHeader);
|
|
563
|
-
} catch {
|
|
564
|
-
return response;
|
|
565
|
-
}
|
|
566
|
-
const origin = extractOrigin(input);
|
|
1079
|
+
async function handlePaymentFlow(originalResponse, input, init, origin, amount, protocol, buildProof, retryWithProof, makeLedgerEntry) {
|
|
567
1080
|
return mutex(async () => {
|
|
568
1081
|
const rl = await ensureInitialised();
|
|
569
1082
|
const sitePolicy = await resolveSitePolicy(origin, limits, twoFactor);
|
|
570
|
-
if (sitePolicy.action === "block") return
|
|
1083
|
+
if (sitePolicy.action === "block") return originalResponse;
|
|
571
1084
|
if (!limits.sitePolicies[origin]) {
|
|
572
1085
|
limits.sitePolicies[origin] = sitePolicy;
|
|
573
1086
|
await storage.saveSitePolicies(limits.sitePolicies);
|
|
574
1087
|
}
|
|
575
|
-
const
|
|
1088
|
+
const spendCheckable = { amount, origin, protocol };
|
|
1089
|
+
const result = rl.check(spendCheckable, origin);
|
|
576
1090
|
if (result.action === "block") {
|
|
577
1091
|
if (result.severity === "trip") {
|
|
578
1092
|
rl.trip();
|
|
579
1093
|
await persist(rl);
|
|
580
1094
|
config.onLimitReached?.(result.reason);
|
|
581
|
-
return
|
|
1095
|
+
return originalResponse;
|
|
582
1096
|
}
|
|
583
1097
|
if (result.severity === "window" && twoFactor) {
|
|
584
1098
|
config.onLimitReached?.(result.reason);
|
|
585
1099
|
const override = await twoFactor.verify({
|
|
586
1100
|
type: "limit-override",
|
|
587
|
-
amount
|
|
1101
|
+
amount,
|
|
588
1102
|
origin,
|
|
589
1103
|
reason: result.reason
|
|
590
1104
|
});
|
|
591
1105
|
if (override) {
|
|
592
1106
|
} else {
|
|
593
|
-
return
|
|
1107
|
+
return originalResponse;
|
|
594
1108
|
}
|
|
595
1109
|
} else {
|
|
596
1110
|
config.onLimitReached?.(result.reason);
|
|
597
|
-
return
|
|
1111
|
+
return originalResponse;
|
|
598
1112
|
}
|
|
599
1113
|
}
|
|
600
1114
|
if (result.action === "yellow-light") {
|
|
601
1115
|
const proceed = config.onYellowLight ? await config.onYellowLight(result.detail) : false;
|
|
602
|
-
if (!proceed) return
|
|
1116
|
+
if (!proceed) return originalResponse;
|
|
603
1117
|
}
|
|
604
|
-
if (limits.require2fa.onHighValueTx &&
|
|
605
|
-
if (!twoFactor) return
|
|
1118
|
+
if (limits.require2fa.onHighValueTx && amount > limits.require2fa.highValueThreshold) {
|
|
1119
|
+
if (!twoFactor) return originalResponse;
|
|
606
1120
|
const verified = await twoFactor.verify({
|
|
607
1121
|
type: "high-value-tx",
|
|
608
|
-
amount
|
|
1122
|
+
amount,
|
|
609
1123
|
origin
|
|
610
1124
|
});
|
|
611
|
-
if (!verified) return
|
|
1125
|
+
if (!verified) return originalResponse;
|
|
612
1126
|
}
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
1127
|
+
let proof;
|
|
1128
|
+
try {
|
|
1129
|
+
proof = await buildProof();
|
|
1130
|
+
} catch {
|
|
1131
|
+
return originalResponse;
|
|
1132
|
+
}
|
|
1133
|
+
rl.record(makeLedgerEntry(proof));
|
|
620
1134
|
await persist(rl);
|
|
621
|
-
|
|
622
|
-
headers.set("X402-Proof", JSON.stringify(proof));
|
|
623
|
-
return fetch(input, { ...init, headers });
|
|
1135
|
+
return retryWithProof(proof);
|
|
624
1136
|
});
|
|
1137
|
+
}
|
|
1138
|
+
const fetchFn = async function x402Fetch2(input, init) {
|
|
1139
|
+
const response = await fetch(input, init);
|
|
1140
|
+
if (response.status !== 402) return response;
|
|
1141
|
+
const origin = extractOrigin(input);
|
|
1142
|
+
const challengeHeader = response.headers.get("X402-Challenge");
|
|
1143
|
+
if (challengeHeader) {
|
|
1144
|
+
let challenge;
|
|
1145
|
+
try {
|
|
1146
|
+
challenge = parseChallenge(challengeHeader);
|
|
1147
|
+
} catch {
|
|
1148
|
+
return response;
|
|
1149
|
+
}
|
|
1150
|
+
return handlePaymentFlow(
|
|
1151
|
+
response,
|
|
1152
|
+
input,
|
|
1153
|
+
init,
|
|
1154
|
+
origin,
|
|
1155
|
+
challenge.amount,
|
|
1156
|
+
"x402",
|
|
1157
|
+
async () => constructProof(challenge),
|
|
1158
|
+
(proof) => {
|
|
1159
|
+
const headers = new Headers(init?.headers);
|
|
1160
|
+
headers.set("X402-Proof", JSON.stringify(proof));
|
|
1161
|
+
return fetch(input, { ...init, headers });
|
|
1162
|
+
},
|
|
1163
|
+
(proof) => ({
|
|
1164
|
+
timestamp: nowFn(),
|
|
1165
|
+
origin,
|
|
1166
|
+
satoshis: challenge.amount,
|
|
1167
|
+
txid: proof.txid
|
|
1168
|
+
})
|
|
1169
|
+
);
|
|
1170
|
+
}
|
|
1171
|
+
const brc105Version = response.headers.get("x-bsv-payment-version");
|
|
1172
|
+
if (brc105Version) {
|
|
1173
|
+
if (!brc105ProofConstructor && !brc105Wallet) return response;
|
|
1174
|
+
let brc105Challenge;
|
|
1175
|
+
try {
|
|
1176
|
+
brc105Challenge = parseBrc105Challenge(response);
|
|
1177
|
+
} catch {
|
|
1178
|
+
return response;
|
|
1179
|
+
}
|
|
1180
|
+
return handlePaymentFlow(
|
|
1181
|
+
response,
|
|
1182
|
+
input,
|
|
1183
|
+
init,
|
|
1184
|
+
origin,
|
|
1185
|
+
brc105Challenge.satoshisRequired,
|
|
1186
|
+
"brc105",
|
|
1187
|
+
async () => {
|
|
1188
|
+
if (brc105ProofConstructor) {
|
|
1189
|
+
return brc105ProofConstructor(brc105Challenge);
|
|
1190
|
+
}
|
|
1191
|
+
return constructBrc105Proof(brc105Challenge, brc105Wallet);
|
|
1192
|
+
},
|
|
1193
|
+
(proof) => {
|
|
1194
|
+
const headers = new Headers(init?.headers);
|
|
1195
|
+
headers.set("x-bsv-payment", JSON.stringify(proof));
|
|
1196
|
+
return fetch(input, { ...init, headers });
|
|
1197
|
+
},
|
|
1198
|
+
(proof) => ({
|
|
1199
|
+
timestamp: nowFn(),
|
|
1200
|
+
origin,
|
|
1201
|
+
satoshis: brc105Challenge.satoshisRequired,
|
|
1202
|
+
txid: proof.txid,
|
|
1203
|
+
protocol: "brc105"
|
|
1204
|
+
})
|
|
1205
|
+
);
|
|
1206
|
+
}
|
|
1207
|
+
return response;
|
|
625
1208
|
};
|
|
626
1209
|
fetchFn.resetLimits = async () => {
|
|
627
1210
|
const rl = await ensureInitialised();
|
|
@@ -725,7 +1308,9 @@ export {
|
|
|
725
1308
|
RateLimiter,
|
|
726
1309
|
TIER_PRESETS,
|
|
727
1310
|
WalletTwoFactorProvider,
|
|
1311
|
+
constructBrc105Proof,
|
|
728
1312
|
createX402Fetch,
|
|
1313
|
+
parseBrc105Challenge,
|
|
729
1314
|
parseChallenge,
|
|
730
1315
|
resolveSitePolicy,
|
|
731
1316
|
resolveSpendLimits,
|