@ai-ecoverse/slicc-browsers 1.13.2 → 1.14.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-ecoverse/slicc-browsers",
3
- "version": "1.13.2",
3
+ "version": "1.14.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",
@@ -61,19 +61,19 @@
61
61
  "fixture-client.mjs"
62
62
  ],
63
63
  "scripts": {
64
- "prepare": "lefthook install",
64
+ "prepare": "slicc-install-hooks",
65
65
  "corpus": "node scripts/corpus.mjs",
66
66
  "validate": "node scripts/validate.mjs",
67
67
  "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",
68
68
  "format": "biome check --write . && gofmt -w . && cargo fmt",
69
- "test": "npm run validate && node --test test/integration/self-contained.test.mjs test/integration/browser-runner.test.mjs test/integration/fixture-client.test.mjs test/integration/git-retry.test.mjs && go test -race -count=1 ./test/integration/... && cargo test --all-features",
69
+ "test": "npm run validate && node --test test/integration/self-contained.test.mjs test/integration/browser-runner.test.mjs test/integration/fixture-client.test.mjs test/integration/git-retry.test.mjs test/integration/dist-tag.test.mjs test/integration/guard-incomplete.test.mjs test/integration/guard-stall.test.mjs && go test -race -count=1 ./test/integration/... && cargo test --all-features",
70
70
  "test:unit": "go test -race -cover ./...",
71
71
  "build": "go build ./... && cargo build --all-features && cargo build --target wasm32-wasip1 --all-features && npm pack --dry-run",
72
72
  "trust": "npx fledgling sync -y --skip-publish",
73
73
  "trust:dry": "npx fledgling sync --dry-run -y --skip-publish",
74
74
  "release": "semantic-release",
75
75
  "test:guard": "npm run test:guard-integration && npm run test:guard-sweep",
76
- "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-redirect-secret.test.mjs test/integration/guard-error-page.test.mjs test/integration/guard-discovery.test.mjs test/integration/guard-browser-page.test.mjs test/integration/guard-scales.test.mjs test/integration/guard-shared-log.test.mjs test/integration/guard-pre-beacon.test.mjs test/integration/guard-world-pause.test.mjs test/integration/self-contained.test.mjs",
76
+ "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-redirect-secret.test.mjs test/integration/guard-error-page.test.mjs test/integration/guard-discovery.test.mjs test/integration/guard-browser-page.test.mjs test/integration/guard-scales.test.mjs test/integration/guard-shared-log.test.mjs test/integration/guard-pre-beacon.test.mjs test/integration/guard-world-pause.test.mjs test/integration/guard-parallel.test.mjs test/integration/self-contained.test.mjs",
77
77
  "test:guard-sweep": "node scripts/test-guards.mjs",
78
78
  "test:gates": "node --test test/integration/gates.test.mjs",
79
79
  "test:guard-target": "node --test test/integration/guard-runner.test.mjs",
@@ -81,7 +81,7 @@
81
81
  "test:unit:guard": "mkdir -p coverage/unit && node --experimental-vm-modules --no-warnings --experimental-test-coverage --test --test-coverage-include=guard-runner.mjs --test-coverage-include=fixture-server.mjs --test-coverage-include=fixture-client.mjs --test-coverage-include=observer-target.mjs --test-coverage-include=protected-error-page.mjs --test-coverage-include=unparseable-url.mjs --test-coverage-include=scripts/browser-runner.mjs --test-coverage-include=scripts/validate.mjs --test-reporter=lcov --test-reporter-destination=coverage/unit/guard.lcov test/unit/guard-*.test.mjs && slicc-diff-cover coverage/unit/guard.lcov"
82
82
  },
