capjs-core 0.1.5 → 0.1.7
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/README.md +5 -2
- package/package.json +5 -2
- package/src/agents.js +700 -0
- package/src/behavior.js +437 -0
- package/src/detect.js +189 -8
- package/src/hashwx-wasm.js +1 -5
- package/src/index.d.ts +198 -43
- package/src/index.js +298 -67
- package/src/instrumentation.js +742 -116
- package/src/rsw.js +13 -5
package/src/index.js
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
import {
|
|
2
|
+
agentFromCodes,
|
|
3
|
+
agentFromHeaders,
|
|
4
|
+
agentFromTrace,
|
|
5
|
+
summarizeAgent,
|
|
6
|
+
verifyWebBotAuth,
|
|
7
|
+
} from "./agents.js";
|
|
8
|
+
import {
|
|
9
|
+
analyzeBehavior,
|
|
10
|
+
DEFAULT_BEHAVIOR_THRESHOLD,
|
|
11
|
+
validateTrace,
|
|
12
|
+
} from "./behavior.js";
|
|
1
13
|
import {
|
|
2
14
|
decryptGcm,
|
|
3
15
|
encryptGcm,
|
|
@@ -15,7 +27,29 @@ import {
|
|
|
15
27
|
verifyInstrumentationResult,
|
|
16
28
|
} from "./instrumentation.js";
|
|
17
29
|
|
|
30
|
+
export {
|
|
31
|
+
agentFromHeaders,
|
|
32
|
+
agentFromTrace,
|
|
33
|
+
parseSignatureAgent,
|
|
34
|
+
summarizeAgent,
|
|
35
|
+
verifyWebBotAuth,
|
|
36
|
+
} from "./agents.js";
|
|
37
|
+
export {
|
|
38
|
+
analyzeBehavior,
|
|
39
|
+
BEHAVIOR_WEIGHTS,
|
|
40
|
+
DEFAULT_BEHAVIOR_THRESHOLD,
|
|
41
|
+
WEAK_CAP,
|
|
42
|
+
} from "./behavior.js";
|
|
43
|
+
|
|
18
44
|
export { detectAutomation } from "./detect.js";
|
|
45
|
+
export {
|
|
46
|
+
boundValues,
|
|
47
|
+
canonicalize,
|
|
48
|
+
generateInstrumentation,
|
|
49
|
+
MAX_BOUND_DATA,
|
|
50
|
+
serializeBound,
|
|
51
|
+
verifyInstrumentationResult,
|
|
52
|
+
} from "./instrumentation.js";
|
|
19
53
|
|
|
20
54
|
import { mintHashwxChallenges, verifyHashwxSolution } from "./hashwx.js";
|
|
21
55
|
|
|
@@ -106,6 +140,161 @@ function assertSecret(secret) {
|
|
|
106
140
|
}
|
|
107
141
|
}
|
|
108
142
|
|
|
143
|
+
function behaviorPolicy(opt) {
|
|
144
|
+
if (!opt) return null;
|
|
145
|
+
const o = typeof opt === "object" ? opt : {};
|
|
146
|
+
const th = Number(o.threshold);
|
|
147
|
+
return {
|
|
148
|
+
b: o.block === true ? 1 : 0,
|
|
149
|
+
s: o.strict === true ? 1 : 0,
|
|
150
|
+
r: o.requireInteraction === false ? 0 : 1,
|
|
151
|
+
th: th >= 0.1 && th <= 1 ? th : DEFAULT_BEHAVIOR_THRESHOLD,
|
|
152
|
+
ag: o.blockAgents === "all" ? 2 : o.blockAgents === "identified" ? 1 : 0,
|
|
153
|
+
va: o.allowVerifiedAgents === true ? 1 : 0,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function requestAgentEvidence(evidence, opts) {
|
|
158
|
+
const req = opts.request;
|
|
159
|
+
if (!req?.headers) return null;
|
|
160
|
+
evidence.push(...agentFromHeaders(req.headers));
|
|
161
|
+
if (opts.verifyWebBotAuth !== true || !req.method || !req.url) return null;
|
|
162
|
+
if (!evidence.some((e) => e.evidence.startsWith("signature_agent:")))
|
|
163
|
+
return null;
|
|
164
|
+
return verifyWebBotAuth(req, {
|
|
165
|
+
authority:
|
|
166
|
+
opts.webBotAuthAuthority ??
|
|
167
|
+
(URL.canParse(req.url) ? new URL(req.url).host : null),
|
|
168
|
+
fetch: opts.fetch,
|
|
169
|
+
trustedOrigins: opts.trustedSignatureAgents,
|
|
170
|
+
nonceCheck: opts.webBotAuthNonceCheck,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const INSTR_ERROR_REASONS = {
|
|
175
|
+
csp_blocked: "instr_csp_blocked",
|
|
176
|
+
csp_script_blocked: "instr_csp_blocked",
|
|
177
|
+
natives_tampered: "instr_natives_tampered",
|
|
178
|
+
env_failed: "instr_env_failed",
|
|
179
|
+
script_failed: "instr_script_failed",
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const instrErrorReason = (code) =>
|
|
183
|
+
Object.hasOwn(INSTR_ERROR_REASONS, code)
|
|
184
|
+
? INSTR_ERROR_REASONS[code]
|
|
185
|
+
: "instr_failed";
|
|
186
|
+
|
|
187
|
+
async function evaluateBehavior(policy, trace, payload, opts, bound) {
|
|
188
|
+
if (policy.ag > 0 && bound?.host === false) {
|
|
189
|
+
return {
|
|
190
|
+
fail: fail(
|
|
191
|
+
bound.isolated
|
|
192
|
+
? "instr_isolated"
|
|
193
|
+
: bound.legacy
|
|
194
|
+
? "instr_widget_outdated"
|
|
195
|
+
: "instr_host_unreachable",
|
|
196
|
+
{ instr_error: true },
|
|
197
|
+
),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
const ctx = { strict: policy.s === 1, threshold: policy.th };
|
|
201
|
+
let pre = analyzeBehavior(trace, ctx);
|
|
202
|
+
if (pre.digest && typeof opts.behaviorReplayCheck === "function") {
|
|
203
|
+
const ttlMs = Math.max(60_000, (payload.exp ?? Date.now()) - Date.now());
|
|
204
|
+
let replayed = false;
|
|
205
|
+
try {
|
|
206
|
+
replayed = (await opts.behaviorReplayCheck(pre.digest, ttlMs)) === true;
|
|
207
|
+
} catch {}
|
|
208
|
+
if (replayed) pre = analyzeBehavior(trace, { ...ctx, replayed: true });
|
|
209
|
+
}
|
|
210
|
+
const evidence = (
|
|
211
|
+
trace && !validateTrace(trace) ? agentFromTrace(trace) : []
|
|
212
|
+
).map((e) =>
|
|
213
|
+
e.agent === "gemini-in-chrome" && bound?.browser === "brave"
|
|
214
|
+
? { ...e, agent: "brave-leo" }
|
|
215
|
+
: e,
|
|
216
|
+
);
|
|
217
|
+
if (bound) {
|
|
218
|
+
const seen = new Set(evidence.map((e) => e.evidence));
|
|
219
|
+
evidence.push(
|
|
220
|
+
...agentFromCodes(bound.all).filter((e) => !seen.has(e.evidence)),
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
const verification = await requestAgentEvidence(evidence, opts);
|
|
224
|
+
const agent = summarizeAgent(evidence, verification);
|
|
225
|
+
|
|
226
|
+
let verdict = pre.verdict;
|
|
227
|
+
if (agent?.detected && verdict !== "automated") verdict = "agent";
|
|
228
|
+
|
|
229
|
+
const behavior = {
|
|
230
|
+
score: pre.score,
|
|
231
|
+
verdict,
|
|
232
|
+
flags: pre.flags,
|
|
233
|
+
signals: pre.signals,
|
|
234
|
+
...(agent && { agent }),
|
|
235
|
+
};
|
|
236
|
+
const flags = pre.verdict === "missing" ? ["bh_missing"] : [...pre.flags];
|
|
237
|
+
if (agent?.detected) flags.push(`agent:${agent.name}`);
|
|
238
|
+
|
|
239
|
+
const blockAgent =
|
|
240
|
+
agent?.detected &&
|
|
241
|
+
!(policy.va === 1 && agent.verified) &&
|
|
242
|
+
(policy.ag === 2 || (policy.ag === 1 && agent.confidence === "high"));
|
|
243
|
+
if (blockAgent) {
|
|
244
|
+
return { fail: fail("behavior_agent", { behavior }) };
|
|
245
|
+
}
|
|
246
|
+
if (pre.verdict === "missing") {
|
|
247
|
+
if ((bound?.legacy || bound?.isolated) && policy.b !== 1) {
|
|
248
|
+
return { behavior, flags };
|
|
249
|
+
}
|
|
250
|
+
return { fail: fail("behavior_missing", { behavior }) };
|
|
251
|
+
}
|
|
252
|
+
if (policy.b === 1) {
|
|
253
|
+
if (pre.verdict === "automated") {
|
|
254
|
+
return { fail: fail("behavior_automated", { behavior }) };
|
|
255
|
+
}
|
|
256
|
+
if (policy.r === 1 && pre.verdict === "no_interaction") {
|
|
257
|
+
return { fail: fail("behavior_no_interaction", { behavior }) };
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return { behavior, flags };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function sealedInstrMeta(instr, expires) {
|
|
264
|
+
if (!instr?.binding && !Array.isArray(instr?.expectedVals)) {
|
|
265
|
+
throw new Error(
|
|
266
|
+
"[capjs-core] instrumentationGenerator must return the binding from generateInstrumentation, or expectedVals",
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
return {
|
|
270
|
+
id: instr.id,
|
|
271
|
+
...(Number.isFinite(instr.iat) && { iat: instr.iat }),
|
|
272
|
+
vars: instr.vars,
|
|
273
|
+
...(instr.binding
|
|
274
|
+
? { binding: instr.binding }
|
|
275
|
+
: { expectedVals: instr.expectedVals }),
|
|
276
|
+
blockAutomatedBrowsers: instr.blockAutomatedBrowsers === true,
|
|
277
|
+
blockAutomatedInteractions: instr.blockAutomatedInteractions === true,
|
|
278
|
+
detectAutomatedInteractions: instr.detectAutomatedInteractions === true,
|
|
279
|
+
behavior: instr.behavior === true,
|
|
280
|
+
behaviorEnforced: instr.behaviorEnforced === true,
|
|
281
|
+
expires,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function instrOptions(opts, ttlMs, bh) {
|
|
286
|
+
const base =
|
|
287
|
+
typeof opts.instrumentation === "object" && opts.instrumentation !== null
|
|
288
|
+
? opts.instrumentation
|
|
289
|
+
: {};
|
|
290
|
+
return {
|
|
291
|
+
...base,
|
|
292
|
+
ttlMs,
|
|
293
|
+
behavior: !!bh,
|
|
294
|
+
behaviorEnforced: !!bh && (bh.b === 1 || bh.ag > 0),
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
109
298
|
export async function generateChallenge(secret, opts = {}) {
|
|
110
299
|
assertSecret(secret);
|
|
111
300
|
|
|
@@ -145,29 +334,21 @@ export async function generateChallenge(secret, opts = {}) {
|
|
|
145
334
|
};
|
|
146
335
|
if (opts.scope) payload.sk = String(opts.scope);
|
|
147
336
|
if (opts.extra && typeof opts.extra === "object") payload.x = opts.extra;
|
|
337
|
+
const bh = behaviorPolicy(opts.behavior);
|
|
338
|
+
if (bh) payload.bh = bh;
|
|
148
339
|
|
|
149
340
|
let instrumentation;
|
|
150
|
-
if (opts.instrumentation) {
|
|
151
|
-
const instrOpts =
|
|
152
|
-
typeof opts.instrumentation === "object" ? opts.instrumentation : {};
|
|
341
|
+
if (opts.instrumentation || bh) {
|
|
153
342
|
const generator = opts.instrumentationGenerator || generateInstrumentation;
|
|
154
|
-
const instr = await generator(
|
|
155
|
-
payload.ei = encryptGcm(
|
|
156
|
-
{
|
|
157
|
-
id: instr.id,
|
|
158
|
-
expectedVals: instr.expectedVals,
|
|
159
|
-
vars: instr.vars,
|
|
160
|
-
blockAutomatedBrowsers: instr.blockAutomatedBrowsers,
|
|
161
|
-
expires,
|
|
162
|
-
},
|
|
163
|
-
secret,
|
|
164
|
-
);
|
|
343
|
+
const instr = await generator(instrOptions(opts, ttlMs, bh));
|
|
344
|
+
payload.ei = encryptGcm(sealedInstrMeta(instr, expires), secret);
|
|
165
345
|
instrumentation = instr.instrumentation;
|
|
166
346
|
}
|
|
167
347
|
|
|
168
348
|
const token = jwtSign(payload, secret);
|
|
169
349
|
const result = { challenge: { c, s, d }, token, expires };
|
|
170
350
|
if (instrumentation) result.instrumentation = instrumentation;
|
|
351
|
+
if (bh) result.behavior = true;
|
|
171
352
|
return result;
|
|
172
353
|
}
|
|
173
354
|
|
|
@@ -232,7 +413,22 @@ export async function validateChallenge(secret, body, opts = {}) {
|
|
|
232
413
|
}
|
|
233
414
|
}
|
|
234
415
|
|
|
416
|
+
if (typeof opts.consumeNonce === "function") {
|
|
417
|
+
const sig = jwtSigHex(body.token);
|
|
418
|
+
if (!sig) return fail("invalid_token");
|
|
419
|
+
const ttlMs = Math.max(1, payload.exp - Date.now());
|
|
420
|
+
let claimed;
|
|
421
|
+
try {
|
|
422
|
+
claimed = await opts.consumeNonce(sig, ttlMs);
|
|
423
|
+
} catch (err) {
|
|
424
|
+
return fail("nonce_store_error", { error: String(err?.message || err) });
|
|
425
|
+
}
|
|
426
|
+
if (!claimed) return fail("already_redeemed");
|
|
427
|
+
}
|
|
428
|
+
|
|
235
429
|
let riskFlags = [];
|
|
430
|
+
let bound = null;
|
|
431
|
+
let trace = null;
|
|
236
432
|
if (payload.ei) {
|
|
237
433
|
const instrMeta = decryptGcm(payload.ei, secret);
|
|
238
434
|
if (!instrMeta) return fail("instr_corrupted", { instr_error: true });
|
|
@@ -247,6 +443,8 @@ export async function validateChallenge(secret, body, opts = {}) {
|
|
|
247
443
|
: "instr_failed",
|
|
248
444
|
{ instr_error: true },
|
|
249
445
|
);
|
|
446
|
+
} else if (typeof body.instr_error === "string") {
|
|
447
|
+
return fail(instrErrorReason(body.instr_error), { instr_error: true });
|
|
250
448
|
} else if (body.instr_timeout === true) {
|
|
251
449
|
return fail("instr_timeout", { instr_error: true });
|
|
252
450
|
} else if (body.instr) {
|
|
@@ -258,22 +456,25 @@ export async function validateChallenge(secret, body, opts = {}) {
|
|
|
258
456
|
});
|
|
259
457
|
}
|
|
260
458
|
if (r.riskFlags?.length) riskFlags = [...riskFlags, ...r.riskFlags];
|
|
459
|
+
if (r.agent) bound = r.agent;
|
|
460
|
+
trace = r.trace ?? null;
|
|
261
461
|
} else {
|
|
262
462
|
return fail("instr_missing", { instr_error: true });
|
|
263
463
|
}
|
|
264
464
|
}
|
|
265
465
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
466
|
+
let behavior;
|
|
467
|
+
let agent = null;
|
|
468
|
+
if (payload.bh && typeof payload.bh === "object") {
|
|
469
|
+
const r = await evaluateBehavior(payload.bh, trace, payload, opts, bound);
|
|
470
|
+
if (r.fail) return r.fail;
|
|
471
|
+
behavior = r.behavior;
|
|
472
|
+
if (r.flags.length) riskFlags = [...riskFlags, ...r.flags];
|
|
473
|
+
} else {
|
|
474
|
+
const evidence = bound ? agentFromCodes(bound.all) : [];
|
|
475
|
+
const verification = await requestAgentEvidence(evidence, opts);
|
|
476
|
+
agent = evidence.length ? summarizeAgent(evidence, verification) : null;
|
|
477
|
+
if (agent?.detected) riskFlags = [...riskFlags, `agent:${agent.name}`];
|
|
277
478
|
}
|
|
278
479
|
|
|
279
480
|
const tokenTtlMs = opts.tokenTtlMs ?? DEFAULT_TOKEN_TTL_MS;
|
|
@@ -292,6 +493,8 @@ export async function validateChallenge(secret, body, opts = {}) {
|
|
|
292
493
|
scope: payload.sk ?? null,
|
|
293
494
|
iat: payload.iat,
|
|
294
495
|
riskFlags,
|
|
496
|
+
...(behavior && { behavior }),
|
|
497
|
+
...(agent && { agent }),
|
|
295
498
|
};
|
|
296
499
|
}
|
|
297
500
|
|
|
@@ -306,14 +509,21 @@ export async function validateChallenge(secret, body, opts = {}) {
|
|
|
306
509
|
scope: payload.sk ?? null,
|
|
307
510
|
iat: payload.iat,
|
|
308
511
|
riskFlags,
|
|
512
|
+
...(behavior && { behavior }),
|
|
513
|
+
...(agent && { agent }),
|
|
309
514
|
};
|
|
310
515
|
}
|
|
311
516
|
|
|
312
517
|
async function generateChallengeV2(secret, opts) {
|
|
313
|
-
const
|
|
518
|
+
const bh = behaviorPolicy(opts.behavior);
|
|
519
|
+
const requested =
|
|
314
520
|
Array.isArray(opts.protocols) && opts.protocols.length
|
|
315
521
|
? opts.protocols
|
|
316
522
|
: ["rsw"];
|
|
523
|
+
const protocols =
|
|
524
|
+
bh && !requested.includes("instrumentation")
|
|
525
|
+
? [...requested, "instrumentation"]
|
|
526
|
+
: requested;
|
|
317
527
|
const ttlMs = opts.expiresMs ?? DEFAULT_CHALLENGE_TTL_MS;
|
|
318
528
|
const now = Date.now();
|
|
319
529
|
const exp = now + ttlMs;
|
|
@@ -361,27 +571,16 @@ async function generateChallengeV2(secret, opts) {
|
|
|
361
571
|
expected.push({ protocol: "hashwx", ...minted.expected });
|
|
362
572
|
}
|
|
363
573
|
} else if (proto === "instrumentation") {
|
|
364
|
-
const instrOpts =
|
|
365
|
-
typeof opts.instrumentation === "object" &&
|
|
366
|
-
opts.instrumentation !== null
|
|
367
|
-
? opts.instrumentation
|
|
368
|
-
: {};
|
|
369
574
|
const generator =
|
|
370
575
|
opts.instrumentationGenerator || generateInstrumentation;
|
|
371
|
-
const instr = await generator(
|
|
576
|
+
const instr = await generator(instrOptions(opts, ttlMs, bh));
|
|
372
577
|
challenges.push({
|
|
373
578
|
protocol: "instrumentation",
|
|
374
579
|
payload: { blob: instr.instrumentation },
|
|
375
580
|
});
|
|
376
581
|
expected.push({
|
|
377
582
|
protocol: "instrumentation",
|
|
378
|
-
instrMeta:
|
|
379
|
-
id: instr.id,
|
|
380
|
-
expectedVals: instr.expectedVals,
|
|
381
|
-
vars: instr.vars,
|
|
382
|
-
blockAutomatedBrowsers: instr.blockAutomatedBrowsers,
|
|
383
|
-
expires: exp,
|
|
384
|
-
},
|
|
583
|
+
instrMeta: sealedInstrMeta(instr, exp),
|
|
385
584
|
});
|
|
386
585
|
} else {
|
|
387
586
|
throw new Error(`[capjs-core] unknown protocol '${proto}'`);
|
|
@@ -397,9 +596,12 @@ async function generateChallengeV2(secret, opts) {
|
|
|
397
596
|
};
|
|
398
597
|
if (opts.scope) tokenPayload.sk = String(opts.scope);
|
|
399
598
|
if (opts.extra && typeof opts.extra === "object") tokenPayload.x = opts.extra;
|
|
599
|
+
if (bh) tokenPayload.bh = bh;
|
|
400
600
|
|
|
401
601
|
const token = jwtSign(tokenPayload, secret);
|
|
402
|
-
|
|
602
|
+
const result = { token, format: 2, challenges, expires: exp };
|
|
603
|
+
if (bh) result.behavior = true;
|
|
604
|
+
return result;
|
|
403
605
|
}
|
|
404
606
|
|
|
405
607
|
async function validateChallengeV2(secret, body, payload, opts = {}) {
|
|
@@ -412,9 +614,13 @@ async function validateChallengeV2(secret, body, payload, opts = {}) {
|
|
|
412
614
|
const expected = decrypted.expected;
|
|
413
615
|
const solutions = body.solutions;
|
|
414
616
|
let riskFlags = [];
|
|
617
|
+
let bound = null;
|
|
618
|
+
let trace = null;
|
|
415
619
|
|
|
416
|
-
if (solutions.length !== expected.length)
|
|
620
|
+
if (!Array.isArray(solutions) || solutions.length !== expected.length)
|
|
621
|
+
return fail("invalid_solutions");
|
|
417
622
|
|
|
623
|
+
const instrSteps = [];
|
|
418
624
|
for (let i = 0; i < expected.length; i++) {
|
|
419
625
|
const e = expected[i];
|
|
420
626
|
const s = solutions[i];
|
|
@@ -432,32 +638,7 @@ async function validateChallengeV2(secret, body, payload, opts = {}) {
|
|
|
432
638
|
} else if (e.protocol === "hashwx") {
|
|
433
639
|
if (!(await verifyHashwxSolution(e, s))) return fail("invalid_solution");
|
|
434
640
|
} else if (e.protocol === "instrumentation") {
|
|
435
|
-
|
|
436
|
-
if (!meta) return fail("instr_corrupted", { instr_error: true });
|
|
437
|
-
if (meta.expires && Date.now() > meta.expires) {
|
|
438
|
-
return fail("instr_expired", { instr_error: true });
|
|
439
|
-
}
|
|
440
|
-
if (s.blocked === true) {
|
|
441
|
-
return fail(
|
|
442
|
-
meta.blockAutomatedBrowsers
|
|
443
|
-
? "instr_automated_browser"
|
|
444
|
-
: "instr_failed",
|
|
445
|
-
{ instr_error: true },
|
|
446
|
-
);
|
|
447
|
-
} else if (s.timeout === true) {
|
|
448
|
-
return fail("instr_timeout", { instr_error: true });
|
|
449
|
-
} else if (s.instr) {
|
|
450
|
-
const r = verifyInstrumentationResult(meta, s.instr);
|
|
451
|
-
if (!r.valid) {
|
|
452
|
-
return fail(r.reason || "instr_failed", {
|
|
453
|
-
instr_error: true,
|
|
454
|
-
...(r.blockedBy && { blockedBy: r.blockedBy }),
|
|
455
|
-
});
|
|
456
|
-
}
|
|
457
|
-
if (r.riskFlags?.length) riskFlags = [...riskFlags, ...r.riskFlags];
|
|
458
|
-
} else {
|
|
459
|
-
return fail("instr_missing", { instr_error: true });
|
|
460
|
-
}
|
|
641
|
+
instrSteps.push([e.instrMeta, s]);
|
|
461
642
|
} else {
|
|
462
643
|
return fail("invalid_solution");
|
|
463
644
|
}
|
|
@@ -476,6 +657,52 @@ async function validateChallengeV2(secret, body, payload, opts = {}) {
|
|
|
476
657
|
if (!claimed) return fail("already_redeemed");
|
|
477
658
|
}
|
|
478
659
|
|
|
660
|
+
for (const [meta, s] of instrSteps) {
|
|
661
|
+
if (!meta) return fail("instr_corrupted", { instr_error: true });
|
|
662
|
+
if (meta.expires && Date.now() > meta.expires) {
|
|
663
|
+
return fail("instr_expired", { instr_error: true });
|
|
664
|
+
}
|
|
665
|
+
if (s.blocked === true) {
|
|
666
|
+
return fail(
|
|
667
|
+
meta.blockAutomatedBrowsers
|
|
668
|
+
? "instr_automated_browser"
|
|
669
|
+
: "instr_failed",
|
|
670
|
+
{ instr_error: true },
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
if (typeof s.error === "string") {
|
|
674
|
+
return fail(instrErrorReason(s.error), { instr_error: true });
|
|
675
|
+
}
|
|
676
|
+
if (s.timeout === true) {
|
|
677
|
+
return fail("instr_timeout", { instr_error: true });
|
|
678
|
+
}
|
|
679
|
+
if (!s.instr) return fail("instr_missing", { instr_error: true });
|
|
680
|
+
const r = verifyInstrumentationResult(meta, s.instr);
|
|
681
|
+
if (!r.valid) {
|
|
682
|
+
return fail(r.reason || "instr_failed", {
|
|
683
|
+
instr_error: true,
|
|
684
|
+
...(r.blockedBy && { blockedBy: r.blockedBy }),
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
if (r.riskFlags?.length) riskFlags = [...riskFlags, ...r.riskFlags];
|
|
688
|
+
if (r.agent) bound = r.agent;
|
|
689
|
+
trace = r.trace ?? null;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
let behavior;
|
|
693
|
+
let agent = null;
|
|
694
|
+
if (payload.bh && typeof payload.bh === "object") {
|
|
695
|
+
const r = await evaluateBehavior(payload.bh, trace, payload, opts, bound);
|
|
696
|
+
if (r.fail) return r.fail;
|
|
697
|
+
behavior = r.behavior;
|
|
698
|
+
if (r.flags.length) riskFlags = [...riskFlags, ...r.flags];
|
|
699
|
+
} else {
|
|
700
|
+
const evidence = bound ? agentFromCodes(bound.all) : [];
|
|
701
|
+
const verification = await requestAgentEvidence(evidence, opts);
|
|
702
|
+
agent = evidence.length ? summarizeAgent(evidence, verification) : null;
|
|
703
|
+
if (agent?.detected) riskFlags = [...riskFlags, `agent:${agent.name}`];
|
|
704
|
+
}
|
|
705
|
+
|
|
479
706
|
const tokenTtlMs = opts.tokenTtlMs ?? DEFAULT_TOKEN_TTL_MS;
|
|
480
707
|
const tokenExpires = Date.now() + tokenTtlMs;
|
|
481
708
|
|
|
@@ -492,6 +719,8 @@ async function validateChallengeV2(secret, body, payload, opts = {}) {
|
|
|
492
719
|
scope: payload.sk ?? null,
|
|
493
720
|
iat: payload.iat,
|
|
494
721
|
riskFlags,
|
|
722
|
+
...(behavior && { behavior }),
|
|
723
|
+
...(agent && { agent }),
|
|
495
724
|
};
|
|
496
725
|
}
|
|
497
726
|
|
|
@@ -506,5 +735,7 @@ async function validateChallengeV2(secret, body, payload, opts = {}) {
|
|
|
506
735
|
scope: payload.sk ?? null,
|
|
507
736
|
iat: payload.iat,
|
|
508
737
|
riskFlags,
|
|
738
|
+
...(behavior && { behavior }),
|
|
739
|
+
...(agent && { agent }),
|
|
509
740
|
};
|
|
510
741
|
}
|