@ai-ecoverse/slicc-browsers 1.7.6 → 1.7.8

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
@@ -81,7 +81,7 @@ Every feature builds for `wasm32-wasip1`.
81
81
  - `rank`: `tiers`, 1 to 6: `sevenOpen` (an endpoint with a seven tab), `endpoint` (any other endpoint), `chrome` (installed `chromeId`), `chromium` (other installed Chromium browsers), `electron` (installed Electron apps), `six` (an endpoint in SLICC six's profile, with or without a seven tab; see [SLICC six's profiles](#slicc-sixs-profiles)).
82
82
  - `discovery`:
83
83
  - `activePortFile` (`DevToolsActivePort`): its first line is the port (1 to 65535) and its second the browser's websocket path, which starts with `/`. A file without both is ignored. A file is live only when its port answers `/json/version` with a `webSocketDebuggerUrl` whose path is that websocket path; otherwise it's stale and ignored. When the port accepts connections but serves no `/json/version`, the endpoint is listed as `wsOnly`, with `ws://127.0.0.1:<port><path>`;
84
- - `probePorts` (9222 to 9229, probed by `slicc` only);
84
+ - `probePorts` (9222 to 9229);
85
85
  - every endpoint's `cdp` is `<scheme>://<host>:<port>`, with the host in lower case and `localhost` and `::1` written as `127.0.0.1`, whatever form the URL was found in. Endpoints with the same host and port, written that way, are one endpoint. A `ws://` `--cdp` URL that is listed as `wsOnly` keeps its websocket URL as given;
86
86
  - `sourceRank`: when two sources find the same host and port, the lower rank wins, and on a tie the first found;
87
87
  - `names`: an endpoint's name when it isn't from a known browser's user-data dir: `electron` when the user agent contains `electronUserAgent`, else `products[<product>]` for the product before the `/` in `/json/version`'s `Browser`, else that product, else `fallback`;
@@ -131,7 +131,7 @@ A same-site frame shares its parent's process and has no target of its own. Ever
131
131
  ## Guard vectors
132
132
 
133
133
  Every CDP guard runs the vectors in `guard-vectors.json` (schema `schema/guard-vectors.schema.json`): bios, slicc-cdp, slicc-extension and slicc-cli. The sources are listed in `sources`:
134
- - slicc-cli's `protectVectors` at `2d5d981` (`test/integration/testdata/link/protect.js`), with the ids kept;
134
+ - slicc-cli's `protectVectors` at `2d5d981` (`test/integration/testdata/link/protect.js`), with the ids kept, and at `fe33052` for `C3.redirect-popup-stays-out` and `C3.direct-popup-stays-out`: a popup opened straight onto, or through a redirect chain to, a protected origin never reaches the client, not at open and not after the page later navigates itself and auto-attach runs again;
135
135
  - slicc-cdp's `test/integration/guard-vectors.json` at `4907a45`, where its `E1`, `E2`, `E3`, `E5` and `B2` are renamed `E1.onload`, `E2.binding`, `E3.bypass-csp`, `E5.console` and `B2.headers`;
136
136
  - the cert lane's E and B rows;
137
137
  - `C5.continue-request` and `C5.continue-response`, added next to `C5.document`.
@@ -171,7 +171,7 @@ Any `cdp`, `page` or `children` step can take `requires`, the capabilities it ne
171
171
  `hops` routes `url` through `/hop` on each `origin`, with its `code`.
172
172
  - `{ "answer": { "url", "stage"?, "cdp"?, "params"?, "hold"? } }`: answer each paused request or auth challenge for `url` with that call; `hold` leaves it paused. Every other paused request continues unchanged.
173
173
  - `{ "children": steps, "where"? }`: run these steps on each target auto-attached from then on, then resume it. `where` scopes them to the row's own target: `{ "openerId": "page" }` (opened by the row's page) or `{ "urlPrefix": url }`; other attached targets are resumed and otherwise left alone.
174
- - `{ "settle": { "beacon"?, "title"?, "ms"?, "variant"?, "session"? } }`: wait for a beacon from `beacon`, for the page `title`, or for the session to detach. It waits up to `ms` (10000 by default), then another 600 ms. `variant` waits the run's detach variant instead.
174
+ - `{ "settle": { "beacon"?, "served"?, "title"?, "ms"?, "variant"?, "session"? } }`: wait for a beacon from `beacon`, for the fixture to log a request for the URL `served`, for the page `title`, or for the session to detach. It waits up to `ms` (10000 by default), then another 600 ms. `variant` waits the run's detach variant instead.
175
175
  - `{ "wait": ms }`.
