@ai-ecoverse/slicc-browsers 1.7.0 → 1.7.2
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 -3
- package/fixture-server.mjs +8 -5
- package/guard-runner.mjs +49 -4
- package/guard-vectors.json +847 -323
- package/package.json +3 -3
- package/schema/guard-vectors.schema.json +19 -56
package/README.md
CHANGED
|
@@ -270,7 +270,13 @@ of supported names. `capabilities.scope` selects owner-specific rows.
|
|
|
270
270
|
Map `ordinary`, `ordinary-same-site`, `seven`, `vault`, `vault-x`, `apex`, `own`
|
|
271
271
|
and `client` to origins. Optionally map `plain`, `cross` and `log`; their defaults
|
|
272
272
|
are `ordinary`, `client` and `ordinary`. Origins must preserve the site
|
|
273
|
-
relationships of the data.
|
|
273
|
+
relationships of the data. Explicitly map an unavailable target to `null`: every
|
|
274
|
+
row needing that origin is reported as `skip` for control, guarded and live, with
|
|
275
|
+
reason `not-applicable:no-<target>-origin`; no substitute origin is used and the
|
|
276
|
+
row never passes. A guard whose whole site is protected, such as the extension
|
|
277
|
+
where every `sliccy.ai` host is protected, passes `ordinary-same-site: null`.
|
|
278
|
+
This also applies to origins referenced in setup, triggers, observations and
|
|
279
|
+
liveness checks, not only the row target. Tags and cookie names are unique per leg. `variant`
|
|
274
280
|
selects one variant or an array; omission selects all. `parallel` defaults to 1.
|
|
275
281
|
`log(row)` receives completed rows. A variant supplies the wait before an
|
|
276
282
|
explicit `Target.detachFromTarget` step and for a `settle.variant` step.
|
|
@@ -301,7 +307,13 @@ observations: a missing or failing effect fails the control. Guarded liveness
|
|
|
301
307
|
requires the exact target URL in the fixture log or a target-origin `/live`
|
|
302
308
|
heartbeat; alternatively a pre-action `/live?tag=...&step=pre&to=...` request and
|
|
303
309
|
an exact-URL block/error from the client or unguarded frame tree. An error for a
|
|
304
|
-
different URL is not evidence.
|
|
310
|
+
different URL is not evidence. `blocked` accepts either `{ url }` for one exact URL
|
|
311
|
+
or `{ anyOf: [url, ...] }` for a nonempty list of exact URLs; each URL can be a
|
|
312
|
+
string or `{ hops, to, index? }`. Both forms allow `session` and `frame`. Redirect
|
|
313
|
+
vectors list every nested hop URL and the final destination because Chromium
|
|
314
|
+
reports the URL intercepted by the guard. A matching block still requires the
|
|
315
|
+
pre-action beacon alongside it; an unrelated error cannot certify the chain.
|
|
316
|
+
A declared setup refusal can supply liveness only
|
|
305
317
|
when its `refusal.match` matched. Transport connectivity and command completion
|
|
306
318
|
alone never prove liveness. Cookie and storage rows seed a canary outside the
|
|
307
319
|
guard and read it back outside the guard; refused writes require unchanged
|
|
@@ -319,7 +331,12 @@ fixture log. A setup beacon on the same origin and tag cannot settle an unfinish
|
|
|
319
331
|
redirect chain. This does not change the observations used to judge either leg.
|
|
320
332
|
|
|
321
333
|
The separately exported Node module `@ai-ecoverse/slicc-browsers/fixture` exposes
|
|
322
|
-
`fixtureServer({ host, port })`, defaulting to a random loopback port.
|
|
334
|
+
`fixtureServer({ host, port, tls })`, defaulting to a random loopback port.
|
|
335
|
+
Pass `tls: { key, cert }` to serve HTTPS using Node TLS key and certificate
|
|
336
|
+
values. The returned origin, request logs, redirect URLs and document heartbeats
|
|
337
|
+
use `https://<Host>` with TLS, so `served`, `hops` and `liveCheck` compare against
|
|
338
|
+
the mapped HTTPS origins, including an extension client at `https://*.sliccy.ai`.
|
|
339
|
+
Without `tls`, the server and logged URLs remain HTTP. It returns
|
|
323
340
|
`origin`, `port`, `log()`, `reset()` and `close()`. Log entries contain `url`,
|
|
324
341
|
`method`, `headers` and `time`. Mirror that API when using another fixture.
|
|
325
342
|
The server serves document, frame, redirect, CSP, authentication, data, image,
|
package/fixture-server.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createServer } from 'node:http';
|
|
2
|
+
import { createServer as createSecureServer } from 'node:https';
|
|
2
3
|
|
|
3
4
|
const tall = Buffer.from(
|
|
4
5
|
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==',
|
|
@@ -75,10 +76,11 @@ function route(req, url, send, html) {
|
|
|
75
76
|
return send(404, 'missing', 'text/plain');
|
|
76
77
|
}
|
|
77
78
|
|
|
78
|
-
export async function fixtureServer({ host = '127.0.0.1', port = 0 } = {}) {
|
|
79
|
+
export async function fixtureServer({ host = '127.0.0.1', port = 0, tls } = {}) {
|
|
79
80
|
let entries = [];
|
|
80
|
-
const
|
|
81
|
-
|
|
81
|
+
const scheme = tls ? 'https' : 'http';
|
|
82
|
+
const handler = (req, res) => {
|
|
83
|
+
const url = new URL(req.url, `${scheme}://${req.headers.host}`);
|
|
82
84
|
entries.push({
|
|
83
85
|
url: url.href,
|
|
84
86
|
method: req.method,
|
|
@@ -104,7 +106,8 @@ export async function fixtureServer({ host = '127.0.0.1', port = 0 } = {}) {
|
|
|
104
106
|
headers
|
|
105
107
|
);
|
|
106
108
|
route(req, url, send, html);
|
|
107
|
-
}
|
|
109
|
+
};
|
|
110
|
+
const server = tls ? createSecureServer(tls, handler) : createServer(handler);
|
|
108
111
|
await new Promise((resolve, reject) => {
|
|
109
112
|
server.once('error', reject);
|
|
110
113
|
server.listen(port, host, resolve);
|
|
@@ -112,7 +115,7 @@ export async function fixtureServer({ host = '127.0.0.1', port = 0 } = {}) {
|
|
|
112
115
|
const address = server.address();
|
|
113
116
|
return {
|
|
114
117
|
port: address.port,
|
|
115
|
-
origin:
|
|
118
|
+
origin: `${scheme}://${host}:${address.port}`,
|
|
116
119
|
log: () => structuredClone(entries),
|
|
117
120
|
reset: () => {
|
|
118
121
|
entries = [];
|
package/guard-runner.mjs
CHANGED
|
@@ -245,7 +245,7 @@ class Execution {
|
|
|
245
245
|
}
|
|
246
246
|
|
|
247
247
|
async blocked(spec, observer) {
|
|
248
|
-
const
|
|
248
|
+
const urls = (spec.anyOf ?? [spec.url]).map(address);
|
|
249
249
|
const events = this.client.events.slice(this.eventStart);
|
|
250
250
|
if (observer === 'client') {
|
|
251
251
|
const requests = new Map(
|
|
@@ -259,10 +259,10 @@ class Execution {
|
|
|
259
259
|
(!spec.frame ||
|
|
260
260
|
event.params.frame.parentId ||
|
|
261
261
|
event.sessionId === this.sessions.child) &&
|
|
262
|
-
event.params.frame.unreachableUrl
|
|
262
|
+
urls.includes(event.params.frame.unreachableUrl) &&
|
|
263
263
|
event.params.frame.url.startsWith('chrome-error://')) ||
|
|
264
264
|
(event.method === 'Network.loadingFailed' &&
|
|
265
|
-
requests.get(event.params.requestId)
|
|
265
|
+
urls.includes(requests.get(event.params.requestId)) &&
|
|
266
266
|
(event.params.blockedReason || event.params.errorText === 'net::ERR_BLOCKED_BY_CLIENT'))
|
|
267
267
|
);
|
|
268
268
|
}
|
|
@@ -279,7 +279,8 @@ class Execution {
|
|
|
279
279
|
const visit = (tree) =>
|
|
280
280
|
Boolean(
|
|
281
281
|
tree &&
|
|
282
|
-
((tree.frame.unreachableUrl
|
|
282
|
+
((urls.includes(tree.frame.unreachableUrl) &&
|
|
283
|
+
tree.frame.url.startsWith('chrome-error://')) ||
|
|
283
284
|
(tree.childFrames ?? []).some(visit))
|
|
284
285
|
);
|
|
285
286
|
return (
|
|
@@ -624,6 +625,7 @@ class Execution {
|
|
|
624
625
|
);
|
|
625
626
|
if (c.refused || c.reached) return this.callCheck(c);
|
|
626
627
|
if (c.detached) {
|
|
628
|
+
const events = this.client.events.slice(this.eventStart, this.cleanupEventStart);
|
|
627
629
|
const session = this.sessions[c.detached];
|
|
628
630
|
return (
|
|
629
631
|
Boolean(session) &&
|
|
@@ -832,6 +834,18 @@ const inactive = (vector, target, capabilities) =>
|
|
|
832
834
|
!supported(vector.requires, capabilities) ||
|
|
833
835
|
(vector.scope !== 'any' && vector.scope !== capabilities.scope);
|
|
834
836
|
|
|
837
|
+
function unavailableOrigin(vector, target, targets = {}) {
|
|
838
|
+
const defaults = { plain: 'ordinary', cross: 'client', log: 'ordinary', own: 'client' };
|
|
839
|
+
const references = [target, 'plain', vector.control.origin];
|
|
840
|
+
for (const match of q([vector.setup, vector.trigger, vector.observe, vector.liveCheck]).matchAll(
|
|
841
|
+
/\{([A-Za-z][A-Za-z-]*)\}/g
|
|
842
|
+
))
|
|
843
|
+
references.push(match[1] === 'beacon' || match[1] === 'probe' ? 'log' : match[1]);
|
|
844
|
+
return references
|
|
845
|
+
.map((name) => (targets[name] === undefined ? (defaults[name] ?? name) : name))
|
|
846
|
+
.find((name) => targets[name] === null);
|
|
847
|
+
}
|
|
848
|
+
|
|
835
849
|
function variables(targets, target, side, slot, index) {
|
|
836
850
|
const tag = `guard-${Date.now()}-${slot}-${index}-${side}-${Math.random().toString(36).slice(2)}`;
|
|
837
851
|
const vars = {
|
|
@@ -860,6 +874,15 @@ async function executeRow(job, opts, clients, set, slot, index) {
|
|
|
860
874
|
via: vector.control.via,
|
|
861
875
|
transportVariant: clients.transportVariant === true,
|
|
862
876
|
};
|
|
877
|
+
const unavailable = unavailableOrigin(vector, target, opts.targets);
|
|
878
|
+
if (unavailable)
|
|
879
|
+
return {
|
|
880
|
+
...base,
|
|
881
|
+
control: 'skip',
|
|
882
|
+
guarded: 'skip',
|
|
883
|
+
live: 'skip',
|
|
884
|
+
reason: `not-applicable:no-${unavailable}-origin`,
|
|
885
|
+
};
|
|
863
886
|
if (vector.kind === 'text')
|
|
864
887
|
return {
|
|
865
888
|
...base,
|
|
@@ -917,6 +940,27 @@ async function executeRow(job, opts, clients, set, slot, index) {
|
|
|
917
940
|
return row;
|
|
918
941
|
}
|
|
919
942
|
|
|
943
|
+
const invalidBlocked = (check) =>
|
|
944
|
+
Boolean(
|
|
945
|
+
check?.blocked &&
|
|
946
|
+
((check.blocked.url === undefined) === (check.blocked.anyOf === undefined) ||
|
|
947
|
+
(check.blocked.anyOf !== undefined &&
|
|
948
|
+
(!Array.isArray(check.blocked.anyOf) || check.blocked.anyOf.length === 0)))
|
|
949
|
+
) ||
|
|
950
|
+
[...(check?.all ?? []), ...(check?.any ?? []), ...(check?.not ? [check.not] : [])].some(
|
|
951
|
+
invalidBlocked
|
|
952
|
+
);
|
|
953
|
+
|
|
954
|
+
function validateBlocked(vectors) {
|
|
955
|
+
for (const vector of vectors)
|
|
956
|
+
if (
|
|
957
|
+
[vector.liveCheck, ...Object.values(vector.observe ?? {}).map((o) => o.check)].some(
|
|
958
|
+
invalidBlocked
|
|
959
|
+
)
|
|
960
|
+
)
|
|
961
|
+
throw new TypeError(`${vector.id}: blocked needs exactly one url or nonempty anyOf`);
|
|
962
|
+
}
|
|
963
|
+
|
|
920
964
|
function validate(set, opts) {
|
|
921
965
|
if (!opts.fixture?.log) throw new TypeError('fixture.log is required');
|
|
922
966
|
for (const vector of set.vectors)
|
|
@@ -933,6 +977,7 @@ function validate(set, opts) {
|
|
|
933
977
|
for (const vector of set.vectors)
|
|
934
978
|
if (vector.kind === 'cdp' && invalidLive(vector.liveCheck))
|
|
935
979
|
throw new TypeError(`${vector.id}: liveCheck needs guarded attempt evidence`);
|
|
980
|
+
validateBlocked(set.vectors);
|
|
936
981
|
const variants = Array.isArray(opts.variant) ? opts.variant : [opts.variant];
|
|
937
982
|
for (const name of variants)
|
|
938
983
|
if (!(name in set.variants)) throw new TypeError(`Unknown variant: ${name}`);
|