@ai-ecoverse/slicc-browsers 1.11.0 → 1.13.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/README.md +20 -6
- package/corpus.json +91 -28
- package/extension-world.mjs +28 -4
- package/fixture-server.mjs +23 -3
- package/guard-driver.mjs +5 -1
- package/guard-runner.mjs +94 -18
- package/guard-vectors.json +1436 -52
- package/package.json +8 -5
- package/protected-error-page.mjs +29 -13
- package/redirect-fragments.mjs +38 -0
- package/redirect-secret.mjs +466 -0
- package/schema/browsers.schema.json +5 -1
- package/schema/guard-vectors.schema.json +10 -0
package/guard-runner.mjs
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
steps,
|
|
15
15
|
} from './guard-driver.mjs';
|
|
16
16
|
import { protectedErrorPage } from './protected-error-page.mjs';
|
|
17
|
+
import { redirectSecret } from './redirect-secret.mjs';
|
|
17
18
|
import { canonicalURL, unparseableURLs } from './unparseable-url.mjs';
|
|
18
19
|
|
|
19
20
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -114,6 +115,7 @@ class Execution {
|
|
|
114
115
|
this.contexts = [];
|
|
115
116
|
this.pending = [];
|
|
116
117
|
this.errors = [];
|
|
118
|
+
this.preSent = [];
|
|
117
119
|
this.notes = [];
|
|
118
120
|
this.answers = [];
|
|
119
121
|
this.internalSessions = new Set();
|
|
@@ -236,6 +238,17 @@ class Execution {
|
|
|
236
238
|
return read.ok && canary.untouched(state.value, read.result?.result?.value);
|
|
237
239
|
}
|
|
238
240
|
|
|
241
|
+
async awaitPre() {
|
|
242
|
+
const sent = [...new Set(this.preSent)];
|
|
243
|
+
if (!sent.length || !usesPre(this.vector)) return [];
|
|
244
|
+
const missing = async () => {
|
|
245
|
+
const seen = await Promise.all(sent.map((url) => this.attemptCheck({ pre: url })));
|
|
246
|
+
return sent.filter((_, index) => !seen[index]);
|
|
247
|
+
};
|
|
248
|
+
await this.poll(async () => (await missing()).length === 0, 5000);
|
|
249
|
+
return missing();
|
|
250
|
+
}
|
|
251
|
+
|
|
239
252
|
async attemptCheck(c) {
|
|
240
253
|
const logs = owned((await this.options.fixture.log()).slice(this.requestStart), this.vars.tag);
|
|
241
254
|
if (c.pre) {
|
|
@@ -609,10 +622,12 @@ class Execution {
|
|
|
609
622
|
this.errors.push(
|
|
610
623
|
`${step.refusal.name} failed without a refusal: ${String(out.error?.message ?? out.error)}`
|
|
611
624
|
);
|
|
612
|
-
if (
|
|
613
|
-
this.errors.push(
|
|
614
|
-
|
|
615
|
-
)
|
|
625
|
+
if (step.extensionWorld && out.result?.incomplete)
|
|
626
|
+
this.errors.push(`extensionWorld ${step.extensionWorld}: ${out.result.incomplete}`);
|
|
627
|
+
const probe = ['redirectSecret', 'unparseable', 'errorPage', 'extensionWorld'].find(
|
|
628
|
+
(key) => step[key]
|
|
629
|
+
);
|
|
630
|
+
if (probe && !out.ok) this.errors.push(`${probe}: ${out.error?.message ?? 'probe failed'}`);
|
|
616
631
|
if (step.as && !out.result?.pending) this.calls[step.as] = out;
|
|
617
632
|
if (step.cdp) (this.calls.$cdp ??= []).push({ method: step.cdp, ...out });
|
|
618
633
|
}
|
|
@@ -662,6 +677,26 @@ class Execution {
|
|
|
662
677
|
await this.settle(step.settle);
|
|
663
678
|
return;
|
|
664
679
|
}
|
|
680
|
+
if (step.redirectSecret) {
|
|
681
|
+
this.internalSessions.add(session);
|
|
682
|
+
try {
|
|
683
|
+
return await outcome(
|
|
684
|
+
redirectSecret({
|
|
685
|
+
client: this.client,
|
|
686
|
+
reader: this.reader,
|
|
687
|
+
session,
|
|
688
|
+
targetId: this.pageTarget,
|
|
689
|
+
ordinary: this.vars.ordinary,
|
|
690
|
+
tag: this.vars.tag,
|
|
691
|
+
fixture: this.options.fixture,
|
|
692
|
+
mode: step.redirectSecret,
|
|
693
|
+
side: this.side,
|
|
694
|
+
})
|
|
695
|
+
);
|
|
696
|
+
} finally {
|
|
697
|
+
this.internalSessions.delete(session);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
665
700
|
if (step.errorPage)
|
|
666
701
|
return outcome(
|
|
667
702
|
protectedErrorPage({
|
|
@@ -675,6 +710,7 @@ class Execution {
|
|
|
675
710
|
internalSessions: this.internalSessions,
|
|
676
711
|
mode: step.errorPage,
|
|
677
712
|
side: this.side,
|
|
713
|
+
secret: step.secret,
|
|
678
714
|
})
|
|
679
715
|
);
|
|
680
716
|
if (step.unparseable)
|
|
@@ -701,6 +737,7 @@ class Execution {
|
|
|
701
737
|
origin: this.vars.ordinary,
|
|
702
738
|
side: this.side,
|
|
703
739
|
controls: (this.clients.extensionWorldControls ??= {}),
|
|
740
|
+
pauseMs: this.bound(this.side === 'control' ? 5000 : 1500, this.side !== 'control'),
|
|
704
741
|
extensionId: this.options.extensionWorldId,
|
|
705
742
|
})
|
|
706
743
|
);
|
|
@@ -834,16 +871,19 @@ class Execution {
|
|
|
834
871
|
this.pageTarget = tab.targetId;
|
|
835
872
|
return { ok: true, result: tab };
|
|
836
873
|
}
|
|
874
|
+
if (pre && step.page !== 'framed') this.preSent.push(url);
|
|
837
875
|
if (step.page === 'navigate') {
|
|
838
876
|
if (pre) await this.evaluate(pre, session);
|
|
839
877
|
return this.navigate(this.client, session, url);
|
|
840
878
|
}
|
|
841
|
-
if (step.page === 'framed')
|
|
879
|
+
if (step.page === 'framed') {
|
|
880
|
+
if (phase === 'trigger') this.preSent.push(url);
|
|
842
881
|
return this.navigate(
|
|
843
882
|
this.client,
|
|
844
883
|
session,
|
|
845
884
|
`${step.from}/framed?tag=${encodeURIComponent(this.vars.tag)}&url=${encodeURIComponent(url)}&log=${encodeURIComponent(this.vars.log)}`
|
|
846
885
|
);
|
|
886
|
+
}
|
|
847
887
|
let expression = pageExpression(step, url);
|
|
848
888
|
if (pre) expression = `(() => { ${pre} return (${expression}); })()`;
|
|
849
889
|
const promise = this.evaluate(expression, session);
|
|
@@ -900,6 +940,8 @@ class Execution {
|
|
|
900
940
|
const want = (this.side === 'control' ? this.vector.control.expected : this.vector.expected)?.[
|
|
901
941
|
name
|
|
902
942
|
];
|
|
943
|
+
if (check?.result && at(this.calls[check.result], check.path) === 'incomplete')
|
|
944
|
+
return 'incomplete';
|
|
903
945
|
if (want !== true || check?.observer !== 'fixture') return this.check(check);
|
|
904
946
|
let seen = false;
|
|
905
947
|
await this.poll(async () => {
|
|
@@ -1098,6 +1140,42 @@ class Execution {
|
|
|
1098
1140
|
}
|
|
1099
1141
|
}
|
|
1100
1142
|
|
|
1143
|
+
async observeAll(unseen) {
|
|
1144
|
+
const observed = { ...this.refused };
|
|
1145
|
+
const incomplete = [];
|
|
1146
|
+
for (const [name, observation] of Object.entries(this.vector.observe ?? {})) {
|
|
1147
|
+
if (!supported(observation.requires, this.options.capabilities)) {
|
|
1148
|
+
observed[name] = 'not-supported';
|
|
1149
|
+
continue;
|
|
1150
|
+
}
|
|
1151
|
+
if ((observation.dependsOn ?? []).some((dep) => this.refused[dep])) continue;
|
|
1152
|
+
observed[name] = await this.observe(name, observation.check);
|
|
1153
|
+
if (!observation.eitherWhen || this.side !== 'guarded') continue;
|
|
1154
|
+
const holds = await this.either(observation.eitherWhen, observed[name], name, unseen);
|
|
1155
|
+
observed[`${name}.either`] = holds;
|
|
1156
|
+
if (holds === 'incomplete') incomplete.push(name);
|
|
1157
|
+
}
|
|
1158
|
+
return { observed, incomplete };
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
async either(check, value, name, unseen) {
|
|
1162
|
+
const holds = () => this.check(check);
|
|
1163
|
+
if (await holds()) return true;
|
|
1164
|
+
if (unseen.length) return usesPre(check) ? 'incomplete' : false;
|
|
1165
|
+
if (value === this.vector.expected?.[name]) return false;
|
|
1166
|
+
return this.poll(holds, 1500);
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
async liveness(unseen, incomplete) {
|
|
1170
|
+
if (this.side === 'control') return { live: true };
|
|
1171
|
+
const alive = () => this.check(this.vector.liveCheck);
|
|
1172
|
+
let live = await alive();
|
|
1173
|
+
if (!live && !unseen.length) live = await this.poll(alive, 1500);
|
|
1174
|
+
if (!unseen.length || (live && !incomplete.length)) return { live };
|
|
1175
|
+
const detail = incomplete.length ? ` (${incomplete.join(', ')} incomplete)` : '';
|
|
1176
|
+
return { live: false, liveWhy: `pre beacon not observed for ${unseen.join(', ')}${detail}` };
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1101
1179
|
async run() {
|
|
1102
1180
|
this.requestStart = (await this.options.fixture.log()).length;
|
|
1103
1181
|
let pumping;
|
|
@@ -1113,25 +1191,16 @@ class Execution {
|
|
|
1113
1191
|
await this.execute(plan.setup, undefined, 'setup');
|
|
1114
1192
|
await this.execute(plan.trigger, undefined, 'trigger');
|
|
1115
1193
|
await sleep(100);
|
|
1116
|
-
const
|
|
1117
|
-
|
|
1118
|
-
if (!supported(observation.requires, this.options.capabilities)) {
|
|
1119
|
-
observed[name] = 'not-supported';
|
|
1120
|
-
continue;
|
|
1121
|
-
}
|
|
1122
|
-
if ((observation.dependsOn ?? []).some((dep) => this.refused[dep])) continue;
|
|
1123
|
-
observed[name] = await this.observe(name, observation.check);
|
|
1124
|
-
if (observation.eitherWhen && this.side === 'guarded')
|
|
1125
|
-
observed[`${name}.either`] = await this.check(observation.eitherWhen);
|
|
1126
|
-
}
|
|
1194
|
+
const unseen = await this.awaitPre();
|
|
1195
|
+
const { observed, incomplete } = await this.observeAll(unseen);
|
|
1127
1196
|
this.options.debug?.({
|
|
1128
1197
|
events: this.client.events.slice(this.eventStart),
|
|
1129
1198
|
calls: this.calls,
|
|
1130
1199
|
requests: await this.options.fixture.log(),
|
|
1131
1200
|
side: this.side,
|
|
1132
1201
|
});
|
|
1133
|
-
const live
|
|
1134
|
-
return { observed, live, errors: this.errors, notes: this.notes };
|
|
1202
|
+
const { live, liveWhy } = await this.liveness(unseen, incomplete);
|
|
1203
|
+
return { observed, live, liveWhy, errors: this.errors, notes: this.notes };
|
|
1135
1204
|
} finally {
|
|
1136
1205
|
this.stopped = true;
|
|
1137
1206
|
await pumping;
|
|
@@ -1154,6 +1223,12 @@ class Execution {
|
|
|
1154
1223
|
}
|
|
1155
1224
|
}
|
|
1156
1225
|
|
|
1226
|
+
const usesPre = (value) =>
|
|
1227
|
+
Boolean(value && typeof value === 'object') &&
|
|
1228
|
+
(Object.hasOwn(value, 'pre') ||
|
|
1229
|
+
Object.hasOwn(value, 'history') ||
|
|
1230
|
+
Object.values(value).some(usesPre));
|
|
1231
|
+
|
|
1157
1232
|
const observerRequired = (check) => {
|
|
1158
1233
|
if (check?.observer === 'unguarded') return true;
|
|
1159
1234
|
if (check?.all) return check.all.some(observerRequired);
|
|
@@ -1446,6 +1521,7 @@ async function executeRow(job, opts, made, set, slot, index, flights) {
|
|
|
1446
1521
|
control: legs.control.observed,
|
|
1447
1522
|
guarded: legs.guarded.observed,
|
|
1448
1523
|
live: legs.control.live && legs.guarded.live,
|
|
1524
|
+
liveWhy: legs.guarded.liveWhy,
|
|
1449
1525
|
}),
|
|
1450
1526
|
transportVariant: base.transportVariant,
|
|
1451
1527
|
observations: { control: legs.control.observed, guarded: legs.guarded.observed },
|