176
176
  - `{ "paused": { "url", "ms" } }`: wait until the client has seen the pause that an `answer` rule for `url` matches (`Fetch.requestPaused` at the rule's stage, or `Fetch.authRequired` for a `Fetch.continueWithAuth` rule). It waits up to `ms`. On the control leg, a pause that never comes fails the row with `paused: no pause for <url> within <ms>ms`. On the guarded leg the guard may hide that pause, so the step waits the full `ms` and doesn't fail. The C5 guessed-id vectors and `C5.auth` use it before their second request, so the probe's interception id falls between the two ids the guess spans.
177
177
  - `{ "guess": { "between", "cdp", "params" }, "as" }`: the call for each request id between the ids paused for the two URLs.
@@ -191,7 +191,7 @@ Popups are observed by `Target.targetCreated` with the opener's `openerId`, or b
191
191
 
192
192
  **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`.
193
193
 
194
- **Refusals.** Each refusable step has a refusal name: its `as`, or `refused.<i>`, where `i` is its index in the vector's steps, setup then trigger, with each `children` step's steps right after it, counted from 0 before `on` is applied. When a guard refuses the step and the error matches `refusal.match`, the runner sets that name to `true` in the leg's observations. On that leg, an observation whose `dependsOn` lists the name is skipped; every other observation is checked as usual. A guard that refuses the step on every origin passes in one of two ways: through the guard, with the step refused on both legs, or with an unguarded control and the guard's matched refusal on the guarded leg. With `via: "guard"`, a refusal on one leg only fails that leg. A guard that allows the step must pass its control through the guard.
194
+ **Refusals.** Each refusable step has a refusal name: its `as`, or `refused.<i>`, where `i` is its index in the vector's steps, setup then trigger, with each `children` step's steps right after it, counted from 0 before `on` is applied. When a guard refuses the step and the error matches `refusal.match`, the runner sets that name to `true` in the leg's observations. On that leg, an observation whose `dependsOn` lists the name is skipped; every other observation is checked as usual. A guard that refuses the step on every origin passes in one of two ways: through the guard, with the step refused on both legs, or with an unguarded control and the guard's matched refusal on the guarded leg. With `via: "guard"`, a refusal on one leg only fails that leg. The comparison only counts steps that ran on both legs. A step that ran on one leg only (for example a `children` step on the control's popup, when the guarded leg never gets that popup attached) doesn't fail the row by itself, and the row reports it in `ran`, as `{ "refused.5": "control-only" }` or `"guarded-only"`. Observations that depend on such a step follow their `dependsOn` on each leg. A refusable step that fails without a matching refusal, for example with an internal error or a timeout, is not a refusal: it fails the row with `<name> failed without a refusal: <message>`. A step skipped because an earlier step failed therefore never passes through `ran`, and only a matched refusal skips the observations that depend on it. A guard that allows the step must pass its control through the guard.
195
195
 
196
196
  **Liveness.** A runner checks `live` through its own CDP, never through the guard path: either the protected document loaded (title, marker or URL), or the guard returned its own refusal for a refusable step. A failing `live` fails the id.
197
197
 
@@ -285,7 +285,11 @@ target. In observations and liveness checks, an `any` alternative that reference
285
285
  a `null` origin is dropped and the rest are evaluated; the row skips only when
286
286
  every alternative of an `any`, or a required (`all`, `not` or plain) check, needs it. Tags and cookie names are unique per leg. `variant`
287
287
  selects one variant or an array; omission selects all. `parallel` defaults to 1.
288
- `log(row)` receives completed rows. A variant supplies the wait before an
288
+ With the `browserContexts` capability each leg runs in its own browser context. A `Target.createBrowserContext` that fails with a timeout is retried once, and the row's `notes` records it; any other failure fails the row.
289
+
290
+ `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.
291
+
292
+ `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
289
293
  explicit `Target.detachFromTarget` step and for a `settle.variant` step.
290
294
 
291
295
  Every runnable vector has a declarative `liveCheck`; every observation has a
@@ -336,7 +340,7 @@ load event never ends a wait. After a child's `children` steps that include `Pag
336
340
  the runner waits, bounded at 5 s, for the child's first document to commit and load before
337
341
  any later child step. `{ "settle": { "beacon", "required": true, "dependsOn"? } }` waits
338
342
  for that beacon at the fixture and fails the row with `settle: no beacon from <origin>
339
- within <ms>ms` when it never arrives, unless a refusal it `dependsOn` was recorded; the rows
343
+ within <ms>ms` when it never arrives, unless a refusal it `dependsOn` was recorded. It checks `dependsOn` again while it waits, since a child's `children` steps may still be running. When the registration it depends on was refused, the settle is skipped, the row's `notes` gets `<leg> settle:skipped-refused beacon <origin>`, and the row is judged on its other observations. The rows
340
344
  whose `allowedBeacon` comes from an earlier document settle on it before their trigger.
341
345
  Beacons use `navigator.sendBeacon`, with a `fetch(…, { keepalive: true })` fallback. An observation that reads a background step's `as` result
342
346
  waits for that step, up to 10 s, and fails with `<as> did not complete within …` past it. Redirect
@@ -439,7 +443,7 @@ A six endpoint has tier `six`, whatever its seven tabs. **A program never choose
439
443
 
440
444
  ## Output schema 1
441
445
 
442
- `schema/browsers.schema.json` describes `slicc browsers --json`, which `playwright-cli list --json` also prints. [slicc-cli's README](https://github.com/ai-ecoverse/slicc-cli#slicc-browsers---json---cdp-url---no-bundle-scan) is the authoritative spec for it; this schema is a copy and follows it. Fields can be added; `source` is one of `devtools-active-port`, `flag`, `probe` and `process`. `tier` is 1 to 6, and `six: true` marks an endpoint in SLICC six's profile (tier 6).
446
+ `schema/browsers.schema.json` describes `slicc browsers --json`, which `playwright-cli list --json` also prints. [slicc-cli's README](https://github.com/ai-ecoverse/slicc-cli#slicc-browsers---json---cdp-url---no-bundle-scan) is the authoritative spec for it; this schema is a copy and follows it. Fields can be added; `source` is one of `devtools-active-port`, `flag`, `probe` and `process`. `tier` is 1 to 6, and `six: true` marks an endpoint in SLICC six's profile (tier 6). An endpoint may carry `tabs`: every page target except those on protected origins. Seven's own tabs are protected, so they are in `sevenTabs` only, never in `tabs`. `playwright-cli list --json` prints it, and a corpus case that checks it needs `tabs`.
443
447
 
444
448
  ## The corpus
445
449
 
@@ -447,7 +451,7 @@ Each case is a fake machine: a home tree, `DevToolsActivePort` files, fake `/jso
447
451
 
448
452
  **`case.json`:**
449
453
  - `description`, and `os?`: the OSes the case runs on (`darwin`, `linux`, `windows`; default all).
450
- - `needs`: what a program must support to run the case: `user-data-dirs` (`DevToolsActivePort` in browsers' default user-data dirs), `seven-profiles` (in the profile roots), `flag` (`--cdp` URLs), `process`, `probe` (port probes, `slicc` only), `origins` (extra seven origins), `installed` (ranking installed browsers), `six` (finding and marking SLICC six's profiles). A program skips a case that needs something it doesn't do.
454
+ - `needs`: what a program must support to run the case: `user-data-dirs` (`DevToolsActivePort` in browsers' default user-data dirs), `seven-profiles` (in the profile roots), `flag` (`--cdp` URLs), `process`, `probe` (port probes), `origins` (extra seven origins), `installed` (ranking installed browsers), `six` (finding and marking SLICC six's profiles), `tabs` (each endpoint's `tabs` list). A program skips a case that needs something it doesn't do.
451
455
  - `env?`: environment variables for discovery, which sees nothing else from the real environment.
452
456
  - `files?`: file contents by path. Every path starts with a token.
453
457
  - `cdp?`, `origins?`, `probePorts?`: `--cdp` URLs, extra seven origins, and the ports to probe.
@@ -508,7 +512,7 @@ for case in corpus().cases {
508
512
 
509
513
  Go 1.26, Rust stable with `wasm32-wasip1`, Node ≥ 24.
510
514
  - `npm run corpus` regenerates `corpus.json` from `corpus/`; the pre-commit hook and CI fail when it's stale.
511
- - `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`, `six` and `auto` in each `expected.json`, and no port from 9222 to 9229.
515
+ - `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.
512
516
  - `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.
513
517
  - `npm test`: `validate`, the Go tests in `test/integration/`, and `cargo test --all-features`.
514
518
  - `npm run build`: the Go module, the crate (also for `wasm32-wasip1`), and `npm pack --dry-run`.
package/corpus.json CHANGED
@@ -664,6 +664,124 @@
664
664
  ]
665
665
  }
666
666
  },