83
83
  "devDependencies": {
84
- "@ai-ecoverse/slicc-shared-web": "2.2.0",
84
+ "@ai-ecoverse/slicc-shared-web": "2.3.5",
85
85
  "@biomejs/biome": "2.5.15",
86
86
  "ajv": "8.20.0",
87
87
  "ajv-formats": "3.0.1",
@@ -1,19 +1,10 @@
1
- import { outcome } from './guard-driver.mjs';
1
+ import { outcome, waitFor } from './guard-driver.mjs';
2
2
  import { createObserverTarget } from './observer-target.mjs';
3
3
 
4
4
  const pause = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
5
5
  const refused = (out) => !out.ok && /^slicc: refused:/.test(out.error?.message ?? '');
6
6
 
7
- async function until(check) {
8
- const deadline = Date.now() + 5000;
9
- do {
10
- if (await check()) return true;
11
- await pause(25);
12
- } while (Date.now() < deadline);
13
- return false;
14
- }
15
-
16
- async function loadedDocument(reader, observer, url) {
7
+ async function loadedDocument(reader, observer, url, until) {
17
8
  return until(async () => {
18
9
  const { frameTree } = await reader.send('Page.getFrameTree', {}, observer);
19
10
  if (frameTree.frame.url !== url || frameTree.frame.unreachableUrl) return false;
@@ -89,6 +80,18 @@ async function redaction({
89
80
  : 'not-supported';
90
81
  }
91
82
 
83
+ async function seed(reader, observer, loaded, initial, allowed) {
84
+ const out = await outcome(
85
+ (async () => {
86
+ await reader.send('Page.enable', {}, observer);
87
+ if (!(await loaded(initial))) return 'history seed did not commit';
88
+ await reader.send('Page.navigate', { url: allowed }, observer);
89
+ if (!(await loaded(allowed))) return 'allowed document did not commit';
90
+ })()
91
+ );
92
+ return out.ok ? out.result : `observer setup failed: ${out.error.message}`;
93
+ }
94
+
92
95
  const secrets = {
93
96
  all: (tag) => `/doc/path-secret-${tag}?query-secret=${tag}#fragment-secret-${tag}`,
94
97
  query: (tag) => `/doc?query-secret=${tag}`,
@@ -107,10 +110,13 @@ export async function protectedErrorPage({
107
110
  secret = 'all',
108
111
  discovery = true,
109
112
  notes = [],
113
+ budget = 5000,
110
114
  }) {
111
115
  const initial = `${protectedOrigin}/doc?n=${tag}-history`;
112
116
  const failedURL = `${protectedOrigin}${secrets[secret](tag)}`;
113
117
  const result = {};
118
+ const until = (check) => waitFor(check, budget);
119
+ const missing = (reason) => ({ ...result, incomplete: `protected-error-page: ${reason}` });
114
120
  if (!discovery) notes.push('eventsRedacted: not-supported:discovery');
115
121
  const start = client.events.length;
116
122
  const { targetId } = await createObserverTarget(
@@ -126,7 +132,7 @@ export async function protectedErrorPage({
126
132
  observer = sessionId;
127
133
  internalSessions.add(observer);
128
134
  const frame = async () => (await reader.send('Page.getFrameTree', {}, observer)).frameTree.frame;
129
- const loaded = (url) => loadedDocument(reader, observer, url);
135
+ const loaded = (url) => loadedDocument(reader, observer, url, until);
130
136
  let stopped = false;
131
137
  const fail = (async () => {
132
138
  while (!stopped) {
@@ -147,12 +153,8 @@ export async function protectedErrorPage({
147
153
  }
148
154
  })();
149
155
  try {
150
- await reader.send('Page.enable', {}, observer);
151
- if (!(await loaded(initial)))
152
- throw new Error('protected-error-page: history seed did not commit');
153
- await reader.send('Page.navigate', { url: `${ordinary}/blank` }, observer);
154
- if (!(await loaded(`${ordinary}/blank`)))
155
- throw new Error('protected-error-page: allowed document did not commit');
156
+ const unseeded = await seed(reader, observer, loaded, initial, `${ordinary}/blank`);
157
+ if (unseeded) return missing(unseeded);
156
158
  const attached = await outcome(
157
159
  client.send('Target.attachToTarget', { targetId, flatten: true })
158
160
  );
@@ -174,10 +176,11 @@ export async function protectedErrorPage({
174
176
  { expression: `setTimeout(()=>location.href=${JSON.stringify(failedURL)},0)` },
175
177
  session
176
178
  );
177
- result.live = await until(async () => {
179
+ let evidence;
180
+ const live = await until(async () => {
178
181
  const f = await frame();
179
182
  const { targetInfo } = await reader.send('Target.getTargetInfo', { targetId });
180
- result.evidence = { frame: f, targetInfo };
183
+ evidence = { frame: f, targetInfo };
181
184
  return (
182
185
  f.url === 'chrome-error://chromewebdata/' &&
183
186
  f.securityOrigin === '://' &&
@@ -185,11 +188,11 @@ export async function protectedErrorPage({
185
188
  targetInfo.url === failedURL
186
189
  );
187
190
  });
188
- if (!result.live && side !== 'control') return result;
189
- if (!result.live)
190
- throw new Error(
191
- `protected-error-page: missing error commit ${JSON.stringify(result.evidence)}`
192
- );
191
+ result.evidence = evidence;
192
+ if (!live && side === 'control')
193
+ return missing(`missing error commit ${JSON.stringify(evidence)}`);
194
+ result.live = live;
195
+ if (!live) return result;
193
196
  await redaction({ client, targetId, failedURL, protectedOrigin, start, result, discovery });
194
197
  const beforeReload = (await frame()).loaderId;
195
198
  result.reload = (await outcome(client.send('Page.reload', {}, session))).ok;
@@ -227,14 +230,14 @@ export async function protectedErrorPage({
227
230
  failing = false;
228
231
  const { entries, currentIndex } = await reader.send('Page.getNavigationHistory', {}, observer);
229
232
  const index = entries.findIndex((e) => e.url === initial);
230
- if (index < 0) throw new Error('protected-error-page: missing protected history');
233
+ if (index < 0) return missing('missing protected history');
231
234
  const beforeRestore = reader.events.length;
232
235
  await reader.send(
233
236
  'Runtime.evaluate',
234
237
  { expression: `setTimeout(() => history.go(${index - currentIndex}), 0)` },
235
238
  observer
236
239
  );
237
- result.restored = await until(() =>
240
+ const restored = await until(() =>
238
241
  reader.events
239
242
  .slice(beforeRestore)
240
243
  .some(
@@ -245,8 +248,8 @@ export async function protectedErrorPage({
245
248
  e.params.frame.url === initial
246
249
  )
247
250
  );
248
- if (!result.restored)
249
- throw new Error('protected-error-page: real protected BFCache restore required');
251
+ if (!restored) return missing('real protected BFCache restore required');
252
+ result.restored = true;
250
253
  await pause(100);
251
254
  if (mode === 'sticky') {
252
255
  await reader.send('Target.detachFromTarget', { sessionId: observer });
@@ -260,11 +263,9 @@ export async function protectedErrorPage({
260
263
  observer
261
264
  );
262
265
  await reader.send('Page.navigate', { url: failedURL }, observer);
263
- result.stickyError = await until(async () => (await frame()).unreachableUrl === failedURL);
264
- if (!result.stickyError)
265
- throw new Error(
266
- `protected-error-page: missing sticky error ${JSON.stringify(await frame())}`
267
- );
266
+ if (!(await until(async () => (await frame()).unreachableUrl === failedURL)))
267
+ return missing(`missing sticky error ${JSON.stringify(await frame())}`);
268
+ result.stickyError = true;
268
269
  await pause(100);
269
270
  }
270
271
  await lockChecks({ client, session, targetId, result });
@@ -1,4 +1,4 @@
1
- import { load, outcome } from './guard-driver.mjs';
1
+ import { load, outcome, waitFor } from './guard-driver.mjs';
2
2
  import { redirectFragments } from './redirect-fragments.mjs';
3
3
 
4
4
  const pause = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
@@ -47,28 +47,33 @@ const headersTextMatch = (text, to) =>
47
47
  )
48
48
  );
49
49
 
50
- export function containsSecret(value, marker) {
50
+ export function containsSecret(value, marker, seen = new Map()) {
51
51
  const visit = (value, depth = 0) => {
52
52
  if (typeof value === 'string') {
53
- if (value.includes(marker)) return true;
54
- if (depth >= 8) return false;
55
- const decoded = [];
56
- try {
57
- decoded.push(decodeURIComponent(value));
58
- } catch {}
59
- try {
60
- decoded.push(JSON.parse(value));
61
- } catch {}
62
- try {
63
- decoded.push(JSON.parse(`"${value}"`));
64
- } catch {}
65
- decoded.push(...decodedBase64(value));
66
- return decoded.some((text) => text !== value && visit(text, depth + 1));
53
+ const key = `${depth}:${value}`;
54
+ if (!seen.has(key)) seen.set(key, text(value, depth));
55
+ return seen.get(key);
67
56
  }
68
57
  if (value && typeof value === 'object')
69
58
  return Object.values(value).some((v) => visit(v, depth));
70
59
  return false;
71
60
  };
61
+ const text = (value, depth) => {
62
+ if (value.includes(marker)) return true;
63
+ if (depth >= 8) return false;
64
+ const decoded = [];
65
+ try {
66
+ decoded.push(decodeURIComponent(value));
67
+ } catch {}
68
+ try {
69
+ decoded.push(JSON.parse(value));
70
+ } catch {}
71
+ try {
72
+ decoded.push(JSON.parse(`"${value}"`));
73
+ } catch {}
74
+ decoded.push(...decodedBase64(value));
75
+ return decoded.some((text) => text !== value && visit(text, depth + 1));
76
+ };
72
77
  return visit(value);
73
78
  }
74
79
 
@@ -85,6 +90,7 @@ class RedirectProbe {
85
90
  this.jobs = new Set();
86
91
  this.networkBodies = new Set();
87
92
  this.registrations = [];
93
+ this.seen = new Map();
88
94
  this.protectedURLs = [
89
95
  `https://seven.sliccy.ai/auth/callback?code=${encodeURIComponent(this.marker)}&state=${encodeURIComponent(this.marker)}`,
90
96
  `https://vault.sliccy.ai/oauth/mcp-callback?code=${encodeURIComponent(this.marker)}`,
@@ -158,7 +164,23 @@ class RedirectProbe {
158
164
  await load(this.reader, this.observer, `${this.ordinary}/doc?n=${this.tag}-${suffix}`);
159
165
  }
160
166
 
161
- async setup() {
167
+ contains(value) {
168
+ return containsSecret(value, this.marker, this.seen);
169
+ }
170
+
171
+ async scan(items) {
172
+ const flags = [];
173
+ let slice = Date.now();
174
+ for (const item of items) {
175
+ flags.push(this.contains(item));
176
+ if (Date.now() - slice < 10) continue;
177
+ await pause(0);
178
+ slice = Date.now();
179
+ }
180
+ return flags;
181
+ }
182
+
183
+ async observe() {
162
184
  this.observer = (
163
185
  await this.reader.send('Target.attachToTarget', { targetId: this.targetId, flatten: true })
164
186
  ).sessionId;
@@ -176,6 +198,9 @@ class RedirectProbe {
176
198
  },
177
199
  ],
178
200
  });
201
+ }
202
+
203
+ async setup() {
179
204
  for (const domain of ['Page', 'Network', 'Runtime', 'Log', 'Audits'])
180
205
  await this.send(`${domain}.enable`);
181
206
  await this.send('Fetch.enable', {
@@ -249,9 +274,9 @@ class RedirectProbe {
249
274
  resourceContentAttempted: true,
250
275
  snapshotAttempted: true,
251
276
  domSnapshotAttempted: true,
252
- ordinaryResource: containsSecret(resource, this.marker),
253
- ordinarySnapshot: containsSecret(snapshot, this.marker),
254
- ordinaryDOM: containsSecret(dom, this.marker),
277
+ ordinaryResource: this.contains(resource),
278
+ ordinarySnapshot: this.contains(snapshot),
279
+ ordinaryDOM: this.contains(dom),
255
280
  });
256
281
  }
257
282
 
@@ -262,9 +287,8 @@ class RedirectProbe {
262
287
  await this.send('Runtime.evaluate', {
263
288
  expression: `fetch(${JSON.stringify(this.holdURL)}).catch(()=>{});void 0`,
264
289
  });
265
- const deadline = Date.now() + 5000;
266
- while (!this.held && Date.now() < deadline) await pause(10);
267
- if (!this.held) throw new Error('redirect body: Fetch override pause missing');
290
+ if (!(await waitFor(() => Boolean(this.held), 5000, 10)))
291
+ throw new Error('redirect body: Fetch override pause missing');
268
292
  const requestId = this.held.params.requestId;
269
293
  const out = await this.send('Fetch.continueRequest', { requestId, url });
270
294
  this.responses.push({
@@ -330,8 +354,8 @@ class RedirectProbe {
330
354
  const dom = await this.send('DOMSnapshot.captureSnapshot', { computedStyles: [] });
331
355
  this.responses.push({
332
356
  iframeSnapshotAttempted: true,
333
- ordinaryIframeSnapshot: containsSecret(snapshot, this.marker),
334
- ordinaryIframeDOM: containsSecret(dom, this.marker),
357
+ ordinaryIframeSnapshot: this.contains(snapshot),
358
+ ordinaryIframeDOM: this.contains(dom),
335
359
  });
336
360
  }
337
361
 
@@ -345,11 +369,13 @@ class RedirectProbe {
345
369
  });
346
370
  const events = this.client.events.slice(this.eventsStart);
347
371
  const messages = this.client.messages?.slice(this.messagesStart) ?? [];
372
+ const eventLeaks = await this.scan(events);
348
373
  const ordinaryEvents = events.filter(
349
- (event) =>
350
- containsSecret(event, this.marker) &&
351
- JSON.stringify(event).includes(new URL(this.plainURL).host)
374
+ (event, index) =>
375
+ eventLeaks[index] && JSON.stringify(event).includes(new URL(this.plainURL).host)
352
376
  );
377
+ const replies = [...messages, ...this.responses];
378
+ const replyLeaks = await this.scan(replies);
353
379
  const extra = events.filter(
354
380
  (event) =>
355
381
  event.sessionId === this.session &&
@@ -377,14 +403,14 @@ class RedirectProbe {
377
403
  entry.url.includes(`/redirect-secret/${this.tag}`)
378
404
  ),
379
405
  rawRecorded: this.messagesStart !== undefined && messages.length > events.length,
380
- leakChannels: [...messages, ...this.responses]
381
- .filter((message) => containsSecret(message, this.marker))
406
+ leakChannels: replies
407
+ .filter((_, index) => replyLeaks[index])
382
408
  .map((message) =>
383
409
  typeof message === 'string'
384
410
  ? (JSON.parse(message).method ?? 'reply')
385
411
  : (message.method ?? 'reply')
386
412
  ),
387
- secretHidden: !containsSecret([...messages, ...this.responses], this.marker),
413
+ secretHidden: !replyLeaks.includes(true),
388
414
  sessionSurvives: survives.ok && survives.result?.result?.value === 'doc',
389
415
  fetchBodyAttempted: attempted('Fetch.getResponseBody'),
390
416
  networkBodyAttempted: attempted('Network.getResponseBody'),
@@ -396,7 +422,7 @@ class RedirectProbe {
396
422
  events,
397
423
  replies: this.responses,
398
424
  protectedOrigins: this.protectedURLs.map((url) => new URL(url).origin),
399
- contains: (value) => containsSecret(value, this.marker),
425
+ contains: (value) => this.contains(value),
400
426
  }),
401
427
  redirectLocation: ordinaryEvents.some(
402
428
  (event) =>
@@ -472,6 +498,11 @@ export async function redirectSecret(options) {
472
498
  throw new Error('redirectSecret fixture registration required');
473
499
  const probe = new RedirectProbe(options);
474
500
  try {
501
+ const observed = await outcome(probe.observe());
502
+ if (!observed.ok)
503
+ return {
504
+ incomplete: `redirect-secret: observer setup failed: ${observed.error.message}`,
505
+ };
475
506
  await probe.setup();
476
507
  if (options.mode === 'body') await probe.body();
477
508
  else await probe.headers();