@ai-ecoverse/slicc-browsers 1.10.1 → 1.10.3

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 CHANGED
@@ -65,7 +65,7 @@ Every feature builds for `wasm32-wasip1`.
65
65
 
66
66
  `guard-driver.mjs` is described under [Guard vectors](#guard-vectors).
67
67
 
68
- `protected.mjs` is the reference matcher for [protected origins](#protected-origins): `import { protectedOrigins } from '@ai-ecoverse/slicc-browsers/protected.mjs'`, then `protectedOrigins(catalog.protectedOrigins, extraOrigins)` returns `{ url(text), target({ url, previous, opener }), cookieDomain(domain), storageKey(key), unparseable(text) }`, each a boolean. `url(text)` uses the load/loaded-URL fail-closed rule; `url(text, { readOnly: true })` preserves the old parse-failure result for read-only keys and value redaction, and `storageKey(key)` uses that mode internally. `unparseable(text)` identifies parse or unwrap failures without changing whether a parsed origin is protected. It needs only the WHATWG `URL`, so it runs in Node and in a browser.
68
+ `protected.mjs` is the reference matcher for [protected origins](#protected-origins): `import { protectedOrigins } from '@ai-ecoverse/slicc-browsers/protected.mjs'`, then `protectedOrigins(catalog.protectedOrigins, extraOrigins)` returns `{ url(text), target({ url, previous, opener }), cookieDomain(domain), storageKey(key), unparseable(text) }`, each a boolean. `url(text)` uses the load/loaded-URL fail-closed rule; `url(text, { readOnly: true })` preserves the old parse-failure result for read-only keys and value redaction, and `storageKey(key)` uses that mode internally. `unparseable(text)` identifies parse or unwrap failures without changing whether a parsed origin is protected. It needs only the WHATWG `URL`, so it runs in Node and in a browser. `protected.mjs` and `seven.mjs` import nothing and re-export nothing, so an extension or service worker can ship either file alone; `npm run validate` (through `scripts/self-contained.mjs`) evaluates each as a module in a fresh `node:vm` context that has only `URL`, with no linker resolution allowed.
69
69
 
70
70
  ## `catalog.json`
71
71
 
@@ -283,6 +283,8 @@ It exports:
283
283
  - `instrument(client, sessionId, log, tag)`, `load(client, sessionId, url)` and `popup(client, openerId)`;
284
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 and the fixture goes through it (`protected.mjs` keeps its own copy, since it imports nothing), 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:
@@ -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
@@ -589,10 +599,10 @@ for case in corpus().cases {
589
599
 
590
600
  Go 1.26, Rust stable with `wasm32-wasip1`, Node ≥ 24.
591
601
  - `npm run corpus` regenerates `corpus.json` from `corpus/`; the pre-commit hook and CI fail when it's stale.
592
- - `npm run validate` checks `catalog.json`, `corpus.json`, `protected.json` and `guard-vectors.json` against the schemas, runs the protected-origin vectors through `protected.mjs`, and checks what a schema can't say: unique ids, bundle ids and names, known Windows roots, rising tiers, compiling patterns, known tokens, tier order, `sevenOpen`, `tabs` (which holds no entry of `sevenTabs`), `six` and `auto` in each `expected.json`, and no port from 9222 to 9229.
602
+ - `npm run validate` checks `catalog.json`, `corpus.json`, `protected.json` and `guard-vectors.json` against the schemas, runs the protected-origin vectors through `protected.mjs`, loads `protected.mjs` and `seven.mjs` with only `URL`, and checks what a schema can't say: unique ids, bundle ids and names, known Windows roots, rising tiers, compiling patterns, known tokens, tier order, `sevenOpen`, `tabs` (which holds no entry of `sevenTabs`), `six` and `auto` in each `expected.json`, and no port from 9222 to 9229.
593
603
  - Every gate prints one verdict line when it passes: `validate: PASS …`, `corpus: PASS <n> cases` (`node scripts/corpus.mjs --check`), `major-guard: PASS v<version> fits <module>`, and `guard-sweep: PASS 14 targets` (`npm run test:guard-sweep`). CI runs each through `scripts/gate.mjs --expect <regex> -- <command>`, which fails when the command exits non-zero or prints no matching line, so a silent or early exit never reads as a pass. `gate.mjs --expect <regex> --in <file>` checks a log instead; the release job uses it on semantic-release's log for the major-guard line. `npm run test:gates` tests the gates, including runs through a symlinked checkout and a symlinked script.
594
604
  - `npm run lint`: the slicc lint tools (Biome, no comments, no unit tests in git, `AGENTS.md`), `gofmt`, `go mod tidy -diff`, `go vet`, golangci-lint, `cargo fmt --check` and clippy.
595
- - `npm test`: `validate`, the Go tests in `test/integration/`, and `cargo test --all-features`.
605
+ - `npm test`: `validate`, `test/integration/self-contained.test.mjs`, the Go tests in `test/integration/`, and `cargo test --all-features`.
596
606
  - `npm run build`: the Go module, the crate (also for `wasm32-wasip1`), and `npm pack --dry-run`.
597
607
 
598
608
  Changing a table in `slicc-cli/internal/browser` means changing `catalog.json` here, releasing, and bumping the dependency; slicc-cli has no tables of its own. A new behaviour gets a corpus case.
@@ -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
- try {
82
- return new URL(req.url, origin);
83
- } catch {
84
- const path = new URL(req.url, `${scheme}://fixture.invalid`);
85
- return {
86
- pathname: path.pathname,
87
- searchParams: path.searchParams,
88
- href: `${origin}${path.pathname}${path.search}${path.hash}`,
89
- origin,
90
- host: req.headers.host,
91
- hostname: req.headers.host.replace(/:\d+$/, ''),
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
@@ -275,3 +275,15 @@ export function summary({ guard, sha, set, results }) {
275
275
  line: `${pass ? 'GUARD-PASS' : 'GUARD-FAIL'} ${guard}@${sha} (vectors ${name})`,
276
276
  };
277
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 = new URL(entry.url);
242
+ const request = parseURL(entry.url);
241
243
  return (
242
- request.pathname === '/live' &&
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
- const request = new URL(entry.url);
254
+ if (entry.url === url || canonicalURL(entry.url) === canonicalURL(url)) return true;
255
+ const request = parseURL(entry.url);
252
256
  return (
253
- entry.url === url ||
254
- (request.pathname === '/live' &&
255
- request.origin === new URL(url).origin &&
256
- request.searchParams.get('tag') === this.vars.tag &&
257
- request.searchParams.get('to') === url &&
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
  }
@@ -1035,22 +1038,27 @@ class Execution {
1035
1038
  async requestCheck(c, start = this.requestStart) {
1036
1039
  const logs = (await this.options.fixture.log()).slice(start);
1037
1040
  if (c.hops)
1038
- return c.hops.every((_, i) => logs.some((entry) => entry.url === hop(c.hops.slice(i), c.to)));
1039
- return logs.some((entry) => {
1040
- const url = new URL(entry.url);
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);
1041
1050
  if (c.beacon)
1042
1051
  return (
1043
- url.pathname === '/beacon' &&
1052
+ url?.pathname === '/beacon' &&
1044
1053
  url.searchParams.get('tag') === this.vars.tag &&
1045
1054
  url.searchParams.get('origin') === c.beacon
1046
1055
  );
1047
- if (c.tagged !== false && !entry.url.includes(this.vars.tag)) return false;
1048
- if (c.origin && url.origin !== c.origin) return false;
1056
+ if (c.origin && url?.origin !== c.origin) return false;
1049
1057
  if (c.served)
1050
- return c.served.startsWith('http') ? entry.url === c.served : url.pathname === c.served;
1058
+ return c.served.startsWith('http') ? entry.url === c.served : url?.pathname === c.served;
1051
1059
  return (
1052
- (c.on.startsWith('http') ? entry.url === c.on : url.pathname === c.on) &&
1053
- 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)
1054
1062
  );
1055
1063
  });
1056
1064
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "setVersion": "1.10.1",
3
+ "setVersion": "1.10.3",
4
4
  "sources": [
5
5
  {
6
6
  "repo": "ai-ecoverse/slicc-cli",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-ecoverse/slicc-browsers",
3
- "version": "1.10.1",
3
+ "version": "1.10.3",
4
4
  "description": "SLICC's shared browser catalog, JSON schemas and discovery conformance corpus",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -60,21 +60,21 @@
60
60
  "validate": "node scripts/validate.mjs",
61
61
  "lint": "slicc-lint && test -z \"$(gofmt -l .)\" && go mod tidy -diff && go vet ./... && golangci-lint run && cargo fmt --check && cargo clippy --all-features --all-targets -- -D warnings",
62
62
  "format": "biome check --write . && gofmt -w . && cargo fmt",
63
- "test": "npm run validate && go test -race -count=1 ./test/integration/... && cargo test --all-features",
63
+ "test": "npm run validate && node --test test/integration/self-contained.test.mjs && go test -race -count=1 ./test/integration/... && cargo test --all-features",
64
64
  "test:unit": "go test -race -cover ./...",
65
65
  "build": "go build ./... && cargo build --all-features && cargo build --target wasm32-wasip1 --all-features && npm pack --dry-run",
66
66
  "trust": "npx fledgling sync -y --skip-publish",
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 test/integration/self-contained.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.25.2",
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
@@ -1,4 +1,5 @@
1
1
  const parse = (text) => {
2
+ if (typeof text !== 'string') return null;
2
3
  try {
3
4
  return new URL(text);
4
5
  } catch {
@@ -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 ?? '');
@@ -14,17 +14,17 @@ export const rootURL = (root, host, path) =>
14
14
  `${root.protocol}//${host}${root.port ? `:${root.port}` : ''}${path}`;
15
15
 
16
16
  export function canonicalURL(value) {
17
- try {
18
- return new URL(value).href;
19
- } catch {
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 = new URL(`${protocol}//example.test${port ? `:${port}` : ''}${path}`);
26
- return rootURL(parsed, host, `${parsed.pathname}${parsed.search}${parsed.hash}`);
27
- }
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
28
  }
29
29
 
30
30
  async function document(reader, targetId, expected, allowNativeRejection = false) {
@@ -212,7 +212,7 @@ export async function unparseableURLs({
212
212
  };
213
213
  await pause(100);
214
214
  const requests = await fixture.log();
215
- result.cookieRequest = requests.some((entry) =>
215
+ result.cookieRequest = owned(requests, tag).some((entry) =>
216
216
  entry.url.includes(`${tag}-create-`) || entry.url.includes(`${tag}-navigate-`)
217
217
  ? entry.headers.cookie?.includes(`${cookie}=${value}`)
218
218
  : false