@ai-ecoverse/slicc-browsers 1.10.0 → 1.10.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 +17 -7
- package/fixture-server.mjs +13 -13
- package/guard-driver.mjs +23 -2
- package/guard-runner.mjs +33 -21
- package/guard-vectors.json +3 -4
- package/package.json +3 -3
- package/protected.mjs +1 -7
- package/schema/guard-vectors.schema.json +2 -6
- package/unparseable-url.mjs +44 -12
package/README.md
CHANGED
|
@@ -182,7 +182,7 @@ The `C9` foreign extension isolated-world family requires `extensionWorld`: a br
|
|
|
182
182
|
|
|
183
183
|
The integration launcher loads shared-web 1.25.2’s harness extension exactly once through `Extensions.loadUnpacked` (`ankmbodmgimjemnojmeddfcdlbocolob`), whose content script runs on HTTP(S) pages. shared-web 1.25.2 also loads it by default in its own harness. A transport unable to host a foreign content-script extension omits `extensionWorld` and reports `not-supported:extensionWorld`; an advertised capability with no observed world fails, rather than silently skipping.
|
|
184
184
|
|
|
185
|
-
`C8.unparseable` probes both raw and canonical space/U+200D/invalid-punycode URLs through creation and navigation. Every guarded destination needs a matched refusal. Its unguarded controls record `document` or `native-rejection` outcomes, including Chrome builds that reject raw U+200D instead of committing it. Both raw and encoded-space forms must commit real fixture documents, because that is the proven cookie leak. An independent observer loads the encoded-space document and confirms it shares a uniquely seeded `.sliccy.ai` cookie; the guarded client must neither list nor attach to it, read its cookie, query that cookie by URL, nor cause a cookie-bearing destination request. The guard does not erase the observer’s private browser state. The row
|
|
185
|
+
`C8.unparseable` probes both raw and canonical space/U+200D/invalid-punycode URLs through creation and navigation. Every guarded destination needs a matched refusal. Its unguarded controls record `document` or `native-rejection` outcomes, including Chrome builds that reject raw U+200D instead of committing it. Both raw and encoded-space forms must commit real fixture documents, because that is the proven cookie leak. An independent observer loads the encoded-space document and confirms it shares a uniquely seeded `.sliccy.ai` cookie; the guarded client must neither list nor attach to it, read its cookie, query that cookie by URL, nor cause a cookie-bearing destination request. The guard does not erase the observer’s private browser state. The row runs on both browser-session and page-session-only transports, uses the default context, and cleans up its own targets and cookie. Cookie seeding always uses the unguarded observer’s browser session, never the guarded client. Only guarded browser-session target listing requires `browserSession`; without it, `listed` records `not-supported` explicitly in probe and observation evidence, and the result lists that field in `notSupported`. Creation, navigation, attachment and `Network.getCookies` with `urls` still require matched refusals; filtering cookies without refusing fails `cookieRefused`. Fixture URLs omit the colon when the port is empty, and reported URLs are compared after canonicalisation, including Chrome’s WHATWG-unparseable encoded-space hosts. Launchers must resolve the raw/canonical space, U+200D/punycode and invalid-punycode `.sliccy.ai` fixture hosts to the fixture server, including for default-port HTTP or HTTPS origins. The HTTP fixture accepts Chrome’s encoded-space Host header without reparsing it as a WHATWG origin.
|
|
186
186
|
|
|
187
187
|
`C8.chrome-search` requests `chrome-search://most-visited/` through `Page.navigate` and requires an explicit guard refusal. Its unguarded control requires a CDP response, not a successful document commit: Chrome can answer with `errorText` when that internal data source is unavailable outside an Instant process. Browser-side load failure is not a guard refusal, and merely hiding the target must fail this row.
|
|
188
188
|
|
|
@@ -206,8 +206,8 @@ A client may register `Fetch.enable` patterns naming a protected origin in eithe
|
|
|
206
206
|
- `scope`: `any`, or the one guard the vector is for;
|
|
207
207
|
- `setup` and `trigger`, both lists of steps;
|
|
208
208
|
- `targets`: the protected or reachability targets it runs against;
|
|
209
|
-
- `requires?`: the capabilities
|
|
210
|
-
- `observe?`: each observed name, as `{ "means", "dependsOn"?, "requires"?, "eitherWhen"? }`. `dependsOn` lists the refusals the observation depends on (see Refusals). `eitherWhen` is a check: when it holds on the guarded leg, that observation's expectation becomes `"either"` there (see Design-dependent outcomes);
|
|
209
|
+
- `requires?`: the capabilities needed for the whole vector, including its steps' `requires`. Observation-only capabilities may be omitted here to keep independent legs runnable. A runner whose transport lacks one reports the vector as `skip` with the reason `not-supported:<capability>`. A row with a `page-or-browser` cookie step that would carry a `browserContextId` reports `not-supported:browserSession` without a browser session, because that parameter is only allowed on the browser target;
|
|
210
|
+
- `observe?`: each observed name, as `{ "means", "dependsOn"?, "requires"?, "eitherWhen"? }`. `requires` gates only that observation when it is not also a whole-vector requirement: unavailable evidence is recorded as `"not-supported"`, excluded from that field's expectations, and reported in the row's `notSupported` list. `dependsOn` lists the refusals the observation depends on (see Refusals). `eitherWhen` is a check: when it holds on the guarded leg, that observation's expectation becomes `"either"` there (see Design-dependent outcomes);
|
|
211
211
|
- `live`: the liveness check;
|
|
212
212
|
- `control` (`origin: "ordinary"`, `via`, `expected`) and `expected`: an object of names and booleans, or text;
|
|
213
213
|
- `skip?` (`target`, `reason`): `reason` starts with a code, as in `not-applicable: …`.
|
|
@@ -247,7 +247,7 @@ Popups are observed by `Target.targetCreated` with the opener's `openerId`, or b
|
|
|
247
247
|
|
|
248
248
|
`{ "extensionWorld": "hidden" | "select" | "debugger" | "drivable", "as": "world", "requires": ["extensionWorld"] }` runs the C9 compound probe on the current ordinary page. The runner attaches an independent observer to discover actual context ids, unique ids and names, and sends probes through the client session. Discovery matches the expected fixture extension’s exact origin, never the first extension world: `run()`’s optional `extensionWorldId` defaults to shared-web’s harness id `ankmbodmgimjemnojmeddfcdlbocolob`. Multiple unrelated content-script worlds are ignored for fixture selection, while guards must still protect foreign worlds. A custom fixture must provide the same content-script marker contract. Object handles are minted and validated on the unguarded control session, then replayed as guesses on the guarded leg; Chrome object handles and DOM node ids are session-local, so observer handles must never be mistaken for client handles. Numeric ids from the control are also replayed alongside fresh observer-discovered selectors, but numeric ids are local to a tab: a control id may name the next tab’s exposed default context, which must work and return no extension marker. The same-tab hidden world’s actual numeric id and every extension unique-id guess still require a matched refusal. Unknown numeric ids and stale object handles (`not exposed`) need explicit refusals rather than Chrome’s invalid-id errors. `guessesRefused` aggregates only required refusals; `guessesReached` must be false on the guarded leg, and `exposedContextsWork` must remain true. Its result contains boolean evidence consumed by ordinary `result` checks. The unguarded leg validates those selectors against Chrome itself before guarded evidence can pass.
|
|
249
249
|
|
|
250
|
-
`{ "unparseable": true, "as": "unparseable"
|
|
250
|
+
`{ "unparseable": true, "as": "unparseable" }` runs the C8 malformed-URL compound probe on the current ordinary page: independent browser ground truth, raw/canonical destination commands, discovery/attach and cookie checks. Boolean evidence is consumed by ordinary `result` checks, with native Chrome outcomes retained in its diagnostics.
|
|
251
251
|
|
|
252
252
|
**The control rule.** Every vector has a control on an ordinary origin: the same setup and trigger against an unprotected origin must succeed. A missing or failing control fails the id. With `via: "guard"`, the control runs through the guard. With `via: "unguarded"`, the runner runs setup and trigger through its own CDP against the ordinary origin and against the protected stand-in, and both must show the effect. The runner reports the control's transport as `via`.
|
|
253
253
|
|
|
@@ -281,8 +281,10 @@ It exports:
|
|
|
281
281
|
- `refusals(vector)`: the vector's refusal names;
|
|
282
282
|
- `outcome`, `call(client, step, sessionId)` and `isRefusal(outcome, step)`;
|
|
283
283
|
- `instrument(client, sessionId, log, tag)`, `load(client, sessionId, url)` and `popup(client, openerId)`;
|
|
284
|
-
- `judge(vector, side, observed)`: `{ pass, refused, skipped, mismatches }`, where `refused` lists the leg's refusals and `skipped` the observations that depend on them;
|
|
284
|
+
- `judge(vector, side, observed)`: `{ pass, refused, skipped, mismatches }`, where `refused` lists the leg's refusals and `skipped` the observations that depend on them or explicitly record unavailable observation capabilities;
|
|
285
285
|
- `result(vector, run)`: a report row from `{ target, variant, transport, via?, capabilities?, control, guarded, live }`. `capabilities` is the transport's; without it, the transport has them all;
|
|
286
|
+
- `parseURL(value, base?)`: a `URL`, or `null` for anything `new URL` rejects or a non-string; every parse of a fixture log entry or a Chrome-reported URL in the runner, the fixture and `protected.mjs` goes through it, so an unparseable entry never throws;
|
|
287
|
+
- `owned(entries, tag)`: the fixture log entries whose raw URL contains `tag`, the row's attribution through its `n=`, `tag=` or `to=` query;
|
|
286
288
|
- `summary({ guard, sha, set, results })`: `set` is the parsed `guard-vectors.json`. It returns `{ set, schemaVersion, guard, sha, variants, targets, transports, results, skipped, notRun, missing, pass, line }`. `missing` lists each `{ id, target, variant, transport }` without a row, for every vector whose `scope` is `any` or the guard, every target it lists, every variant in `variants` or `results`, and every transport in `results`; `pass` needs it empty, where `line` is `GUARD-PASS <guard>@<sha> (vectors @ai-ecoverse/slicc-browsers@X.Y.Z)` or `GUARD-FAIL …`.
|
|
287
289
|
|
|
288
290
|
**`npm run validate`** checks that:
|
|
@@ -294,7 +296,7 @@ It exports:
|
|
|
294
296
|
- a skip names one of the vector's targets;
|
|
295
297
|
- `observe` defines every expected name;
|
|
296
298
|
- in a vector with a refusable step, every observation, except an `as` of a step before the first refusable one, has `dependsOn`, and each name in it is a refusable step's refusal name;
|
|
297
|
-
- every `Target.setAutoAttach`, `children`, child-session, browser-session and browser-context step has the matching `requires`, and a vector's `requires`
|
|
299
|
+
- every `Target.setAutoAttach`, `children`, child-session, browser-session and browser-context step has the matching `requires`, and a vector's `requires` includes its steps' requirements; observation-only requirements can remain local;
|
|
298
300
|
- every `{name}` is in `placeholders`;
|
|
299
301
|
- no URL uses a port from 9222 to 9229.
|
|
300
302
|
|
|
@@ -362,7 +364,7 @@ With the `browserContexts` capability each leg runs in its own browser context.
|
|
|
362
364
|
|
|
363
365
|
`onStart(row)` receives each row's `id`, `target`, `variant`, `transport` and `via` as the row starts. `run()` returns a promise that also has `inFlight()`, and its result has `inFlight` too. `inFlight()` lists the rows that are running now, each as `{ id, target, variant, leg, phase, step, clients }`, where `step` is the index of the current step in its `phase` (`setup`, `trigger` or `child`), and `clients` lists each client's pending CDP calls as `{ method, session, ageMs }` (`client`, plus `unguarded` when it is a separate client). With `rowTimeoutMs`, a row still running after that many milliseconds fails on its own. Its `errors` hold `row timed out after <ms>ms: <the in-flight view>`, `inFlight` holds the view, and the clients that `clientFactory` made for it are closed. The run then goes on to the next row. It is off by default, and a client passed as `client` is never closed.
|
|
364
366
|
|
|
365
|
-
`log(row)` receives completed rows. Besides `control`, `guarded`, `live`, `why` and `errors`, a row may carry `ran` (steps that ran on one leg only) and `notes` (settles skipped because their registration was refused). Both are informational: they explain a row's verdict, and adopters (bios, slicc-cli, slicc-cdp) must not count them as failures, or as passes. The verdict is `control`, `guarded` and `live`. A variant supplies the wait before an
|
|
367
|
+
`log(row)` receives completed rows. Besides `control`, `guarded`, `live`, `why` and `errors`, a row may carry `notSupported` (unavailable observation fields), `ran` (steps that ran on one leg only) and `notes` (settles skipped because their registration was refused). Both are informational: they explain a row's verdict, and adopters (bios, slicc-cli, slicc-cdp) must not count them as failures, or as passes. The verdict is `control`, `guarded` and `live`. A variant supplies the wait before an
|
|
366
368
|
explicit `Target.detachFromTarget` step and for a `settle.variant` step.
|
|
367
369
|
|
|
368
370
|
Every runnable vector has a declarative `liveCheck`; every observation has a
|
|
@@ -437,6 +439,14 @@ redirect chains it also requires the exact final destination request in the
|
|
|
437
439
|
fixture log. A setup beacon on the same origin and tag cannot settle an unfinished
|
|
438
440
|
redirect chain. This does not change the observations used to judge either leg.
|
|
439
441
|
|
|
442
|
+
The fixture log is shared by every row in a run, and `C8.unparseable` leaves entries such
|
|
443
|
+
as `http://exa%20mple.sliccy.ai:<port>/doc?n=<tag>-create-0` that `new URL` rejects. A
|
|
444
|
+
row reads only the entries that carry its own tag; a tagged `liveCheck`, `served`,
|
|
445
|
+
`on` or `beacon` check never sees another row's entries, and an entry that does not
|
|
446
|
+
parse is matched only by its raw or canonical string, never parsed into a throw.
|
|
447
|
+
`hops` checks compare exact URLs. `"tagged": false` reads every entry and skips the ones
|
|
448
|
+
that don't parse.
|
|
449
|
+
|
|
440
450
|
The separately exported Node module `@ai-ecoverse/slicc-browsers/fixture` exposes
|
|
441
451
|
`fixtureServer({ host, port, tls, latency })`, defaulting to a random loopback port. `latency(url)` returns milliseconds to hold that response, for timing tests.
|
|
442
452
|
Pass `tls: { key, cert }` to serve HTTPS using Node TLS key and certificate
|
package/fixture-server.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createServer } from 'node:http';
|
|
2
2
|
import { createServer as createSecureServer } from 'node:https';
|
|
3
|
+
import { parseURL } from './guard-driver.mjs';
|
|
3
4
|
|
|
4
5
|
const tall = Buffer.from(
|
|
5
6
|
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==',
|
|
@@ -78,19 +79,18 @@ function route(req, url, send, html) {
|
|
|
78
79
|
|
|
79
80
|
function requestURL(req, scheme) {
|
|
80
81
|
const origin = `${scheme}://${req.headers.host}`;
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
82
|
+
const parsed = parseURL(req.url, origin);
|
|
83
|
+
if (parsed) return parsed;
|
|
84
|
+
const path =
|
|
85
|
+
parseURL(req.url, `${scheme}://fixture.invalid`) ?? new URL(`${scheme}://fixture.invalid/`);
|
|
86
|
+
return {
|
|
87
|
+
pathname: path.pathname,
|
|
88
|
+
searchParams: path.searchParams,
|
|
89
|
+
href: `${origin}${path.pathname}${path.search}${path.hash}`,
|
|
90
|
+
origin,
|
|
91
|
+
host: req.headers.host,
|
|
92
|
+
hostname: String(req.headers.host ?? '').replace(/:\d+$/, ''),
|
|
93
|
+
};
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
export async function fixtureServer({ host = '127.0.0.1', port = 0, tls, latency } = {}) {
|
package/guard-driver.mjs
CHANGED
|
@@ -139,8 +139,10 @@ export function judge(vector, side, observed) {
|
|
|
139
139
|
const mismatches = observed === true ? [] : [want];
|
|
140
140
|
return { pass: mismatches.length === 0, refused, skipped: [], mismatches };
|
|
141
141
|
}
|
|
142
|
-
const skipped = Object.keys(want).filter(
|
|
143
|
-
(
|
|
142
|
+
const skipped = Object.keys(want).filter(
|
|
143
|
+
(k) =>
|
|
144
|
+
(observed?.[k] === 'not-supported' && vector.observe?.[k]?.requires?.length > 0) ||
|
|
145
|
+
(vector.observe?.[k]?.dependsOn ?? []).some((d) => refused.includes(d))
|
|
144
146
|
);
|
|
145
147
|
const mismatches = Object.entries(want)
|
|
146
148
|
.filter(
|
|
@@ -227,6 +229,13 @@ export function result(vector, run) {
|
|
|
227
229
|
guarded,
|
|
228
230
|
live,
|
|
229
231
|
...(Object.keys(ran).length ? { ran } : {}),
|
|
232
|
+
...(Object.values(run.guarded ?? {}).includes('not-supported')
|
|
233
|
+
? {
|
|
234
|
+
notSupported: Object.keys(run.guarded).filter(
|
|
235
|
+
(name) => run.guarded[name] === 'not-supported'
|
|
236
|
+
),
|
|
237
|
+
}
|
|
238
|
+
: {}),
|
|
230
239
|
...(why.length ? { why: why.join('; ') } : {}),
|
|
231
240
|
};
|
|
232
241
|
}
|
|
@@ -266,3 +275,15 @@ export function summary({ guard, sha, set, results }) {
|
|
|
266
275
|
line: `${pass ? 'GUARD-PASS' : 'GUARD-FAIL'} ${guard}@${sha} (vectors ${name})`,
|
|
267
276
|
};
|
|
268
277
|
}
|
|
278
|
+
|
|
279
|
+
export function parseURL(value, base) {
|
|
280
|
+
if (typeof value !== 'string') return null;
|
|
281
|
+
try {
|
|
282
|
+
return new URL(value, base);
|
|
283
|
+
} catch {
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export const owned = (entries, tag) =>
|
|
289
|
+
(entries ?? []).filter((entry) => typeof entry?.url === 'string' && entry.url.includes(tag));
|
package/guard-runner.mjs
CHANGED
|
@@ -7,11 +7,13 @@ import {
|
|
|
7
7
|
isRefusal,
|
|
8
8
|
loadedAfter,
|
|
9
9
|
outcome,
|
|
10
|
+
owned,
|
|
11
|
+
parseURL,
|
|
10
12
|
summary as report,
|
|
11
13
|
result,
|
|
12
14
|
steps,
|
|
13
15
|
} from './guard-driver.mjs';
|
|
14
|
-
import { unparseableURLs } from './unparseable-url.mjs';
|
|
16
|
+
import { canonicalURL, unparseableURLs } from './unparseable-url.mjs';
|
|
15
17
|
|
|
16
18
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
17
19
|
const namedResultMs = 10000;
|
|
@@ -233,13 +235,13 @@ class Execution {
|
|
|
233
235
|
}
|
|
234
236
|
|
|
235
237
|
async attemptCheck(c) {
|
|
236
|
-
const logs = (await this.options.fixture.log()).slice(this.requestStart);
|
|
238
|
+
const logs = owned((await this.options.fixture.log()).slice(this.requestStart), this.vars.tag);
|
|
237
239
|
if (c.pre) {
|
|
238
240
|
const url = address(c.pre);
|
|
239
241
|
return logs.some((entry) => {
|
|
240
|
-
const request =
|
|
242
|
+
const request = parseURL(entry.url);
|
|
241
243
|
return (
|
|
242
|
-
request
|
|
244
|
+
request?.pathname === '/live' &&
|
|
243
245
|
request.searchParams.get('tag') === this.vars.tag &&
|
|
244
246
|
request.searchParams.get('step') === 'pre' &&
|
|
245
247
|
request.searchParams.get('to') === url
|
|
@@ -247,15 +249,16 @@ class Execution {
|
|
|
247
249
|
});
|
|
248
250
|
}
|
|
249
251
|
const url = address(c.loaded);
|
|
252
|
+
const origin = parseURL(url)?.origin;
|
|
250
253
|
return logs.some((entry) => {
|
|
251
|
-
|
|
254
|
+
if (entry.url === url || canonicalURL(entry.url) === canonicalURL(url)) return true;
|
|
255
|
+
const request = parseURL(entry.url);
|
|
252
256
|
return (
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
request.searchParams.get('step') !== 'pre')
|
|
257
|
+
request?.pathname === '/live' &&
|
|
258
|
+
request.origin === origin &&
|
|
259
|
+
request.searchParams.get('tag') === this.vars.tag &&
|
|
260
|
+
request.searchParams.get('to') === url &&
|
|
261
|
+
request.searchParams.get('step') !== 'pre'
|
|
259
262
|
);
|
|
260
263
|
});
|
|
261
264
|
}
|
|
@@ -664,6 +667,7 @@ class Execution {
|
|
|
664
667
|
origin: this.vars.ordinary,
|
|
665
668
|
tag: this.vars.tag,
|
|
666
669
|
fixture: this.options.fixture,
|
|
670
|
+
browserSession: supported(['browserSession'], this.options.capabilities),
|
|
667
671
|
})
|
|
668
672
|
);
|
|
669
673
|
if (step.extensionWorld)
|
|
@@ -1034,22 +1038,27 @@ class Execution {
|
|
|
1034
1038
|
async requestCheck(c, start = this.requestStart) {
|
|
1035
1039
|
const logs = (await this.options.fixture.log()).slice(start);
|
|
1036
1040
|
if (c.hops)
|
|
1037
|
-
return c.hops.every((_, i) =>
|
|
1038
|
-
|
|
1039
|
-
|
|
1041
|
+
return c.hops.every((_, i) =>
|
|
1042
|
+
logs.some((entry) => entry?.url === hop(c.hops.slice(i), c.to))
|
|
1043
|
+
);
|
|
1044
|
+
const mine =
|
|
1045
|
+
c.tagged === false
|
|
1046
|
+
? logs.filter((entry) => typeof entry?.url === 'string')
|
|
1047
|
+
: owned(logs, this.vars.tag);
|
|
1048
|
+
return mine.some((entry) => {
|
|
1049
|
+
const url = parseURL(entry.url);
|
|
1040
1050
|
if (c.beacon)
|
|
1041
1051
|
return (
|
|
1042
|
-
url
|
|
1052
|
+
url?.pathname === '/beacon' &&
|
|
1043
1053
|
url.searchParams.get('tag') === this.vars.tag &&
|
|
1044
1054
|
url.searchParams.get('origin') === c.beacon
|
|
1045
1055
|
);
|
|
1046
|
-
if (c.
|
|
1047
|
-
if (c.origin && url.origin !== c.origin) return false;
|
|
1056
|
+
if (c.origin && url?.origin !== c.origin) return false;
|
|
1048
1057
|
if (c.served)
|
|
1049
|
-
return c.served.startsWith('http') ? entry.url === c.served : url
|
|
1058
|
+
return c.served.startsWith('http') ? entry.url === c.served : url?.pathname === c.served;
|
|
1050
1059
|
return (
|
|
1051
|
-
(c.on.startsWith('http') ? entry.url === c.on : url
|
|
1052
|
-
entry.headers[c.header.toLowerCase()] === (c.value ?? this.vars.tag)
|
|
1060
|
+
(c.on.startsWith('http') ? entry.url === c.on : url?.pathname === c.on) &&
|
|
1061
|
+
entry.headers?.[c.header.toLowerCase()] === (c.value ?? this.vars.tag)
|
|
1053
1062
|
);
|
|
1054
1063
|
});
|
|
1055
1064
|
}
|
|
@@ -1086,7 +1095,10 @@ class Execution {
|
|
|
1086
1095
|
await sleep(100);
|
|
1087
1096
|
const observed = { ...this.refused };
|
|
1088
1097
|
for (const [name, observation] of Object.entries(this.vector.observe ?? {})) {
|
|
1089
|
-
if (!supported(observation.requires, this.options.capabilities))
|
|
1098
|
+
if (!supported(observation.requires, this.options.capabilities)) {
|
|
1099
|
+
observed[name] = 'not-supported';
|
|
1100
|
+
continue;
|
|
1101
|
+
}
|
|
1090
1102
|
if ((observation.dependsOn ?? []).some((dep) => this.refused[dep])) continue;
|
|
1091
1103
|
observed[name] = await this.observe(name, observation.check);
|
|
1092
1104
|
if (observation.eitherWhen && this.side === 'guarded')
|
package/guard-vectors.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"setVersion": "1.10.
|
|
3
|
+
"setVersion": "1.10.2",
|
|
4
4
|
"sources": [
|
|
5
5
|
{
|
|
6
6
|
"repo": "ai-ecoverse/slicc-cli",
|
|
@@ -133,12 +133,10 @@
|
|
|
133
133
|
"trigger": [
|
|
134
134
|
{
|
|
135
135
|
"unparseable": true,
|
|
136
|
-
"as": "unparseable"
|
|
137
|
-
"requires": ["browserSession"]
|
|
136
|
+
"as": "unparseable"
|
|
138
137
|
}
|
|
139
138
|
],
|
|
140
139
|
"targets": ["ordinary"],
|
|
141
|
-
"requires": ["browserSession"],
|
|
142
140
|
"observe": {
|
|
143
141
|
"live": {
|
|
144
142
|
"means": "unparseable URL protection live",
|
|
@@ -169,6 +167,7 @@
|
|
|
169
167
|
},
|
|
170
168
|
"listed": {
|
|
171
169
|
"means": "unparseable URL protection listed",
|
|
170
|
+
"requires": ["browserSession"],
|
|
172
171
|
"check": {
|
|
173
172
|
"result": "unparseable",
|
|
174
173
|
"path": "result.listed",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-ecoverse/slicc-browsers",
|
|
3
|
-
"version": "1.10.
|
|
3
|
+
"version": "1.10.2",
|
|
4
4
|
"description": "SLICC's shared browser catalog, JSON schemas and discovery conformance corpus",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -67,14 +67,14 @@
|
|
|
67
67
|
"trust:dry": "npx fledgling sync --dry-run -y --skip-publish",
|
|
68
68
|
"release": "semantic-release",
|
|
69
69
|
"test:guard": "npm run test:guard-integration && npm run test:guard-sweep",
|
|
70
|
-
"test:guard-integration": "node --test --test-concurrency=2 test/integration/guard-lifecycle.test.mjs test/integration/guard-fit.test.mjs test/integration/guard-chain.test.mjs test/integration/guard-validation.test.mjs test/integration/guard-liveness.test.mjs test/integration/guard-observations.test.mjs test/integration/guard-child-latency.test.mjs test/integration/guard-adoption.test.mjs test/integration/guard-scoping.test.mjs test/integration/guard-late-pause.test.mjs test/integration/guard-legs.test.mjs test/integration/guard-fetch-enable.test.mjs test/integration/guard-diagnostics.test.mjs test/integration/guard-chrome.test.mjs test/integration/guard-worlds.test.mjs test/integration/guard-world-discovery.test.mjs test/integration/guard-world-guesses.test.mjs test/integration/guard-unparseable.test.mjs test/integration/guard-scales.test.mjs",
|
|
70
|
+
"test:guard-integration": "node --test --test-concurrency=2 test/integration/guard-lifecycle.test.mjs test/integration/guard-fit.test.mjs test/integration/guard-chain.test.mjs test/integration/guard-validation.test.mjs test/integration/guard-liveness.test.mjs test/integration/guard-observations.test.mjs test/integration/guard-child-latency.test.mjs test/integration/guard-adoption.test.mjs test/integration/guard-scoping.test.mjs test/integration/guard-late-pause.test.mjs test/integration/guard-legs.test.mjs test/integration/guard-fetch-enable.test.mjs test/integration/guard-diagnostics.test.mjs test/integration/guard-chrome.test.mjs test/integration/guard-worlds.test.mjs test/integration/guard-world-discovery.test.mjs test/integration/guard-world-guesses.test.mjs test/integration/guard-unparseable.test.mjs test/integration/guard-scales.test.mjs test/integration/guard-shared-log.test.mjs",
|
|
71
71
|
"test:guard-sweep": "node scripts/test-guards.mjs",
|
|
72
72
|
"test:gates": "node --test test/integration/gates.test.mjs",
|
|
73
73
|
"test:guard-target": "node --test test/integration/guard-runner.test.mjs",
|
|
74
74
|
"test:unit:guard": "node --experimental-test-coverage --test --test-coverage-include=guard-runner.mjs --test-coverage-include=fixture-server.mjs --test-coverage-lines=100 test/unit/guard-*.test.mjs"
|
|
75
75
|
},
|
|
76
76
|
"devDependencies": {
|
|
77
|
-
"@ai-ecoverse/slicc-shared-web": "1.
|
|
77
|
+
"@ai-ecoverse/slicc-shared-web": "1.27.0",
|
|
78
78
|
"@biomejs/biome": "2.5.15",
|
|
79
79
|
"ajv": "8.20.0",
|
|
80
80
|
"ajv-formats": "3.0.1",
|
package/protected.mjs
CHANGED
|
@@ -430,7 +430,7 @@
|
|
|
430
430
|
{
|
|
431
431
|
"type": "object",
|
|
432
432
|
"additionalProperties": false,
|
|
433
|
-
"required": ["unparseable", "as"
|
|
433
|
+
"required": ["unparseable", "as"],
|
|
434
434
|
"properties": {
|
|
435
435
|
"unparseable": {
|
|
436
436
|
"const": true
|
|
@@ -439,11 +439,7 @@
|
|
|
439
439
|
"$ref": "#/$defs/name"
|
|
440
440
|
},
|
|
441
441
|
"requires": {
|
|
442
|
-
"$ref": "#/$defs/requires"
|
|
443
|
-
"type": "array",
|
|
444
|
-
"contains": {
|
|
445
|
-
"const": "browserSession"
|
|
446
|
-
}
|
|
442
|
+
"$ref": "#/$defs/requires"
|
|
447
443
|
}
|
|
448
444
|
}
|
|
449
445
|
},
|
package/unparseable-url.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { outcome } from './guard-driver.mjs';
|
|
1
|
+
import { outcome, owned, parseURL } from './guard-driver.mjs';
|
|
2
2
|
|
|
3
3
|
const pause = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
4
4
|
const refused = (out) => !out.ok && /^slicc: refused:/.test(out.error?.message ?? '');
|
|
@@ -10,7 +10,25 @@ const hosts = [
|
|
|
10
10
|
['xn--a.sliccy.ai', 'xn--a.sliccy.ai'],
|
|
11
11
|
];
|
|
12
12
|
|
|
13
|
+
export const rootURL = (root, host, path) =>
|
|
14
|
+
`${root.protocol}//${host}${root.port ? `:${root.port}` : ''}${path}`;
|
|
15
|
+
|
|
16
|
+
export function canonicalURL(value) {
|
|
17
|
+
if (typeof value !== 'string') return value;
|
|
18
|
+
const direct = parseURL(value);
|
|
19
|
+
if (direct) return direct.href;
|
|
20
|
+
const match = /^(https?:)\/\/([^/?#]+)(.*)$/.exec(value);
|
|
21
|
+
if (!match) return value;
|
|
22
|
+
const [, protocol, authority, path] = match;
|
|
23
|
+
const host = authority.replace(/:\d*$/, '').replaceAll(' ', '%20');
|
|
24
|
+
const port = /:(\d*)$/.exec(authority)?.[1];
|
|
25
|
+
const parsed = parseURL(`${protocol}//example.test${port ? `:${port}` : ''}${path}`);
|
|
26
|
+
if (!parsed) return value;
|
|
27
|
+
return rootURL(parsed, host, `${parsed.pathname}${parsed.search}${parsed.hash}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
13
30
|
async function document(reader, targetId, expected, allowNativeRejection = false) {
|
|
31
|
+
expected = canonicalURL(expected);
|
|
14
32
|
const { sessionId } = await reader.send('Target.attachToTarget', { targetId, flatten: true });
|
|
15
33
|
try {
|
|
16
34
|
const deadline = Date.now() + 5000;
|
|
@@ -19,7 +37,7 @@ async function document(reader, targetId, expected, allowNativeRejection = false
|
|
|
19
37
|
do {
|
|
20
38
|
const { targetInfo } = await reader.send('Target.getTargetInfo', { targetId });
|
|
21
39
|
targetURL = targetInfo.url;
|
|
22
|
-
if (targetInfo.url !== expected) {
|
|
40
|
+
if (canonicalURL(targetInfo.url) !== expected) {
|
|
23
41
|
last = { url: targetInfo.url, title: targetInfo.title };
|
|
24
42
|
await pause(25);
|
|
25
43
|
continue;
|
|
@@ -41,7 +59,7 @@ async function document(reader, targetId, expected, allowNativeRejection = false
|
|
|
41
59
|
);
|
|
42
60
|
const value = out.result?.result?.value;
|
|
43
61
|
last = value ?? out;
|
|
44
|
-
if (out.ok && value?.title === 'doc' && value.href === expected) return value;
|
|
62
|
+
if (out.ok && value?.title === 'doc' && canonicalURL(value.href) === expected) return value;
|
|
45
63
|
await pause(25);
|
|
46
64
|
} while (Date.now() < deadline);
|
|
47
65
|
if (allowNativeRejection && ['', 'about:blank', 'about:blank#blocked'].includes(targetURL))
|
|
@@ -75,10 +93,10 @@ async function destinations({ client, reader, root, tag, targets, session, targe
|
|
|
75
93
|
out.error?.code === -32000 && /invalid URL/i.test(out.error?.message ?? '');
|
|
76
94
|
for (const [index, [input, canonical]] of hosts.entries()) {
|
|
77
95
|
const strict = canonical === 'exa%20mple.sliccy.ai';
|
|
78
|
-
const base =
|
|
96
|
+
const base = rootURL(root, canonical, `/doc?n=${tag}`);
|
|
79
97
|
const created = await outcome(
|
|
80
98
|
client.send('Target.createTarget', {
|
|
81
|
-
url:
|
|
99
|
+
url: rootURL(root, input, `/doc?n=${tag}-create-${index}`),
|
|
82
100
|
background: true,
|
|
83
101
|
})
|
|
84
102
|
);
|
|
@@ -95,7 +113,7 @@ async function destinations({ client, reader, root, tag, targets, session, targe
|
|
|
95
113
|
const navigated = await outcome(
|
|
96
114
|
client.send(
|
|
97
115
|
'Page.navigate',
|
|
98
|
-
{ url:
|
|
116
|
+
{ url: rootURL(root, input, `/doc?n=${tag}-navigate-${index}`) },
|
|
99
117
|
session
|
|
100
118
|
)
|
|
101
119
|
);
|
|
@@ -116,7 +134,7 @@ async function destinations({ client, reader, root, tag, targets, session, targe
|
|
|
116
134
|
await outcome(
|
|
117
135
|
client.send(
|
|
118
136
|
'Page.navigate',
|
|
119
|
-
{ url:
|
|
137
|
+
{ url: rootURL(root, root.hostname, `/doc?n=${tag}-ordinary-${index}`) },
|
|
120
138
|
session
|
|
121
139
|
)
|
|
122
140
|
);
|
|
@@ -124,7 +142,16 @@ async function destinations({ client, reader, root, tag, targets, session, targe
|
|
|
124
142
|
return result;
|
|
125
143
|
}
|
|
126
144
|
|
|
127
|
-
export async function unparseableURLs({
|
|
145
|
+
export async function unparseableURLs({
|
|
146
|
+
client,
|
|
147
|
+
reader,
|
|
148
|
+
session,
|
|
149
|
+
targetId,
|
|
150
|
+
origin,
|
|
151
|
+
tag,
|
|
152
|
+
fixture,
|
|
153
|
+
browserSession,
|
|
154
|
+
}) {
|
|
128
155
|
const root = new URL(origin);
|
|
129
156
|
const cookie = `seven-unparseable-${tag}`;
|
|
130
157
|
const value = `secret-${tag}`;
|
|
@@ -134,7 +161,7 @@ export async function unparseableURLs({ client, reader, session, targetId, origi
|
|
|
134
161
|
await reader.send('Storage.setCookies', {
|
|
135
162
|
cookies: [{ name: cookie, value, domain: '.sliccy.ai', path: '/', secure: false }],
|
|
136
163
|
});
|
|
137
|
-
const url =
|
|
164
|
+
const url = rootURL(root, 'exa%20mple.sliccy.ai', `/doc?n=${tag}-observer`);
|
|
138
165
|
const { targetId: probeId } = await reader.send('Target.createTarget', {
|
|
139
166
|
url,
|
|
140
167
|
background: true,
|
|
@@ -143,7 +170,9 @@ export async function unparseableURLs({ client, reader, session, targetId, origi
|
|
|
143
170
|
const ground = await document(reader, probeId, url);
|
|
144
171
|
observer = (await reader.send('Target.attachToTarget', { targetId: probeId, flatten: true }))
|
|
145
172
|
.sessionId;
|
|
146
|
-
const listed =
|
|
173
|
+
const listed = browserSession
|
|
174
|
+
? await outcome(client.send('Target.getTargets', {}))
|
|
175
|
+
: 'not-supported';
|
|
147
176
|
const attached = await outcome(
|
|
148
177
|
client.send('Target.attachToTarget', { targetId: probeId, flatten: true })
|
|
149
178
|
);
|
|
@@ -168,7 +197,10 @@ export async function unparseableURLs({ client, reader, session, targetId, origi
|
|
|
168
197
|
const result = {
|
|
169
198
|
browserVersion: (await reader.send('Browser.getVersion', {})).product,
|
|
170
199
|
live: ground.cookie.includes(`${cookie}=${value}`),
|
|
171
|
-
listed:
|
|
200
|
+
listed:
|
|
201
|
+
listed === 'not-supported'
|
|
202
|
+
? listed
|
|
203
|
+
: listed.ok && listed.result.targetInfos?.some((info) => info.targetId === probeId),
|
|
172
204
|
attached: attached.ok,
|
|
173
205
|
attachRefused: refused(attached),
|
|
174
206
|
readCookie,
|
|
@@ -180,7 +212,7 @@ export async function unparseableURLs({ client, reader, session, targetId, origi
|
|
|
180
212
|
};
|
|
181
213
|
await pause(100);
|
|
182
214
|
const requests = await fixture.log();
|
|
183
|
-
result.cookieRequest = requests.some((entry) =>
|
|
215
|
+
result.cookieRequest = owned(requests, tag).some((entry) =>
|
|
184
216
|
entry.url.includes(`${tag}-create-`) || entry.url.includes(`${tag}-navigate-`)
|
|
185
217
|
? entry.headers.cookie?.includes(`${cookie}=${value}`)
|
|
186
218
|
: false
|