@ai-ecoverse/slicc-browsers 1.13.1 → 1.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -204,6 +204,8 @@ The `C8.newtab` reachability control attaches through the guard to a settled ord
204
204
 
205
205
  `C8.network-error` and `C8.iframe-error` are ordinary-error reachability controls: request the configured `network-error` URL (default `http://nonexistent.invalid/`) directly or in an ordinary iframe, observe a failed Document request, reattach to the settled tab, evaluate and snapshot its document, then navigate the same tab to an ordinary document. Both control and guarded legs must pass. The failing URL is unchanged on the control, rather than replaced with a healthy origin, and the rows require `browserSession` but not `chromeUrlsAttach`. With Chrome for Testing 156.0.8078.4, scheme-wide `chrome-error` protection made the direct-error row fail both legs because reattachment was refused; removing it passed both rows, with readable DOM and successful recovery. The old emulator still allowed the cross-process iframe parent because the iframe TargetInfo kept the failing HTTP URL, but that layout-specific behavior is not grounds for protecting the error scheme. Plain-client controls and reachability legs also passed both rows.
206
206
 
207
+ `E4` and the `E6` rows are runnable, each with a guarded-through control on the ordinary origin that must show the effect. `E4` sets a `Debugger.setBreakpointByUrl` on line 0 of the `listener=1` document's `/listener.js?n=<tag>` with a condition that sends `{beacon}` and returns `false`; the control needs that beacon, and the guarded leg needs neither the beacon nor a `Debugger.paused` in the protected document. `E4.instrumentation` sets `Debugger.setInstrumentationBreakpoint` with `beforeScriptWithSourceMapExecution`, so only `/listener.js`, whose `SourceMap` header names a source map, pauses: `beforeScriptExecution` would also pause the runner's own `Runtime.evaluate` and any extension's content script first. `E4.event-listener` sets `DOMDebugger.setEventListenerBreakpoint` for `load`. Both set up before the trigger, need a `Debugger.paused` on the control (in `/listener.js`, or for `E4.event-listener` with `listener:load`, which only that document registers, since a listener pause reports no script URL), none on the guarded leg, and end with `Debugger.disable` to resume. The `E6` overrides are set on the page session before the trigger: `E6.headers` (`x-probe`), `E6.ua-network` and `E6.ua-emulation` (`User-Agent: slicc-probe-<tag>`) and `E6.cache-disabled` (`Pragma: no-cache`) are read from the stand-in's request log; `E6.network-conditions` (1500 ms latency) needs a navigation `duration` of at least 1000 ms, since Chrome delays the body rather than `responseStart`; `E6.locale` (`fr-CH`), `E6.timezone` (`Pacific/Chatham`) and `E6.network-state` (`navigator.onLine` false) are read by the unguarded observer in the destination document, and `E6.network-state` starts on `{cross}`, because Chrome drops that override on a same-site navigation once a second session is attached to the page. A guard that keeps its own session on the page, as the test emulator's Fetch shield does, can still lose it, and then fails that control rather than passing it; `E6.blocked-urls` has the observer fetch a blocked `/data` subresource there. Observer reads first check that the page is the exact destination URL, so a blocked or replaced document never shows the effect. The five overrides Chrome also applies inside a cross-site iframe have an `.iframe` row, triggered through `/framed`; `E6.network-conditions.iframe` reads the iframe's `duration` from the parent's resource timing. Each override is refusable at registration, with its observation depending on that refusal. These rows settle on the destination's load or request rather than on the detach variant, because each effect lands when the request is made or the script runs.
208
+
207
209
  A client may register `Fetch.enable` patterns naming a protected origin in either of two conformant ways: the guard allows registration but never pauses or forwards protected requests to the client and refuses continue, fulfil or fail calls for request ids it did not hand out; or the guard refuses registration with its declared refusal text. Setup calls explicitly naming a protected placeholder are refusable with `refusal.onlyProtected: true`, and interception-dependent observations depend on that refusal. Wildcard and ordinary-only patterns do not receive this exemption. The ordinary-origin control still runs through the guard and must succeed.
208
210
 
209
211
  **Top level.**
@@ -250,8 +252,9 @@ Any `cdp`, `page` or `children` step can take `requires`, the capabilities it ne
250
252
  Popups are observed by `Target.targetCreated` with the opener's `openerId`, or by the child's `Target.attachedToTarget`, never by the URL: a popup waiting for the debugger can still be `about:blank`.
251
253
 
252
254
  **The stand-in** serves these routes on every origin. It never listens on 9222 to 9229.
253
- - `/doc?n=`: an HTML page with the `data-canary` marker; `csp=1` adds `Content-Security-Policy: script-src 'none'` and an inline script that sends a beacon tagged `inline-<n>` to the log, which the CSP blocks unless it is bypassed;
255
+ - `/doc?n=`: an HTML page with the `data-canary` marker; `listener=1` adds `<script src="/listener.js?n=<n>">`; `csp=1` adds `Content-Security-Policy: script-src 'none'` and an inline script that sends a beacon tagged `inline-<n>` to the log, which the CSP blocks unless it is bypassed;
254
256
  - `/hop?code=&to=` and `/framed?src=`;
257
+ - `/listener.js`: one line registering a window `load` listener, served with `SourceMap: /listener.js.map`;
255
258
  - `/data?n=`: `real-<n>`, with CORS;
256
259
  - `/img`: a 1×1 PNG;
257
260
  - `/auth`: Basic `u`:`p`, then `authed`;
@@ -281,7 +284,7 @@ For remote/page harnesses, `fixtureClient(base)` is exported by `./guard-runner`
281
284
 
