@ai-ecoverse/slicc-browsers 1.7.2 → 1.7.4
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 +42 -12
- package/fixture-server.mjs +4 -2
- package/guard-driver.mjs +30 -8
- package/guard-runner.mjs +360 -84
- package/guard-vectors.json +2779 -686
- package/package.json +5 -3
- package/schema/guard-vectors.schema.json +105 -5
package/guard-runner.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
expand,
|
|
5
5
|
hop,
|
|
6
6
|
isRefusal,
|
|
7
|
+
loadedAfter,
|
|
7
8
|
outcome,
|
|
8
9
|
summary as report,
|
|
9
10
|
result,
|
|
@@ -11,6 +12,7 @@ import {
|
|
|
11
12
|
} from './guard-driver.mjs';
|
|
12
13
|
|
|
13
14
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
15
|
+
const namedResultMs = 10000;
|
|
14
16
|
const q = JSON.stringify;
|
|
15
17
|
const at = (value, path = '') =>
|
|
16
18
|
path
|
|
@@ -108,6 +110,8 @@ class Execution {
|
|
|
108
110
|
this.answers = [];
|
|
109
111
|
this.attachSettings = new Map();
|
|
110
112
|
this.childSteps = [];
|
|
113
|
+
this.childRuns = new Map();
|
|
114
|
+
this.named = new Map();
|
|
111
115
|
this.processed = new Set();
|
|
112
116
|
this.eventStart = this.client.events.length;
|
|
113
117
|
this.actionEventStart = this.eventStart;
|
|
@@ -150,12 +154,9 @@ class Execution {
|
|
|
150
154
|
async navigate(client, session, url) {
|
|
151
155
|
const start = client.events.length;
|
|
152
156
|
const out = await outcome(client.send('Page.navigate', { url }, session));
|
|
153
|
-
if (out.ok && !out.result.errorText)
|
|
157
|
+
if (out.ok && !out.result.errorText && out.result.loaderId)
|
|
154
158
|
await this.poll(
|
|
155
|
-
() =>
|
|
156
|
-
client.events
|
|
157
|
-
.slice(start)
|
|
158
|
-
.some((event) => event.method === 'Page.loadEventFired' && event.sessionId === session),
|
|
159
|
+
() => loadedAfter(client.events.slice(start), session, out.result.loaderId),
|
|
159
160
|
3000
|
|
160
161
|
);
|
|
161
162
|
return out;
|
|
@@ -283,12 +284,99 @@ class Execution {
|
|
|
283
284
|
tree.frame.url.startsWith('chrome-error://')) ||
|
|
284
285
|
(tree.childFrames ?? []).some(visit))
|
|
285
286
|
);
|
|
286
|
-
|
|
287
|
+
if (
|
|
287
288
|
read.ok &&
|
|
288
289
|
(spec.frame
|
|
289
290
|
? (read.result.frameTree.childFrames ?? []).some(visit)
|
|
290
291
|
: visit(read.result.frameTree))
|
|
292
|
+
)
|
|
293
|
+
return true;
|
|
294
|
+
return spec.frame ? Boolean(await this.blockedFrameTarget(urls, targetId)) : false;
|
|
295
|
+
} finally {
|
|
296
|
+
await outcome(
|
|
297
|
+
this.reader.send('Target.detachFromTarget', { sessionId: attached.result.sessionId })
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async frameIds(targetId) {
|
|
303
|
+
const attached = await outcome(
|
|
304
|
+
this.reader.send('Target.attachToTarget', { targetId, flatten: true })
|
|
305
|
+
);
|
|
306
|
+
if (!attached.ok) return [];
|
|
307
|
+
try {
|
|
308
|
+
const read = await outcome(
|
|
309
|
+
this.reader.send('Page.getFrameTree', {}, attached.result.sessionId)
|
|
291
310
|
);
|
|
311
|
+
const ids = (tree) => [tree.frame.id, ...(tree.childFrames ?? []).flatMap(ids)];
|
|
312
|
+
return read.ok ? ids(read.result.frameTree) : [];
|
|
313
|
+
} finally {
|
|
314
|
+
await outcome(
|
|
315
|
+
this.reader.send('Target.detachFromTarget', { sessionId: attached.result.sessionId })
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
async blockedFrameTarget(urls, parentTarget) {
|
|
321
|
+
const parents = await this.frameIds(parentTarget);
|
|
322
|
+
const listed = await outcome(this.reader.send('Target.getTargets', {}));
|
|
323
|
+
if (!listed.ok) return undefined;
|
|
324
|
+
for (const info of listed.result.targetInfos) {
|
|
325
|
+
if (info.type !== 'iframe' || !parents.includes(info.parentFrameId)) continue;
|
|
326
|
+
const attached = await outcome(
|
|
327
|
+
this.reader.send('Target.attachToTarget', { targetId: info.targetId, flatten: true })
|
|
328
|
+
);
|
|
329
|
+
if (!attached.ok) continue;
|
|
330
|
+
try {
|
|
331
|
+
const read = await outcome(
|
|
332
|
+
this.reader.send('Page.getFrameTree', {}, attached.result.sessionId)
|
|
333
|
+
);
|
|
334
|
+
const frame = read.result?.frameTree?.frame;
|
|
335
|
+
if (frame?.url.startsWith('chrome-error://') && urls.includes(frame.unreachableUrl))
|
|
336
|
+
return info.targetId;
|
|
337
|
+
} finally {
|
|
338
|
+
await outcome(
|
|
339
|
+
this.reader.send('Target.detachFromTarget', { sessionId: attached.result.sessionId })
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return undefined;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async frameBlocked(spec) {
|
|
347
|
+
const urls = (spec.anyOf ?? [spec.url]).map(address);
|
|
348
|
+
const targetId = spec.session === 'child' ? this.targets.at(-1) : this.pageTarget;
|
|
349
|
+
if (!targetId) return false;
|
|
350
|
+
const frame = await this.blockedFrameTarget(urls, targetId);
|
|
351
|
+
if (!frame) return false;
|
|
352
|
+
const attached = await outcome(
|
|
353
|
+
this.client.send('Target.attachToTarget', { targetId: frame, flatten: true })
|
|
354
|
+
);
|
|
355
|
+
if (attached.ok)
|
|
356
|
+
await outcome(
|
|
357
|
+
this.client.send('Target.detachFromTarget', { sessionId: attached.result.sessionId })
|
|
358
|
+
);
|
|
359
|
+
return !attached.ok;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
async history(spec) {
|
|
363
|
+
const urls = (spec.anyOf ?? [spec.url]).map(address);
|
|
364
|
+
const targetId = spec.session === 'child' ? this.targets.at(-1) : this.pageTarget;
|
|
365
|
+
if (!targetId) return false;
|
|
366
|
+
const pre = await Promise.all(urls.map((url) => this.attemptCheck({ pre: url })));
|
|
367
|
+
if (!pre.some(Boolean)) return false;
|
|
368
|
+
const loaded = await Promise.all(urls.map((url) => this.attemptCheck({ loaded: url })));
|
|
369
|
+
const unloaded = urls.filter((_, index) => !loaded[index]);
|
|
370
|
+
if (!unloaded.length) return false;
|
|
371
|
+
const attached = await outcome(
|
|
372
|
+
this.reader.send('Target.attachToTarget', { targetId, flatten: true })
|
|
373
|
+
);
|
|
374
|
+
if (!attached.ok) return false;
|
|
375
|
+
try {
|
|
376
|
+
const read = await outcome(
|
|
377
|
+
this.reader.send('Page.getNavigationHistory', {}, attached.result.sessionId)
|
|
378
|
+
);
|
|
379
|
+
return read.ok && read.result.entries.some((entry) => unloaded.includes(entry.url));
|
|
292
380
|
} finally {
|
|
293
381
|
await outcome(
|
|
294
382
|
this.reader.send('Target.detachFromTarget', { sessionId: attached.result.sessionId })
|
|
@@ -339,50 +427,50 @@ class Execution {
|
|
|
339
427
|
event.params.targetInfo?.targetId !== this.pageTarget &&
|
|
340
428
|
event.params.targetInfo?.targetId !== this.canaryTarget &&
|
|
341
429
|
event.params.sessionId !== this.sessions.page
|
|
342
|
-
)
|
|
343
|
-
|
|
344
|
-
if (
|
|
345
|
-
info?.browserContextId &&
|
|
346
|
-
this.vars.context &&
|
|
347
|
-
info.browserContextId !== this.vars.context
|
|
348
|
-
)
|
|
349
|
-
continue;
|
|
350
|
-
this.sessions.child = event.params.sessionId;
|
|
351
|
-
if (info?.targetId) this.targets.push(info.targetId);
|
|
352
|
-
this.pending.push(
|
|
353
|
-
(async () => {
|
|
354
|
-
const reload = this.childSteps.findIndex((step) => step.cdp === 'Page.reload');
|
|
355
|
-
if (reload < 0) await this.execute(this.childSteps, event.params.sessionId);
|
|
356
|
-
else {
|
|
357
|
-
await this.execute(this.childSteps.slice(0, reload), event.params.sessionId);
|
|
358
|
-
const start = this.client.events.length;
|
|
359
|
-
await outcome(
|
|
360
|
-
this.client.send('Runtime.runIfWaitingForDebugger', {}, event.params.sessionId)
|
|
361
|
-
);
|
|
362
|
-
await this.poll(
|
|
363
|
-
() =>
|
|
364
|
-
this.client.events
|
|
365
|
-
.slice(start)
|
|
366
|
-
.some(
|
|
367
|
-
(e) =>
|
|
368
|
-
e.method === 'Page.loadEventFired' &&
|
|
369
|
-
e.sessionId === event.params.sessionId
|
|
370
|
-
),
|
|
371
|
-
1500
|
|
372
|
-
);
|
|
373
|
-
await this.execute(this.childSteps.slice(reload), event.params.sessionId);
|
|
374
|
-
}
|
|
375
|
-
await outcome(
|
|
376
|
-
this.client.send('Runtime.runIfWaitingForDebugger', {}, event.params.sessionId)
|
|
377
|
-
);
|
|
378
|
-
})().catch((error) => this.errors.push(String(error.message ?? error)))
|
|
379
|
-
);
|
|
380
|
-
}
|
|
430
|
+
)
|
|
431
|
+
this.adopt(event);
|
|
381
432
|
}
|
|
382
433
|
await sleep(10);
|
|
383
434
|
}
|
|
384
435
|
}
|
|
385
436
|
|
|
437
|
+
adopt(event) {
|
|
438
|
+
const info = event.params.targetInfo;
|
|
439
|
+
const child = event.params.sessionId;
|
|
440
|
+
if (info?.browserContextId && this.vars.context && info.browserContextId !== this.vars.context)
|
|
441
|
+
return;
|
|
442
|
+
if (this.childWhere && !this.childMatches(info)) {
|
|
443
|
+
if (event.params.waitingForDebugger)
|
|
444
|
+
this.pending.push(outcome(this.client.send('Runtime.runIfWaitingForDebugger', {}, child)));
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
this.sessions.child = child;
|
|
448
|
+
if (info?.targetId) this.targets.push(info.targetId);
|
|
449
|
+
const started = (async () => {
|
|
450
|
+
const reload = this.childSteps.findIndex((step) => step.cdp === 'Page.reload');
|
|
451
|
+
if (reload < 0) await this.execute(this.childSteps, child);
|
|
452
|
+
else {
|
|
453
|
+
await this.execute(this.childSteps.slice(0, reload), child);
|
|
454
|
+
const start = this.client.events.length;
|
|
455
|
+
await outcome(this.client.send('Runtime.runIfWaitingForDebugger', {}, child));
|
|
456
|
+
await this.poll(
|
|
457
|
+
() =>
|
|
458
|
+
this.client.events
|
|
459
|
+
.slice(start)
|
|
460
|
+
.some((e) => e.method === 'Page.loadEventFired' && e.sessionId === child),
|
|
461
|
+
1500
|
|
462
|
+
);
|
|
463
|
+
await this.execute(this.childSteps.slice(reload), child);
|
|
464
|
+
}
|
|
465
|
+
const resumed = this.client.events.length;
|
|
466
|
+
await outcome(this.client.send('Runtime.runIfWaitingForDebugger', {}, child));
|
|
467
|
+
if (this.childSteps.some((step) => step.cdp === 'Page.enable'))
|
|
468
|
+
await this.poll(() => this.firstLoad(child, resumed), 5000);
|
|
469
|
+
})().catch((error) => this.errors.push(String(error.message ?? error)));
|
|
470
|
+
this.childRuns.set(child, started);
|
|
471
|
+
this.pending.push(started);
|
|
472
|
+
}
|
|
473
|
+
|
|
386
474
|
async answer(event) {
|
|
387
475
|
const p = event.params;
|
|
388
476
|
const rule = this.answers.find(
|
|
@@ -410,23 +498,58 @@ class Execution {
|
|
|
410
498
|
this.calls[`answer.${p.requestId}.${event.method}`] = out;
|
|
411
499
|
}
|
|
412
500
|
|
|
501
|
+
firstLoad(session, from) {
|
|
502
|
+
const events = this.client.events.slice(from);
|
|
503
|
+
return events.some(
|
|
504
|
+
(event, index) =>
|
|
505
|
+
event.method === 'Page.frameNavigated' &&
|
|
506
|
+
event.sessionId === session &&
|
|
507
|
+
!event.params?.frame?.parentId &&
|
|
508
|
+
events
|
|
509
|
+
.slice(index + 1)
|
|
510
|
+
.some((e) => e.method === 'Page.loadEventFired' && e.sessionId === session)
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
childMatches(info) {
|
|
515
|
+
if (this.childWhere.openerId === 'page') return info?.openerId === this.pageTarget;
|
|
516
|
+
return String(info?.url ?? '').startsWith(this.childWhere.urlPrefix);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
async childrenSettled() {
|
|
520
|
+
await Promise.all([...this.childRuns.values()]);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
detached(session) {
|
|
524
|
+
return this.client.events
|
|
525
|
+
.slice(this.eventStart)
|
|
526
|
+
.some((e) => e.method === 'Target.detachedFromTarget' && e.params?.sessionId === session);
|
|
527
|
+
}
|
|
528
|
+
|
|
413
529
|
async execute(list, childSession, phase = 'child') {
|
|
530
|
+
if (phase === 'trigger') await this.childrenSettled();
|
|
414
531
|
for (const raw of list) {
|
|
415
532
|
const step = expand(raw, this.vars);
|
|
416
533
|
if (!supported(step.requires, this.options.capabilities) || step.text) continue;
|
|
534
|
+
if (!childSession && step.session === 'child') await this.childrenSettled();
|
|
417
535
|
const session = childSession ?? this.session(step.session);
|
|
418
536
|
const out = await this.operation(step, session, phase);
|
|
419
537
|
if (!out) continue;
|
|
538
|
+
const gone = Boolean(childSession) && !out.ok && this.detached(childSession);
|
|
539
|
+
const refused =
|
|
540
|
+
!gone &&
|
|
541
|
+
(step.refusable
|
|
542
|
+
? isRefusal(out, step)
|
|
543
|
+
: /refused/.test(String(out.error?.message ?? out.error)));
|
|
420
544
|
this.trace.push({
|
|
421
545
|
phase,
|
|
422
546
|
declared: Boolean(step.refusable),
|
|
423
547
|
session,
|
|
424
548
|
out,
|
|
425
|
-
refused
|
|
426
|
-
|
|
427
|
-
: /refused/.test(String(out.error?.message ?? out.error)),
|
|
549
|
+
refused,
|
|
550
|
+
...(gone ? { detached: true } : {}),
|
|
428
551
|
});
|
|
429
|
-
if (step.refusable) this.refused[step.refusal.name] =
|
|
552
|
+
if (step.refusable) this.refused[step.refusal.name] = refused;
|
|
430
553
|
if (step.as && !out.result?.pending) this.calls[step.as] = out;
|
|
431
554
|
if (step.cdp) (this.calls.$cdp ??= []).push({ method: step.cdp, ...out });
|
|
432
555
|
this.options.debug?.({ step, out, side: this.side });
|
|
@@ -436,6 +559,7 @@ class Execution {
|
|
|
436
559
|
async operation(step, session, phase) {
|
|
437
560
|
if (step.children) {
|
|
438
561
|
this.childSteps = step.children;
|
|
562
|
+
this.childWhere = step.where;
|
|
439
563
|
return;
|
|
440
564
|
}
|
|
441
565
|
if (step.answer) {
|
|
@@ -457,12 +581,13 @@ class Execution {
|
|
|
457
581
|
}
|
|
458
582
|
|
|
459
583
|
async settle(settle) {
|
|
584
|
+
if ((settle.dependsOn ?? []).some((name) => this.refused[name])) return;
|
|
460
585
|
if (settle.variant) await sleep(this.delay);
|
|
461
586
|
if (settle.title && !this.reader) {
|
|
462
587
|
await sleep(settle.ms ?? 1500);
|
|
463
588
|
return;
|
|
464
589
|
}
|
|
465
|
-
await this.poll(async () => {
|
|
590
|
+
const met = await this.poll(async () => {
|
|
466
591
|
if (settle.beacon)
|
|
467
592
|
return (
|
|
468
593
|
(await this.requestCheck({ beacon: settle.beacon }, this.actionRequestStart)) &&
|
|
@@ -470,10 +595,20 @@ class Execution {
|
|
|
470
595
|
(await this.requestCheck({ served: this.actionDestination }, this.actionRequestStart)))
|
|
471
596
|
);
|
|
472
597
|
if (settle.title) return this.check({ title: settle.title, session: settle.session });
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
598
|
+
const events = this.client.events.slice(this.actionEventStart);
|
|
599
|
+
return events.some(
|
|
600
|
+
(event, index) =>
|
|
601
|
+
event.method === 'Page.frameNavigated' &&
|
|
602
|
+
!event.params?.frame?.parentId &&
|
|
603
|
+
events
|
|
604
|
+
.slice(index + 1)
|
|
605
|
+
.some((e) => e.method === 'Page.loadEventFired' && e.sessionId === event.sessionId)
|
|
606
|
+
);
|
|
476
607
|
}, settle.ms ?? 1500);
|
|
608
|
+
if (settle.required && !met)
|
|
609
|
+
this.errors.push(
|
|
610
|
+
`settle: no ${settle.beacon ? `beacon from ${settle.beacon}` : settle.title ? `title ${settle.title}` : 'load'} within ${settle.ms ?? 1500}ms`
|
|
611
|
+
);
|
|
477
612
|
}
|
|
478
613
|
|
|
479
614
|
async guess(guess, session) {
|
|
@@ -502,18 +637,19 @@ class Execution {
|
|
|
502
637
|
if (step.cdp === 'Target.setAutoAttach' && step.params?.autoAttach === false)
|
|
503
638
|
this.cleanupEventStart = this.client.events.length;
|
|
504
639
|
const params = { ...step.params };
|
|
505
|
-
if (
|
|
506
|
-
/^Storage\.(getCookies|setCookies|clearCookies)$/.test(step.cdp) &&
|
|
507
|
-
this.vars.context &&
|
|
508
|
-
!params.browserContextId
|
|
509
|
-
)
|
|
510
|
-
params.browserContextId = this.vars.context;
|
|
511
640
|
const session =
|
|
512
641
|
step.session === 'page-or-browser' &&
|
|
513
642
|
supported(['browserSession'], this.options.capabilities) &&
|
|
514
643
|
/^Storage\.(getCookies|setCookies|clearCookies)$/.test(step.cdp)
|
|
515
644
|
? undefined
|
|
516
645
|
: sessionId;
|
|
646
|
+
if (
|
|
647
|
+
/^Storage\.(getCookies|setCookies|clearCookies)$/.test(step.cdp) &&
|
|
648
|
+
this.vars.context &&
|
|
649
|
+
!params.browserContextId &&
|
|
650
|
+
session === undefined
|
|
651
|
+
)
|
|
652
|
+
params.browserContextId = this.vars.context;
|
|
517
653
|
const out = await outcome(this.client.send(step.cdp, params, session));
|
|
518
654
|
if (step.cdp === 'Target.setAutoAttach' && out.ok) {
|
|
519
655
|
if (params.autoAttach) this.attachSettings.set(session, true);
|
|
@@ -568,11 +704,11 @@ class Execution {
|
|
|
568
704
|
if (pre) expression = `(() => { ${pre} return (${expression}); })()`;
|
|
569
705
|
const promise = this.evaluate(expression, session);
|
|
570
706
|
if (step.background) {
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
);
|
|
707
|
+
const done = promise.then((out) => {
|
|
708
|
+
if (typeof step.as === 'string') this.calls[step.as] = out;
|
|
709
|
+
});
|
|
710
|
+
this.pending.push(done);
|
|
711
|
+
if (typeof step.as === 'string') this.named.set(step.as, done);
|
|
576
712
|
return { ok: true, result: { pending: true } };
|
|
577
713
|
}
|
|
578
714
|
const out = await promise;
|
|
@@ -584,9 +720,38 @@ class Execution {
|
|
|
584
720
|
return out;
|
|
585
721
|
}
|
|
586
722
|
|
|
723
|
+
async namedSettled(names) {
|
|
724
|
+
const waits = names.map((name) => this.named.get(name)).filter(Boolean);
|
|
725
|
+
if (!waits.length) return true;
|
|
726
|
+
let timer;
|
|
727
|
+
const expired = new Promise((resolve) => {
|
|
728
|
+
timer = setTimeout(() => resolve(false), this.options.namedResultMs ?? namedResultMs);
|
|
729
|
+
});
|
|
730
|
+
const done = await Promise.race([Promise.all(waits).then(() => true), expired]);
|
|
731
|
+
clearTimeout(timer);
|
|
732
|
+
if (!done) this.pending = this.pending.filter((work) => !waits.includes(work));
|
|
733
|
+
return done;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
async ready(c) {
|
|
737
|
+
if (c.observer === 'unguarded' && !this.reader) return false;
|
|
738
|
+
return this.namedReady(c);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
async namedReady(c) {
|
|
742
|
+
const names = [c.result, ...(c.compare ?? []), c.refused, c.reached]
|
|
743
|
+
.flat()
|
|
744
|
+
.filter((name) => typeof name === 'string' && this.named.has(name));
|
|
745
|
+
if (!names.length || (await this.namedSettled(names))) return true;
|
|
746
|
+
this.errors.push(
|
|
747
|
+
`${names.join(', ')} did not complete within ${this.options.namedResultMs ?? namedResultMs}ms`
|
|
748
|
+
);
|
|
749
|
+
return false;
|
|
750
|
+
}
|
|
751
|
+
|
|
587
752
|
async check(check) {
|
|
588
753
|
const c = expand(check, this.vars);
|
|
589
|
-
if (
|
|
754
|
+
if (!(await this.ready(c))) return false;
|
|
590
755
|
const events = this.client.events.slice(this.eventStart);
|
|
591
756
|
const evidence = await this.evidence(c);
|
|
592
757
|
if (evidence !== undefined) return evidence;
|
|
@@ -671,6 +836,8 @@ class Execution {
|
|
|
671
836
|
);
|
|
672
837
|
if (c.state) return this.readState(c.state);
|
|
673
838
|
if (c.blocked) return this.blocked(c.blocked, c.observer);
|
|
839
|
+
if (c.history) return this.history(c.history);
|
|
840
|
+
if (c.frameBlocked) return this.frameBlocked(c.frameBlocked);
|
|
674
841
|
if (c.pre || c.loaded) return this.attemptCheck(c);
|
|
675
842
|
}
|
|
676
843
|
|
|
@@ -786,6 +953,8 @@ class Execution {
|
|
|
786
953
|
if (!supported(observation.requires, this.options.capabilities)) continue;
|
|
787
954
|
if ((observation.dependsOn ?? []).some((dep) => this.refused[dep])) continue;
|
|
788
955
|
observed[name] = await this.check(observation.check);
|
|
956
|
+
if (observation.eitherWhen && this.side === 'guarded')
|
|
957
|
+
observed[`${name}.either`] = await this.check(observation.eitherWhen);
|
|
789
958
|
}
|
|
790
959
|
this.options.debug?.({
|
|
791
960
|
events: this.client.events.slice(this.eventStart),
|
|
@@ -826,7 +995,9 @@ const observerRequired = (check) => {
|
|
|
826
995
|
const needsObserver = (vector) =>
|
|
827
996
|
vector.control.via === 'unguarded' ||
|
|
828
997
|
observerRequired(vector.liveCheck) ||
|
|
829
|
-
Object.values(vector.observe ?? {}).some(
|
|
998
|
+
Object.values(vector.observe ?? {}).some(
|
|
999
|
+
(observation) => observerRequired(observation.check) || observerRequired(observation.eitherWhen)
|
|
1000
|
+
);
|
|
830
1001
|
|
|
831
1002
|
const inactive = (vector, target, capabilities) =>
|
|
832
1003
|
vector.skip?.target === target ||
|
|
@@ -834,16 +1005,87 @@ const inactive = (vector, target, capabilities) =>
|
|
|
834
1005
|
!supported(vector.requires, capabilities) ||
|
|
835
1006
|
(vector.scope !== 'any' && vector.scope !== capabilities.scope);
|
|
836
1007
|
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
1008
|
+
const allSteps = (list) => list.flatMap((step) => [step, ...allSteps(step.children ?? [])]);
|
|
1009
|
+
const browserContextStep = (vector, capabilities) =>
|
|
1010
|
+
allSteps([...vector.setup, ...vector.trigger]).some(
|
|
1011
|
+
(step) =>
|
|
1012
|
+
step.params?.browserContextId !== undefined ||
|
|
1013
|
+
(step.session === 'page-or-browser' &&
|
|
1014
|
+
/^Storage\.(getCookies|setCookies|clearCookies)$/.test(step.cdp ?? '') &&
|
|
1015
|
+
supported(['browserContexts'], capabilities))
|
|
1016
|
+
);
|
|
1017
|
+
|
|
1018
|
+
const originDefaults = { plain: 'ordinary', cross: 'client', log: 'ordinary', own: 'client' };
|
|
1019
|
+
const knownOrigins = [
|
|
1020
|
+
'ordinary',
|
|
1021
|
+
'ordinary-same-site',
|
|
1022
|
+
'seven',
|
|
1023
|
+
'vault',
|
|
1024
|
+
'vault-x',
|
|
1025
|
+
'apex',
|
|
1026
|
+
'own',
|
|
1027
|
+
'client',
|
|
1028
|
+
'plain',
|
|
1029
|
+
'cross',
|
|
1030
|
+
'log',
|
|
1031
|
+
];
|
|
1032
|
+
|
|
1033
|
+
function nullOrigins(value, targets) {
|
|
1034
|
+
return [...q(value).matchAll(/\{([A-Za-z][A-Za-z-]*)\}/g)]
|
|
1035
|
+
.map((match) => (match[1] === 'beacon' || match[1] === 'probe' ? 'log' : match[1]))
|
|
1036
|
+
.map((name) => (targets[name] === undefined ? (originDefaults[name] ?? name) : name))
|
|
1037
|
+
.filter((name) => knownOrigins.includes(name) && (targets[name] ?? null) === null);
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
function prune(check, targets) {
|
|
1041
|
+
if (!check || typeof check !== 'object') return { check };
|
|
1042
|
+
if (check.any) {
|
|
1043
|
+
const parts = check.any.map((item) => prune(item, targets));
|
|
1044
|
+
const kept = parts.filter((part) => !part.missing).map((part) => part.check);
|
|
1045
|
+
return kept.length ? { check: { ...check, any: kept } } : { missing: parts[0]?.missing };
|
|
1046
|
+
}
|
|
1047
|
+
if (check.all || check.not) {
|
|
1048
|
+
const parts = (check.all ?? [check.not]).map((item) => prune(item, targets));
|
|
1049
|
+
const missing = parts.find((part) => part.missing)?.missing;
|
|
1050
|
+
if (missing) return { missing };
|
|
1051
|
+
return {
|
|
1052
|
+
check: check.all
|
|
1053
|
+
? { ...check, all: parts.map((part) => part.check) }
|
|
1054
|
+
: { ...check, not: parts[0].check },
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
const missing = nullOrigins(check, targets)[0];
|
|
1058
|
+
return missing ? { missing } : { check };
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
function available(vector, target, targets = {}) {
|
|
1062
|
+
const fixed = nullOrigins(
|
|
1063
|
+
[target, '{plain}', vector.control.origin, vector.setup, vector.trigger].map((value) =>
|
|
1064
|
+
typeof value === 'string' && !value.startsWith('{') ? `{${value}}` : value
|
|
1065
|
+
),
|
|
1066
|
+
targets
|
|
1067
|
+
)[0];
|
|
1068
|
+
if (fixed) return { missing: fixed };
|
|
1069
|
+
const live = prune(vector.liveCheck, targets);
|
|
1070
|
+
if (live.missing) return { missing: live.missing };
|
|
1071
|
+
const observe = {};
|
|
1072
|
+
for (const [name, observation] of Object.entries(vector.observe ?? {})) {
|
|
1073
|
+
const pruned = prune(observation.check, targets);
|
|
1074
|
+
if (pruned.missing) return { missing: pruned.missing };
|
|
1075
|
+
const either = observation.eitherWhen ? prune(observation.eitherWhen, targets) : undefined;
|
|
1076
|
+
observe[name] = {
|
|
1077
|
+
...observation,
|
|
1078
|
+
check: pruned.check,
|
|
1079
|
+
...(observation.eitherWhen ? { eitherWhen: either.missing ? undefined : either.check } : {}),
|
|
1080
|
+
};
|
|
1081
|
+
}
|
|
1082
|
+
return {
|
|
1083
|
+
vector: {
|
|
1084
|
+
...vector,
|
|
1085
|
+
...(vector.liveCheck ? { liveCheck: live.check } : {}),
|
|
1086
|
+
...(vector.observe ? { observe } : {}),
|
|
1087
|
+
},
|
|
1088
|
+
};
|
|
847
1089
|
}
|
|
848
1090
|
|
|
849
1091
|
function variables(targets, target, side, slot, index) {
|
|
@@ -865,7 +1107,9 @@ function variables(targets, target, side, slot, index) {
|
|
|
865
1107
|
}
|
|
866
1108
|
|
|
867
1109
|
async function executeRow(job, opts, clients, set, slot, index) {
|
|
868
|
-
const {
|
|
1110
|
+
const { target, variant } = job;
|
|
1111
|
+
const scoped = available(job.vector, target, opts.targets);
|
|
1112
|
+
const vector = scoped.vector ?? job.vector;
|
|
869
1113
|
const base = {
|
|
870
1114
|
id: vector.id,
|
|
871
1115
|
target,
|
|
@@ -874,7 +1118,7 @@ async function executeRow(job, opts, clients, set, slot, index) {
|
|
|
874
1118
|
via: vector.control.via,
|
|
875
1119
|
transportVariant: clients.transportVariant === true,
|
|
876
1120
|
};
|
|
877
|
-
const unavailable =
|
|
1121
|
+
const unavailable = scoped.missing;
|
|
878
1122
|
if (unavailable)
|
|
879
1123
|
return {
|
|
880
1124
|
...base,
|
|
@@ -891,8 +1135,27 @@ async function executeRow(job, opts, clients, set, slot, index) {
|
|
|
891
1135
|
live: 'not-run',
|
|
892
1136
|
appliesTo: vector.appliesTo ?? [],
|
|
893
1137
|
};
|
|
894
|
-
if (inactive(vector, target, opts.capabilities))
|
|
895
|
-
|
|
1138
|
+
if (inactive(vector, target, opts.capabilities)) {
|
|
1139
|
+
const missing = (vector.requires ?? []).find((cap) => !supported([cap], opts.capabilities));
|
|
1140
|
+
return {
|
|
1141
|
+
...base,
|
|
1142
|
+
control: 'skip',
|
|
1143
|
+
guarded: 'skip',
|
|
1144
|
+
live: 'skip',
|
|
1145
|
+
reason: missing ? `not-supported:${missing}` : 'not-applicable',
|
|
1146
|
+
};
|
|
1147
|
+
}
|
|
1148
|
+
if (
|
|
1149
|
+
!supported(['browserSession'], opts.capabilities) &&
|
|
1150
|
+
browserContextStep(vector, opts.capabilities)
|
|
1151
|
+
)
|
|
1152
|
+
return {
|
|
1153
|
+
...base,
|
|
1154
|
+
control: 'skip',
|
|
1155
|
+
guarded: 'skip',
|
|
1156
|
+
live: 'skip',
|
|
1157
|
+
reason: 'not-supported:browserSession',
|
|
1158
|
+
};
|
|
896
1159
|
if (needsObserver(vector) && !clients.unguarded)
|
|
897
1160
|
return {
|
|
898
1161
|
...base,
|
|
@@ -954,9 +1217,10 @@ const invalidBlocked = (check) =>
|
|
|
954
1217
|
function validateBlocked(vectors) {
|
|
955
1218
|
for (const vector of vectors)
|
|
956
1219
|
if (
|
|
957
|
-
[
|
|
958
|
-
|
|
959
|
-
|
|
1220
|
+
[
|
|
1221
|
+
vector.liveCheck,
|
|
1222
|
+
...Object.values(vector.observe ?? {}).flatMap((o) => [o.check, o.eitherWhen]),
|
|
1223
|
+
].some(invalidBlocked)
|
|
960
1224
|
)
|
|
961
1225
|
throw new TypeError(`${vector.id}: blocked needs exactly one url or nonempty anyOf`);
|
|
962
1226
|
}
|
|
@@ -1060,10 +1324,22 @@ export function summary({ set, results, guard = 'reference', sha = 'unknown' })
|
|
|
1060
1324
|
const observerSkipped = results.filter(
|
|
1061
1325
|
(row) => row.reason === 'not-supported:unguarded-observer'
|
|
1062
1326
|
).length;
|
|
1327
|
+
const skips = {};
|
|
1328
|
+
for (const row of results)
|
|
1329
|
+
if (row.control === 'skip')
|
|
1330
|
+
skips[row.reason ?? 'unknown'] = (skips[row.reason ?? 'unknown'] ?? 0) + 1;
|
|
1331
|
+
const skipped = Object.values(skips).reduce((sum, count) => sum + count, 0);
|
|
1332
|
+
const line = observerSkipped ? value.line.replace('GUARD-PASS', 'GUARD-FAIL') : value.line;
|
|
1063
1333
|
return {
|
|
1064
1334
|
...value,
|
|
1065
1335
|
pass: value.pass && observerSkipped === 0,
|
|
1066
|
-
line:
|
|
1336
|
+
line: skipped
|
|
1337
|
+
? `${line} skips ${skipped} (${Object.entries(skips)
|
|
1338
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
1339
|
+
.map(([reason, count]) => `${reason}=${count}`)
|
|
1340
|
+
.join(', ')})`
|
|
1341
|
+
: line,
|
|
1342
|
+
skips,
|
|
1067
1343
|
observerSkipped,
|
|
1068
1344
|
counts: Object.fromEntries(
|
|
1069
1345
|
['control', 'guarded', 'live'].map((side) => [
|