667
+ {
668
+ "name": "endpoint-tabs",
669
+ "case": {
670
+ "description": "tabs lists every page target except those on protected origins; seven's own tabs are only in sevenTabs, and iframes, workers and other protected pages are left out",
671
+ "needs": [
672
+ "flag",
673
+ "tabs"
674
+ ],
675
+ "cdp": [
676
+ "127.0.0.1:9331"
677
+ ],
678
+ "http": {
679
+ "127.0.0.1:9331": {
680
+ "version": {
681
+ "Browser": "Chrome/154.0.8037.98",
682
+ "Protocol-Version": "1.3",
683
+ "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/154.0.8037.98 Safari/537.36",
684
+ "V8-Version": "15.4.79.1",
685
+ "WebKit-Version": "537.36 (@a1b2c3)",
686
+ "webSocketDebuggerUrl": "ws://127.0.0.1:9331/devtools/browser/t1"
687
+ },
688
+ "list": [
689
+ {
690
+ "id": "P1",
691
+ "type": "page",
692
+ "url": "https://lars.sliccy.ai/",
693
+ "title": "seven",
694
+ "webSocketDebuggerUrl": "ws://127.0.0.1/devtools/page/P1"
695
+ },
696
+ {
697
+ "id": "P2",
698
+ "type": "page",
699
+ "url": "https://example.test/docs",
700
+ "title": "docs",
701
+ "webSocketDebuggerUrl": "ws://127.0.0.1/devtools/page/P2"
702
+ },
703
+ {
704
+ "id": "P3",
705
+ "type": "page",
706
+ "url": "https://www.sliccy.ai/",
707
+ "title": "SLICC",
708
+ "webSocketDebuggerUrl": "ws://127.0.0.1/devtools/page/P3"
709
+ },
710
+ {
711
+ "id": "P4",
712
+ "type": "page",
713
+ "url": "https://vault.sliccy.ai/",
714
+ "title": "vault",
715
+ "webSocketDebuggerUrl": "ws://127.0.0.1/devtools/page/P4"
716
+ },
717
+ {
718
+ "id": "P5",
719
+ "type": "page",
720
+ "url": "about:blank",
721
+ "title": "",
722
+ "webSocketDebuggerUrl": "ws://127.0.0.1/devtools/page/P5"
723
+ },
724
+ {
725
+ "id": "P6",
726
+ "type": "iframe",
727
+ "url": "https://example.test/frame",
728
+ "title": "frame",
729
+ "webSocketDebuggerUrl": "ws://127.0.0.1/devtools/page/P6"
730
+ },
731
+ {
732
+ "id": "P7",
733
+ "type": "service_worker",
734
+ "url": "https://example.test/sw.js",
735
+ "title": "worker",
736
+ "webSocketDebuggerUrl": "ws://127.0.0.1/devtools/page/P7"
737
+ }
738
+ ]
739
+ }
740
+ }
741
+ },
742
+ "expected": {
743
+ "schema": 1,
744
+ "minChromium": 116,
745
+ "entries": [
746
+ {
747
+ "id": "cdp:127.0.0.1:9331",
748
+ "kind": "endpoint",
749
+ "tier": 1,
750
+ "name": "Chrome",
751
+ "version": "154.0.8037.98",
752
+ "endpoint": {
753
+ "cdp": "http://127.0.0.1:9331",
754
+ "webSocketDebuggerUrl": "ws://127.0.0.1:9331/devtools/browser/t1",
755
+ "name": "Chrome",
756
+ "version": "154.0.8037.98",
757
+ "product": "Chrome/154.0.8037.98",
758
+ "userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/154.0.8037.98 Safari/537.36",
759
+ "source": "flag",
760
+ "sevenOpen": true,
761
+ "sevenTabs": [
762
+ {
763
+ "id": "P1",
764
+ "url": "https://lars.sliccy.ai/",
765
+ "title": "seven"
766
+ }
767
+ ],
768
+ "tabs": [
769
+ {
770
+ "id": "P2",
771
+ "url": "https://example.test/docs",
772
+ "title": "docs"
773
+ },
774
+ {
775
+ "id": "P5",
776
+ "url": "about:blank",
777
+ "title": ""
778
+ }
779
+ ]
780
+ }
781
+ }
782
+ ]
783
+ }
784
+ },
667
785
  {
668
786
  "name": "extra-origins",
669
787
  "case": {
package/guard-driver.mjs CHANGED
@@ -173,7 +173,18 @@ export function result(vector, run) {
173
173
  }
174
174
  const c = run.control ? judge(vector, 'control', run.control) : null;
175
175
  const g = run.guarded ? judge(vector, 'guarded', run.guarded) : null;
176
- const only = (a, b) => (a?.refused ?? []).filter((name) => !(b?.refused ?? []).includes(name));
176
+ const ranOn = (observed, name) => typeof observed?.[name] === 'boolean';
177
+ const both = (name) => ranOn(run.control, name) && ranOn(run.guarded, name);
178
+ const only = (a, b) =>
179
+ (a?.refused ?? []).filter((name) => both(name) && !(b?.refused ?? []).includes(name));
180
+ const ran = Object.fromEntries(
181
+ refusals(vector)
182
+ .filter(
183
+ (name) =>
184
+ run.control && run.guarded && ranOn(run.control, name) !== ranOn(run.guarded, name)
185
+ )
186
+ .map((name) => [name, ranOn(run.control, name) ? 'control-only' : 'guarded-only'])
187
+ );
177
188
  const why = [];
178
189
  let control = c?.pass ? 'pass' : 'fail';
179
190
  if (!c) why.push('no control');
@@ -195,7 +206,14 @@ export function result(vector, run) {
195
206
  } else why.push(...g.mismatches);
196
207
  const live = run.live === true ? 'pass' : 'fail';
197
208
  if (live === 'fail') why.push('not live');
198
- return { ...row, control, guarded, live, ...(why.length ? { why: why.join('; ') } : {}) };
209
+ return {
210
+ ...row,
211
+ control,
212
+ guarded,
213
+ live,
214
+ ...(Object.keys(ran).length ? { ran } : {}),
215
+ ...(why.length ? { why: why.join('; ') } : {}),
216
+ };
199
217
  }
200
218
 
201
219
  const passed = (r) =>
package/guard-runner.mjs CHANGED
@@ -107,6 +107,7 @@ class Execution {
107
107
  this.contexts = [];
108
108
  this.pending = [];
109
109
  this.errors = [];
110
+ this.notes = [];
110
111
  this.answers = [];
111
112
  this.attachSettings = new Map();
112
113
  this.childSteps = [];
@@ -384,12 +385,21 @@ class Execution {
384
385
  }
385
386
  }
386
387
 
388
+ async createContext() {
389
+ const client = this.reader ?? this.client;
390
+ try {
391
+ return (await client.send('Target.createBrowserContext', {})).browserContextId;
392
+ } catch (error) {
393
+ const message = String(error?.message ?? error);
394
+ if (!/timed? ?out/i.test(message)) throw error;
395
+ this.notes.push(`Target.createBrowserContext retried once after: ${message}`);
396
+ return (await client.send('Target.createBrowserContext', {})).browserContextId;
397
+ }
398
+ }
399
+
387
400
  async seed() {
388
401
  if (supported(['browserContexts'], this.options.capabilities)) {
389
- const { browserContextId } = await (this.reader ?? this.client).send(
390
- 'Target.createBrowserContext',
391
- {}
392
- );
402
+ const browserContextId = await this.createContext();
393
403
  this.vars.context = browserContextId;
394
404
  this.contexts.push(browserContextId);
395
405
  }
@@ -413,7 +423,7 @@ class Execution {
413
423
  }
414
424
 
415
425
  async pump() {
416
- while (!this.stopped) {
426
+ while (!this.stopped && !this.clients.flight?.abandoned) {
417
427
  for (const event of this.client.events.slice(this.eventStart)) {
418
428
  if (this.processed.has(event)) continue;
419
429
  this.processed.add(event);
@@ -545,32 +555,47 @@ class Execution {
545
555
  .some((e) => e.method === 'Target.detachedFromTarget' && e.params?.sessionId === session);
546
556
  }
547
557
 
558
+ record(step, out, session, phase, gone) {
559
+ const refused =
560
+ !gone &&
561
+ (step.refusable
562
+ ? isRefusal(out, step)
563
+ : /refused/.test(String(out.error?.message ?? out.error)));
564
+ this.trace.push({
565
+ phase,
566
+ declared: Boolean(step.refusable),
567
+ session,
568
+ out,
569
+ refused,
570
+ ...(gone ? { detached: true } : {}),
571
+ });
572
+ if (step.refusable) this.refused[step.refusal.name] = refused;
573
+ if (step.refusable && !out.ok && !refused && !gone)
574
+ this.errors.push(
575
+ `${step.refusal.name} failed without a refusal: ${String(out.error?.message ?? out.error)}`
576
+ );
577
+ if (step.as && !out.result?.pending) this.calls[step.as] = out;
578
+ if (step.cdp) (this.calls.$cdp ??= []).push({ method: step.cdp, ...out });
579
+ }
580
+
548
581
  async execute(list, childSession, phase = 'child') {
549
582
  if (phase === 'trigger') await this.childrenSettled();
550
- for (const raw of list) {
583
+ for (const [index, raw] of list.entries()) {
584
+ if (this.clients.flight)
585
+ Object.assign(this.clients.flight, { leg: this.side, phase, step: index });
551
586
  const step = expand(raw, this.vars);
552
587
  if (!supported(step.requires, this.options.capabilities) || step.text) continue;
553
588
  if (!childSession && step.session === 'child') await this.childrenSettled();
554
589
  const session = childSession ?? this.session(step.session);
555
590
  const out = await this.operation(step, session, phase);
556
591
  if (!out) continue;
557
- const gone = Boolean(childSession) && !out.ok && this.detached(childSession);
558
- const refused =
559
- !gone &&
560
- (step.refusable
561
- ? isRefusal(out, step)
562
- : /refused/.test(String(out.error?.message ?? out.error)));
563
- this.trace.push({
564
- phase,
565
- declared: Boolean(step.refusable),
566
- session,
592
+ this.record(
593
+ step,
567
594
  out,
568
- refused,
569
- ...(gone ? { detached: true } : {}),
570
- });
571
- if (step.refusable) this.refused[step.refusal.name] = refused;
572
- if (step.as && !out.result?.pending) this.calls[step.as] = out;
573
- if (step.cdp) (this.calls.$cdp ??= []).push({ method: step.cdp, ...out });
595
+ session,
596
+ phase,
597
+ Boolean(childSession) && !out.ok && this.detached(childSession)
598
+ );
574
599
  this.options.debug?.({ step, out, side: this.side });
575
600
  }
576
601
  }
@@ -604,13 +629,21 @@ class Execution {
604
629
  }
605
630
 
606
631
  async settle(settle) {
607
- if ((settle.dependsOn ?? []).some((name) => this.refused[name])) return;
632
+ const refused = () => (settle.dependsOn ?? []).some((name) => this.refused[name]);
633
+ const skipped = () => {
634
+ this.notes.push(
635
+ `settle:skipped-refused ${settle.beacon ? `beacon ${settle.beacon}` : settle.served ? `request ${settle.served}` : settle.title ? `title ${settle.title}` : 'load'}`
636
+ );
637
+ };
638
+ if (refused()) return skipped();
608
639
  if (settle.variant) await sleep(this.delay);
609
640
  if (settle.title && !this.reader) {
610
641
  await sleep(settle.ms ?? 1500);
611
642
  return;
612
643
  }
613
644
  const met = await this.poll(async () => {
645
+ if (refused()) return true;
646
+ if (settle.served) return this.requestCheck({ served: settle.served, tagged: false });
614
647
  if (settle.beacon)
615
648
  return (
616
649
  (await this.requestCheck({ beacon: settle.beacon }, this.actionRequestStart)) &&
@@ -628,9 +661,10 @@ class Execution {
628
661
  .some((e) => e.method === 'Page.loadEventFired' && e.sessionId === event.sessionId)
629
662
  );
630
663
  }, settle.ms ?? 1500);
664
+ if (refused()) return skipped();
631
665
  if (settle.required && !met)
632
666
  this.errors.push(
633
- `settle: no ${settle.beacon ? `beacon from ${settle.beacon}` : settle.title ? `title ${settle.title}` : 'load'} within ${settle.ms ?? 1500}ms`
667
+ `settle: no ${settle.beacon ? `beacon from ${settle.beacon}` : settle.served ? `request for ${settle.served}` : settle.title ? `title ${settle.title}` : 'load'} within ${settle.ms ?? 1500}ms`
634
668
  );
635
669
  }
636
670
 
@@ -986,7 +1020,7 @@ class Execution {
986
1020
  side: this.side,
987
1021
  });
988
1022
  const live = this.side === 'control' ? true : await this.check(this.vector.liveCheck);
989
- return { observed, live, errors: this.errors };
1023
+ return { observed, live, errors: this.errors, notes: this.notes };
990
1024
  } finally {
991
1025
  this.stopped = true;
992
1026
  await pumping;
@@ -1129,8 +1163,87 @@ function variables(targets, target, side, slot, index) {
1129
1163
  return vars;
1130
1164
  }
1131
1165
 
1132
- async function executeRow(job, opts, clients, set, slot, index) {
1166
+ function tracked(client, calls, name) {
1167
+ if (!client || typeof client.send !== 'function') return client;
1168
+ let next = 0;
1169
+ const send = (method, params, sessionId) => {
1170
+ const id = ++next;
1171
+ calls.set(id, { name, method, session: sessionId, since: Date.now() });
1172
+ return Promise.resolve()
1173
+ .then(() => client.send(method, params, sessionId))
1174
+ .finally(() => calls.delete(id));
1175
+ };
1176
+ return new Proxy(client, {
1177
+ get(base, key) {
1178
+ if (key === 'send') return send;
1179
+ const value = Reflect.get(base, key);
1180
+ return typeof value === 'function' ? value.bind(base) : value;
1181
+ },
1182
+ });
1183
+ }
1184
+
1185
+ function view(flight, base, clients) {
1186
+ const now = Date.now();
1187
+ const pending = (name) =>
1188
+ [...flight.calls.values()]
1189
+ .filter((call) => call.name === name)
1190
+ .map(({ method, session, since }) => ({ method, session, ageMs: now - since }));
1191
+ return {
1192
+ id: base.id,
1193
+ target: base.target,
1194
+ variant: base.variant,
1195
+ leg: flight.leg,
1196
+ phase: flight.phase,
1197
+ step: flight.step,
1198
+ clients: {
1199
+ client: pending('client'),
1200
+ ...(clients.unguarded && clients.unguarded !== clients.client
1201
+ ? { unguarded: pending('unguarded') }
1202
+ : {}),
1203
+ },
1204
+ };
1205
+ }
1206
+
1207
+ async function watch(work, ms, snapshot) {
1208
+ if (!ms) {
1209
+ await work;
1210
+ return undefined;
1211
+ }
1212
+ let timer;
1213
+ const expired = new Promise((resolve) => {
1214
+ timer = setTimeout(() => resolve(snapshot()), ms);
1215
+ });
1216
+ const stuck = await Promise.race([work.then(() => undefined), expired]);
1217
+ clearTimeout(timer);
1218
+ return stuck;
1219
+ }
1220
+
1221
+ async function timedOut(stuck, base, made, work, opts) {
1222
+ const close = made.close;
1223
+ made.close = undefined;
1224
+ await close?.call(made);
1225
+ work.catch(() => {});
1226
+ const failed = {
1227
+ ...base,
1228
+ control: 'fail',
1229
+ guarded: 'fail',
1230
+ live: 'fail',
1231
+ why: `row timed out after ${opts.rowTimeoutMs}ms`,
1232
+ errors: [`row timed out after ${opts.rowTimeoutMs}ms: ${JSON.stringify(stuck)}`],
1233
+ inFlight: stuck,
1234
+ };
1235
+ opts.log?.(failed);
1236
+ return failed;
1237
+ }
1238
+
1239
+ async function executeRow(job, opts, made, set, slot, index, flights) {
1133
1240
  const { target, variant } = job;
1241
+ const calls = new Map();
1242
+ const client = tracked(made.client, calls, 'client');
1243
+ const unguarded =
1244
+ made.unguarded === made.client ? client : tracked(made.unguarded, calls, 'unguarded');
1245
+ const flight = { calls, leg: undefined, phase: undefined, step: undefined };
1246
+ const clients = { ...made, client, unguarded, flight };
1134
1247
  const scoped = available(job.vector, target, opts.targets);
1135
1248
  const vector = scoped.vector ?? job.vector;
1136
1249
  const base = {
@@ -1188,20 +1301,30 @@ async function executeRow(job, opts, clients, set, slot, index) {
1188
1301
  reason: 'not-supported:unguarded-observer',
1189
1302
  };
1190
1303
  const legs = {};
1191
- for (const side of ['control', 'guarded']) {
1192
- try {
1193
- const vars = variables(opts.targets, target, side, slot, index);
1194
- legs[side] = await new Execution(
1195
- vector,
1196
- side,
1197
- opts,
1198
- clients,
1199
- vars,
1200
- set.variants[variant]
1201
- ).run();
1202
- } catch (error) {
1203
- legs[side] = { observed: {}, live: false, errors: [String(error.message ?? error)] };
1304
+ flights.add({ flight, base, clients });
1305
+ opts.onStart?.(base);
1306
+ const sides = (async () => {
1307
+ for (const side of ['control', 'guarded']) {
1308
+ try {
1309
+ const vars = variables(opts.targets, target, side, slot, index);
1310
+ legs[side] = await new Execution(
1311
+ vector,
1312
+ side,
1313
+ opts,
1314
+ clients,
1315
+ vars,
1316
+ set.variants[variant]
1317
+ ).run();
1318
+ } catch (error) {
1319
+ legs[side] = { observed: {}, live: false, errors: [String(error.message ?? error)] };
1320
+ }
1204
1321
  }
1322
+ })();
1323
+ const stuck = await watch(sides, opts.rowTimeoutMs, () => view(flight, base, clients));
1324
+ for (const entry of flights) if (entry.flight === flight) flights.delete(entry);
1325
+ if (stuck) {
1326
+ flight.abandoned = true;
1327
+ return timedOut(stuck, base, made, sides, opts);
1205
1328
  }
1206
1329
  const row = {
1207
1330
  ...result(vector, {
@@ -1218,6 +1341,11 @@ async function executeRow(job, opts, clients, set, slot, index) {
1218
1341
  errors: [...legs.control.errors, ...legs.guarded.errors],
1219
1342
  designDependent: vector.designDependent ?? [],
1220
1343
  };
1344
+ const notes = [
1345
+ ...(legs.control.notes ?? []).map((note) => `control ${note}`),
1346
+ ...(legs.guarded.notes ?? []).map((note) => `guarded ${note}`),
1347
+ ];
1348
+ if (notes.length) row.notes = notes;
1221
1349
  if (row.errors.length) {
1222
1350
  row.control = 'fail';
1223
1351
  row.guarded = 'fail';
@@ -1273,7 +1401,16 @@ function validate(set, opts) {
1273
1401
  return variants;
1274
1402
  }
1275
1403
 
1276
- export async function run(set, options) {
1404
+ export function run(set, options) {
1405
+ const flights = new Set();
1406
+ const inFlight = () =>
1407
+ [...flights].map(({ flight, base, clients }) => view(flight, base, clients));
1408
+ const running = runAll(set, options, flights).then((done) => ({ ...done, inFlight }));
1409
+ running.inFlight = inFlight;
1410
+ return running;
1411
+ }
1412
+
1413
+ async function runAll(set, options, flights) {
1277
1414
  const opts = { capabilities: {}, variant: Object.keys(set.variants), parallel: 1, ...options };
1278
1415
  const variants = validate(set, opts);
1279
1416
  const jobs = set.vectors.flatMap((vector) =>
@@ -1317,11 +1454,11 @@ export async function run(set, options) {
1317
1454
  });
1318
1455
  await previous;
1319
1456
  try {
1320
- results[index] = await executeRow(job, opts, clients, set, slot, index);
1457
+ results[index] = await executeRow(job, opts, clients, set, slot, index, flights);
1321
1458
  } finally {
1322
1459
  done();
1323
1460
  }
1324
- } else results[index] = await executeRow(job, opts, clients, set, slot, index);
1461
+ } else results[index] = await executeRow(job, opts, clients, set, slot, index, flights);
1325
1462
  } finally {
1326
1463
  await clients.close?.();
1327
1464
  }
@@ -1,12 +1,17 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "setVersion": "1.7.6",
3
+ "setVersion": "1.7.8",
4
4
  "sources": [
5
5
  {
6
6
  "repo": "ai-ecoverse/slicc-cli",
7
7
  "ref": "2d5d98188667cd70180f825c7cba985399167732",
8
8
  "path": "test/integration/testdata/link/protect.js"
9
9
  },
10
+ {
11
+ "repo": "ai-ecoverse/slicc-cli",
12
+ "ref": "fe33052afd7bf94e98f38f85e95d29df7cb9e8c2",
13
+ "path": "test/integration/testdata/link/protect.js"
14
+ },
10
15
  {
11
16
  "repo": "ai-ecoverse/slicc-cdp",
12
17
  "ref": "4907a45d5ab8547483eaa013d4378deafe9f82d8",
@@ -4825,6 +4830,302 @@
4825
4830
  "observer": "client"
4826
4831
  }
4827
4832
  },
4833
+ {
4834
+ "id": "C3.redirect-popup-stays-out",
4835
+ "family": "C",
4836
+ "kind": "cdp",
4837
+ "scope": "any",
4838
+ "setup": [
4839
+ {
4840
+ "text": "an allowed opener with auto-attach opens a popup on a redirect chain that ends on a page that later navigates itself to another path of its origin, then auto-attaches again without detaching"
4841
+ },
4842
+ {
4843
+ "page": "tab",
4844
+ "url": "{ordinary}/doc?n=opener"
4845
+ },
4846
+ {
4847
+ "cdp": "Target.setAutoAttach",
4848
+ "params": {
4849
+ "autoAttach": true,
4850
+ "waitForDebuggerOnStart": true,
4851
+ "flatten": true
4852
+ },
4853
+ "session": "browser",
4854
+ "refusable": true,
4855
+ "refusal": {
4856
+ "match": "^slicc: refused"
4857
+ },
4858
+ "requires": ["autoAttach", "browserSession"]
4859
+ },
4860
+ {
4861
+ "children": [
4862
+ {
4863
+ "cdp": "Page.enable"
4864
+ }
4865
+ ],
4866
+ "where": {
4867
+ "openerId": "page"
4868
+ },
4869
+ "requires": ["autoAttach"]
4870
+ }
4871
+ ],
4872
+ "trigger": [
4873
+ {
4874
+ "page": "open",
4875
+ "url": "{target}/page?k=deferred&defer=3000&n={tag}&to={target}/doc?n={tag}-later",
4876
+ "hops": [
4877
+ {
4878
+ "origin": "{ordinary}",
4879
+ "code": 302
4880
+ },
4881
+ {
4882
+ "origin": "{cross}",
4883
+ "code": 307
4884
+ }
4885
+ ]
4886
+ },
4887
+ {
4888
+ "settle": {
4889
+ "served": "{target}/doc?n={tag}-later",
4890
+ "ms": 15000,
4891
+ "required": true
4892
+ }
4893
+ },
4894
+ {
4895
+ "cdp": "Target.setAutoAttach",
4896
+ "params": {
4897
+ "autoAttach": true,
4898
+ "waitForDebuggerOnStart": false,
4899
+ "flatten": true
4900
+ },
4901
+ "session": "browser",
4902
+ "refusable": true,
4903
+ "refusal": {
4904
+ "match": "refused"
4905
+ },
4906
+ "requires": ["autoAttach", "browserSession"]
4907
+ },
4908
+ {
4909
+ "wait": 1000
4910
+ },
4911
+ {
4912
+ "cdp": "Target.setAutoAttach",
4913
+ "params": {
4914
+ "autoAttach": false,
4915
+ "waitForDebuggerOnStart": false,
4916
+ "flatten": true
4917
+ },
4918
+ "session": "browser",
4919
+ "refusable": true,
4920
+ "refusal": {
4921
+ "match": "refused"
4922
+ },
4923
+ "requires": ["autoAttach", "browserSession"]
4924
+ }
4925
+ ],
4926
+ "targets": ["seven", "vault", "vault-x", "apex"],
4927
+ "requires": ["autoAttach", "browserSession"],
4928
+ "observe": {
4929
+ "committed": {
4930
+ "means": "the stand-in served the popup's first page on {target}",
4931
+ "dependsOn": [],
4932
+ "check": {
4933
+ "served": "/page",
4934
+ "origin": "{target}",
4935
+ "observer": "fixture"
4936
+ }
4937
+ },
4938
+ "navigatedLater": {
4939
+ "means": "the popup navigated itself to {target}/doc?n={tag}-later",
4940
+ "dependsOn": [],
4941
+ "check": {
4942
+ "served": "{target}/doc?n={tag}-later",
4943
+ "observer": "fixture"
4944
+ }
4945
+ },
4946
+ "attached": {
4947
+ "means": "the client got Target.attachedToTarget for the popup on {target}, at open or later",
4948
+ "dependsOn": ["refused.2"],
4949
+ "requires": ["autoAttach"],
4950
+ "check": {
4951
+ "attached": "{target}",
4952
+ "session": "child",
4953
+ "observer": "client"
4954
+ }
4955
+ }
4956
+ },
4957
+ "live": "the runner's own CDP sees {target}'s document load (title, marker or URL), or the guard returned its refusal for a refusable step",
4958
+ "control": {
4959
+ "origin": "ordinary",
4960
+ "via": "guard",
4961
+ "expected": {
4962
+ "committed": true,
4963
+ "navigatedLater": true,
4964
+ "attached": true
4965
+ }
4966
+ },
4967
+ "expected": {
4968
+ "committed": true,
4969
+ "navigatedLater": true,
4970
+ "attached": false
4971
+ },
4972
+ "liveCheck": {
4973
+ "any": [
4974
+ {
4975
+ "loaded": "{target}/doc?n={tag}-later",
4976
+ "observer": "fixture"
4977
+ },
4978
+ {
4979
+ "registration": "any",
4980
+ "observer": "client"
4981
+ }
4982
+ ],
4983
+ "observer": "client"
4984
+ }
4985
+ },
4986
+ {
4987
+ "id": "C3.direct-popup-stays-out",
4988
+ "family": "C",
4989
+ "kind": "cdp",
4990
+ "scope": "any",
4991
+ "setup": [
4992
+ {
4993
+ "text": "an allowed opener with auto-attach opens a popup straight on a page that later navigates itself to another path of its origin, then auto-attaches again without detaching"
4994
+ },
4995
+ {
4996
+ "page": "tab",
4997
+ "url": "{ordinary}/doc?n=opener"
4998
+ },
4999
+ {
5000
+ "cdp": "Target.setAutoAttach",
5001
+ "params": {
5002
+ "autoAttach": true,
5003
+ "waitForDebuggerOnStart": true,
5004
+ "flatten": true
5005
+ },
5006
+ "session": "browser",
5007
+ "refusable": true,
5008
+ "refusal": {
5009
+ "match": "^slicc: refused"
5010
+ },
5011
+ "requires": ["autoAttach", "browserSession"]
5012
+ },
5013
+ {
5014
+ "children": [
5015
+ {
5016
+ "cdp": "Page.enable"
5017
+ }
5018
+ ],
5019
+ "where": {
5020
+ "openerId": "page"
5021
+ },
5022
+ "requires": ["autoAttach"]
5023
+ }
5024
+ ],
5025
+ "trigger": [
5026
+ {
5027
+ "page": "open",
5028
+ "url": "{target}/page?k=deferred&defer=3000&n={tag}&to={target}/doc?n={tag}-later"
5029
+ },
5030
+ {
5031
+ "settle": {
5032
+ "served": "{target}/doc?n={tag}-later",
5033
+ "ms": 15000,
5034
+ "required": true
5035
+ }
5036
+ },
5037
+ {
5038
+ "cdp": "Target.setAutoAttach",
5039
+ "params": {
5040
+ "autoAttach": true,
5041
+ "waitForDebuggerOnStart": false,
5042
+ "flatten": true
5043
+ },
5044
+ "session": "browser",
5045
+ "refusable": true,
5046
+ "refusal": {
5047
+ "match": "refused"
5048
+ },
5049
+ "requires": ["autoAttach", "browserSession"]
5050
+ },
5051
+ {
5052
+ "wait": 1000
5053
+ },
5054
+ {
5055
+ "cdp": "Target.setAutoAttach",
5056
+ "params": {
5057
+ "autoAttach": false,
5058
+ "waitForDebuggerOnStart": false,
5059
+ "flatten": true
5060
+ },
5061
+ "session": "browser",
5062
+ "refusable": true,
5063
+ "refusal": {
5064
+ "match": "refused"
5065
+ },
5066
+ "requires": ["autoAttach", "browserSession"]
5067
+ }
5068
+ ],
5069
+ "targets": ["seven", "vault", "vault-x", "apex"],
5070
+ "requires": ["autoAttach", "browserSession"],
5071
+ "observe": {
5072
+ "committed": {
5073
+ "means": "the stand-in served the popup's first page on {target}",
5074
+ "dependsOn": [],
5075
+ "check": {
5076
+ "served": "/page",
5077
+ "origin": "{target}",
5078
+ "observer": "fixture"
5079
+ }
5080
+ },
5081
+ "navigatedLater": {
5082
+ "means": "the popup navigated itself to {target}/doc?n={tag}-later",
5083
+ "dependsOn": [],
5084
+ "check": {
5085
+ "served": "{target}/doc?n={tag}-later",
5086
+ "observer": "fixture"
5087
+ }
5088
+ },
5089
+ "attached": {
5090
+ "means": "the client got Target.attachedToTarget for the popup on {target}, at open or later",
5091
+ "dependsOn": ["refused.2"],
5092
+ "requires": ["autoAttach"],
5093
+ "check": {
5094
+ "attached": "{target}",
5095
+ "session": "child",
5096
+ "observer": "client"
5097
+ }
5098
+ }
5099
+ },
5100
+ "live": "the runner's own CDP sees {target}'s document load (title, marker or URL), or the guard returned its refusal for a refusable step",
5101
+ "control": {
5102
+ "origin": "ordinary",
5103
+ "via": "guard",
5104
+ "expected": {
5105
+ "committed": true,
5106
+ "navigatedLater": true,
5107
+ "attached": true
5108
+ }
5109
+ },
5110
+ "expected": {
5111
+ "committed": true,
5112
+ "navigatedLater": true,
5113
+ "attached": false
5114
+ },
5115
+ "liveCheck": {
5116
+ "any": [
5117
+ {
5118
+ "loaded": "{target}/doc?n={tag}-later",
5119
+ "observer": "fixture"
5120
+ },
5121
+ {
5122
+ "registration": "any",
5123
+ "observer": "client"
5124
+ }
5125
+ ],
5126
+ "observer": "client"
5127
+ }
5128
+ },
4828
5129
  {
4829
5130
  "id": "C4.static-cross-site",
4830
5131
  "family": "C",
@@ -5172,7 +5473,7 @@
5172
5473
  },
5173
5474
  "beacon": {
5174
5475
  "means": "a beacon from {target}",
5175
- "dependsOn": ["refused.7", "refused.8"],
5476
+ "dependsOn": ["refused.3", "refused.7", "refused.8"],
5176
5477
  "requires": ["autoAttach"],
5177
5478
  "check": {
5178
5479
  "beacon": "{target}",
@@ -5458,11 +5759,24 @@
5458
5759
  "means": "a child frame is a chrome-error page for {target}/doc?n={tag}",
5459
5760
  "dependsOn": ["refused.8"],
5460
5761
  "check": {
5461
- "result": "frameTree",
5462
- "path": "result",
5463
- "equals": {
5464
- "$regex": "chrome-error://"
5465
- },
5762
+ "any": [
5763
+ {
5764
+ "result": "frameTree",
5765
+ "path": "result",
5766
+ "equals": {
5767
+ "$regex": "chrome-error://"
5768
+ },
5769
+ "observer": "client"
5770
+ },
5771
+ {
5772
+ "frameBlocked": {
5773
+ "url": "{target}/doc?n={tag}",
5774
+ "session": "page",
5775
+ "frame": true
5776
+ },
5777
+ "observer": "unguarded"
5778
+ }
5779
+ ],
5466
5780
  "observer": "client"
5467
5781
  }
5468
5782
  }
@@ -5690,11 +6004,24 @@
5690
6004
  "means": "a child frame is a chrome-error page for {target}/doc?n={tag}",
5691
6005
  "dependsOn": ["refused.8"],
5692
6006
  "check": {
5693
- "result": "frameTree",
5694
- "path": "result",
5695
- "equals": {
5696
- "$regex": "chrome-error://"
5697
- },
6007
+ "any": [
6008
+ {
6009
+ "result": "frameTree",
6010
+ "path": "result",
6011
+ "equals": {
6012
+ "$regex": "chrome-error://"
6013
+ },
6014
+ "observer": "client"
6015
+ },
6016
+ {
6017
+ "frameBlocked": {
6018
+ "url": "{target}/doc?n={tag}",
6019
+ "session": "page",
6020
+ "frame": true
6021
+ },
6022
+ "observer": "unguarded"
6023
+ }
6024
+ ],
5698
6025
  "observer": "client"
5699
6026
  }
5700
6027
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-ecoverse/slicc-browsers",
3
- "version": "1.7.6",
3
+ "version": "1.7.8",
4
4
  "description": "SLICC's shared browser catalog, JSON schemas and discovery conformance corpus",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -65,13 +65,13 @@
65
65
  "trust:dry": "npx fledgling sync --dry-run -y --skip-publish",
66
66
  "release": "semantic-release",
67
67
  "test:guard": "npm run test:guard-integration && npm run test:guard-sweep",
68
- "test:guard-integration": "node --test 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",
68
+ "test:guard-integration": "node --test 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-diagnostics.test.mjs",
69
69
  "test:guard-sweep": "node scripts/test-guards.mjs",
70
70
  "test:guard-target": "node --test test/integration/guard-runner.test.mjs",
71
71
  "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"
72
72
  },
73
73
  "devDependencies": {
74
- "@ai-ecoverse/slicc-shared-web": "1.23.0",
74
+ "@ai-ecoverse/slicc-shared-web": "1.23.1",
75
75
  "@biomejs/biome": "2.5.15",
76
76
  "ajv": "8.20.0",
77
77
  "ajv-formats": "3.0.1",
@@ -75,7 +75,8 @@
75
75
  "wsOnly": { "const": true },
76
76
  "six": { "const": true },
77
77
  "sevenOpen": { "type": "boolean" },
78
- "sevenTabs": { "type": "array", "items": { "$ref": "#/$defs/tab" } }
78
+ "sevenTabs": { "type": "array", "items": { "$ref": "#/$defs/tab" } },
79
+ "tabs": { "type": "array", "items": { "$ref": "#/$defs/tab" } }
79
80
  }
80
81
  },
81
82
  "installed": {
@@ -47,7 +47,8 @@
47
47
  "probe",
48
48
  "origins",
49
49
  "installed",
50
- "six"
50
+ "six",
51
+ "tabs"
51
52
  ]
52
53
  }
53
54
  },
@@ -582,6 +582,9 @@
582
582
  "beacon": {
583
583
  "type": "string"
584
584
  },
585
+ "served": {
586
+ "type": "string"
587
+ },
585
588
  "title": {
586
589
  "type": "string"
587
590
  },