282
285
  **Observability.** `observable.own: false` marks a vector the guard host's own origin can't show (headers, auth, redirects); a runner whose `own` is a static server reports it as `skip` with `not-observable`.
283
286
 
284
- **Applies to.** `appliesTo` lists the triggers a vector runs with (`href`, `iframe`, `popup`, `redirect`). Chrome doesn't apply some session overrides inside a cross-site iframe, so those E6 rows list `href` only.
287
+ **Applies to.** `appliesTo` lists the triggers a vector runs with (`href`, `iframe`, `popup`, `redirect`). Chrome doesn't apply some session overrides inside a cross-site iframe, so `E6.locale`, `E6.timezone`, `E6.network-state` and `E6.blocked-urls` have no `.iframe` row.
285
288
 
286
289
  **Variants.** `variants` gives the delay in milliseconds before the client's `Target.detachFromTarget`: `prompt` 0, `lagging` 8000. A runner may add its own variant names.
287
290
 
@@ -633,7 +636,7 @@ Go 1.26, Rust stable with `wasm32-wasip1`, Node ≥ 24.
633
636
  - `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.
634
637
  - 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.
635
638
  - `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.
636
- - `npm test`: `validate`, `test/integration/self-contained.test.mjs`, the Go tests in `test/integration/`, and `cargo test --all-features`.
639
+ - `npm test`: `validate`, `test/integration/self-contained.test.mjs`, `test/integration/git-retry.test.mjs`, the Go tests in `test/integration/`, and `cargo test --all-features`.
637
640
  - `npm run build`: the Go module, the crate (also for `wasm32-wasip1`), and `npm pack --dry-run`.
638
641
 
639
642
  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.
@@ -646,6 +649,8 @@ semantic-release cuts one version from `main` for all three packages (`.github/w
646
649
  3. **crates.io:** with the repository variable `SLICC_BROWSERS_CRATES_PUBLISH` set to `true`, `Cargo.toml` is stamped with the version and `cargo publish` runs with a short-lived token from crates.io trusted publishing (`rust-lang/crates-io-auth-action`). A version that is already on crates.io is skipped.
647
650
  4. **Cert line:** after the npm publish, the workflow runs `npm pack @ai-ecoverse/slicc-browsers@X.Y.Z` against the registry and appends `cert: @ai-ecoverse/slicc-browsers@X.Y.Z sha256=<hex> gitHead=<sha>` to the `vX.Y.Z` GitHub release and the job summary, once; the cert lane copies it from there.
648
651
 
652
+ GitHub sometimes rejects the push of `refs/notes/semantic-release-vX.Y.Z` with a bare `(failed)` right after the tag push; no ruleset covers `refs/notes/*`. The semantic-release step runs with `pipefail` and puts `scripts/git-retry.mjs` first on `PATH` as `git`: it runs the real git (`REAL_GIT`) and retries a push of a `refs/notes/` ref up to five times, 5, 10, 15 and 20 seconds apart; any other git command runs once. When semantic-release still fails, the job fails before npm and crates.io publish. Recover without moving the tag: create the GitHub release on the existing tag, then run `gh workflow run release.yml --ref main -f tag=vX.Y.Z`. A tag without its note counts as on the default channel (semantic-release reads a missing note as `channels: [null]`), so the next release builds on it. `test/integration/git-retry.test.mjs` tests the retry with a fake git.
653
+
649
654
  A run started by hand publishes what's missing without a new release: `gh workflow run release.yml --ref main` when `main`'s head has its tag, or `gh workflow run release.yml --ref main -f tag=vX.Y.Z` for any existing tag. `gh workflow run release.yml --ref main -f cert-only=true` only adds the cert line for the latest npm version.
650
655
 
651
656
  **Go module versions.** A pushed tag is the release: `proxy.golang.org` caches it and `sum.golang.org` records its hash for good. Never move or delete a pushed tag; retract a bad version in `go.mod` and release anew. A `BREAKING CHANGE` needs a `/vN` module path first.
@@ -12,9 +12,12 @@ const escape = (value) =>
12
12
  function document(url, html) {
13
13
  const p = url.searchParams;
14
14
  const csp = p.has('csp') || url.pathname === '/csp';
15
+ const listener = p.has('listener')
16
+ ? `<script src="/listener.js?n=${encodeURIComponent(p.get('n') ?? '')}"></script>`
17
+ : '';
15
18
  return html(
16
19
  'doc',
17
- `<p>document ${escape(p.get('n') ?? '')}</p>${csp ? '<script>document.title="inline"; document.body.dataset.inline="true"</script>' : ''}`,
20
+ `<p>document ${escape(p.get('n') ?? '')}</p>${listener}${csp ? '<script>document.title="inline"; document.body.dataset.inline="true"</script>' : ''}`,
18
21
  csp ? { 'content-security-policy': "script-src 'none'" } : {}
19
22
  );
20
23
  }
@@ -54,6 +57,13 @@ function route(req, url, send, html, redirects) {
54
57
  `globalThis.vectorScript=${JSON.stringify(`real-${p.get('n') ?? ''}`)}`,
55
58
  'text/javascript'
56
59
  ),
60
+ '/listener.js': () =>
61
+ send(
62
+ 200,
63
+ "addEventListener('load', function () { document.body.dataset.listened = 'true'; });",
64
+ 'text/javascript',
65
+ { sourcemap: '/listener.js.map' }
66
+ ),
57
67
  '/page': () => html('trigger', trigger(p)),
58
68
  '/api/cookies': () => send(200, req.headers.cookie ?? 'none', 'text/plain'),
59
69
  '/plant': () =>