@ai-ecoverse/slicc-browsers 1.14.3 → 1.14.4

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
@@ -190,9 +190,9 @@ The `C9` foreign extension isolated-world family requires `extensionWorld`: a br
190
190
 
191
191
  The integration launcher loads shared-web 1.25.2’s harness extension exactly once through `Extensions.loadUnpacked` (`ankmbodmgimjemnojmeddfcdlbocolob`), whose content script runs on HTTP(S) pages. shared-web 1.25.2 also loads it by default in its own harness. A transport unable to host a foreign content-script extension omits `extensionWorld` and reports `not-supported:extensionWorld`; an advertised capability with no observed world fails, rather than silently skipping.
192
192
 
193
- `C2.redirect-secret` probes ordinary fixture 302s to seven’s `/auth/callback?code=…&state=…` and vault’s `/oauth/mcp-callback?code=…`, plus `#access_token=…` variants of both. It navigates with Page and scripted `location.href`, fetches a subresource and sends an XHR. The 302 carries `Location`, `Content-Location`, `Refresh: 0;url=…` and `Link: <…>; rel=preconnect`. Page, Network (including ExtraInfo), Runtime, Log, Audits and both Fetch stages are enabled. Every received raw message is scanned, including replies, across an eight-second tail, for raw, percent-encoded, JSON-escaped and base64 marker representations. Both response-body APIs are attempted on each hop; guarded bodies need matched refusals. Page frame trees, resource trees and navigation history are read before recovery to catch error-page leftovers. A separate structural check requires every protected `Network.Request.urlFragment` and `Page.Frame.urlFragment` to be empty or absent, even if its value contains no marker; the ordinary `#access_token` control independently requires fragment evidence in request events, frame events, frame-tree replies and resource-tree replies. The ordinary session must survive. The unguarded ordinary redirect control preserves all four headers in `redirectResponse`, ExtraInfo’s headers and `headersText`, and Fetch’s response headers. `C2.redirect-secret-mix` repeats that matrix with 301/303/307/308. The protected leg requires in-place ExtraInfo delivery with origin-only header values, not merely the absence of a marker.
193
+ `C2.redirect-secret` probes ordinary fixture 302s to seven’s `/auth/callback?code=…&state=…` and vault’s `/oauth/mcp-callback?code=…`, plus `#access_token=…` variants of both. It navigates with Page and scripted `location.href`, fetches a subresource and sends an XHR. The 302 carries `Location`, `Content-Location`, `Refresh: 0;url=…` and `Link: <…>; rel=preconnect`. Page, Network (including ExtraInfo), Runtime, Log, Audits and both Fetch stages are enabled. Every received raw message is scanned, including replies but not the probe’s own observer session traffic, across an eight-second tail, for raw, percent-encoded, JSON-escaped and base64 marker representations. Both response-body APIs are attempted on each hop; guarded bodies need matched refusals. `Network.getResponseBody` is attempted, once per id, with every network id the client learns for a 301–308 hop: the `requestId` of a Network event carrying the redirect status and the `networkId` of the Fetch response-stage pause. For each registered hop whose id the client learned neither way, the probe reads the hop’s real id from the harness’s own unguarded tap (the observer session, which enables Network on the guarded leg) and attempts the read on the guarded session with it. The evidence’s `networkBodyCarriers` counts the ids per carrier, and `networkBodyPath` names the weakest carrier any id needed: `withheld+tap`, then `fetch-networkId`, then `network`, or `none` without any id. These reads never hold up a Fetch pause, and the probe gives them five seconds after `Fetch.disable` to settle. A read still unanswered then is not a refusal. `networkBodyAttempted` and `networkBodyRefused` hold only when every attempt is refused. On the guarded leg a `fetch-networkId` or `withheld+tap` carrier also needs the same row’s unguarded control to have seen the redirect’s Network events. Without that proof, or with no id at all, both read `incomplete` and the probe returns `redirect-secret: network body leg <reason>`; an attempt that is not refused is measured and fails. Every `Network.requestWillBeSent` the client sees for a registered redirect URL must end within the step in a redirect `requestWillBeSent`, `loadingFinished` or `loadingFailed` for that `requestId`. The evidence’s `hops` records per `requestId` the `delivery` (`redacted` for an origin-only protected redirect in place, `withheld` for a terminal without the redirect, `ordinary`, `raw` or `unterminated`) and the `terminal` event method. `requestsTerminated` is false on any `unterminated` request, a measured guard failure rather than missing evidence; `hopsRedacted` is false on any `raw` or `ordinary` hop, so a delivered protected hop must be origin-only, never the raw or a substituted URL; the guarded leg expects it. Page frame trees, resource trees and navigation history are read before recovery to catch error-page leftovers. A separate structural check requires every protected `Network.Request.urlFragment` and `Page.Frame.urlFragment` to be empty or absent, even if its value contains no marker; the ordinary `#access_token` control independently requires fragment evidence in request events, frame events, frame-tree replies and resource-tree replies. The ordinary session must survive. The unguarded ordinary redirect control preserves all four headers in `redirectResponse`, ExtraInfo’s headers and `headersText`, and Fetch’s response headers. `C2.redirect-secret-mix` repeats that matrix with 301/303/307/308. The protected leg requires in-place ExtraInfo delivery with origin-only header values, not merely the absence of a marker.
194
194
 
195
- `C2.redirect-body` calls `Network.loadNetworkResource` on an ordinary redirect with `includeCredentials: true`, reads any stream with `IO.read`, replays a redirected XHR, requests Page resource content, and captures Page/MHTML and DOM snapshots after a main-frame redirect. It also captures an ordinary parent containing an iframe redirected to vault, including its error document, and overrides a paused ordinary Fetch request to the allowed redirect URL. No protected marker may appear in any message or body, and no request from those operations may send the independently seeded seven cookie. The ordinary control must return the body, resource content and snapshots, and accept the URL override. Each probe cleans up its redirect registrations, observer session and cookie. The focused Chrome tests use HTTPS with SPKI-pinned fixture trust and default-port protected URLs; host resolver rules route seven and vault to the fixture rather than the public service.
195
+ `C2.redirect-body` calls `Network.loadNetworkResource` on an ordinary redirect with `includeCredentials: true`, reads any stream with `IO.read`, replays a redirected XHR, requests Page resource content, and captures Page/MHTML and DOM snapshots after a main-frame redirect. It also captures an ordinary parent containing an iframe redirected to vault, including its error document, and overrides a paused ordinary Fetch request to the allowed redirect URL. No protected marker may appear in any message or body, and no request from those operations may send the independently seeded seven cookie. The ordinary control must return the body, resource content and snapshots, and accept the URL override. Its network-body carrier, `networkBodyAttempted`, `networkBodyRefused`, `hops`, `requestsTerminated` and `hopsRedacted` follow `C2.redirect-secret`. Each probe cleans up its redirect registrations, observer session and cookie. The focused Chrome tests use HTTPS with SPKI-pinned fixture trust and default-port protected URLs; host resolver rules route seven and vault to the fixture rather than the public service.
196
196
 
197
197
  `C8.unparseable` probes both raw and canonical space/U+200D/invalid-punycode URLs through creation and navigation. Every guarded destination needs a matched refusal. Its unguarded controls record `document` or `native-rejection` outcomes, including Chrome builds that reject raw U+200D instead of committing it. Both raw and encoded-space forms must commit real fixture documents, because that is the proven cookie leak. An independent observer loads the encoded-space document and confirms it shares a uniquely seeded `.sliccy.ai` cookie; the guarded client must neither list nor attach to it, read its cookie, query that cookie by URL, nor cause a cookie-bearing destination request. The guard does not erase the observer’s private browser state. The row runs on both browser-session and page-session-only transports, uses the default context, and cleans up its own targets and cookie. Cookie seeding always uses the unguarded observer’s browser session, never the guarded client. Only guarded browser-session target listing requires `browserSession`; without it, `listed` records `not-supported` explicitly in probe and observation evidence, and the result lists that field in `notSupported`. Creation, navigation, attachment and `Network.getCookies` with `urls` still require matched refusals; filtering cookies without refusing fails `cookieRefused`. Fixture URLs omit the colon when the port is empty, and reported URLs are compared after canonicalisation, including Chrome’s WHATWG-unparseable encoded-space hosts. Launchers must resolve the raw/canonical space, U+200D/punycode and invalid-punycode `.sliccy.ai` fixture hosts to the fixture server, including for default-port HTTP or HTTPS origins. The HTTP fixture accepts Chrome’s encoded-space Host header without reparsing it as a WHATWG origin.
198
198
 
package/guard-runner.mjs CHANGED
@@ -703,6 +703,7 @@ class Execution {
703
703
  fixture: this.options.fixture,
704
704
  mode: step.redirectSecret,
705
705
  side: this.side,
706
+ controls: (this.clients.redirectSecretControls ??= {}),
706
707
  })
707
708
  );
708
709
  } finally {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "setVersion": "1.14.3",
3
+ "setVersion": "1.14.4",
4
4
  "sources": [
5
5
  {
6
6
  "repo": "ai-ecoverse/slicc-cli",
@@ -309,6 +309,42 @@
309
309
  "equals": true,
310
310
  "observer": "client"
311
311
  }
312
+ },
313
+ "requestsTerminated": {
314
+ "means": "redirect secret: every seen redirect request ends, no unterminated request",
315
+ "check": {
316
+ "result": "redirect",
317
+ "path": "result.requestsTerminated",
318
+ "equals": true,
319
+ "observer": "client"
320
+ }
321
+ },
322
+ "hopsRedacted": {
323
+ "means": "redirect secret: every protected hop is origin-only, never the raw URL",
324
+ "check": {
325
+ "result": "redirect",
326
+ "path": "result.hopsRedacted",
327
+ "equals": true,
328
+ "observer": "client"
329
+ }
330
+ },
331
+ "networkBodyAttempted": {
332
+ "means": "redirect secret: networkBodyAttempted",
333
+ "check": {
334
+ "result": "redirect",
335
+ "path": "result.networkBodyAttempted",
336
+ "equals": true,
337
+ "observer": "client"
338
+ }
339
+ },
340
+ "networkBodyRefused": {
341
+ "means": "redirect secret: networkBodyRefused",
342
+ "check": {
343
+ "result": "redirect",
344
+ "path": "result.networkBodyRefused",
345
+ "equals": true,
346
+ "observer": "client"
347
+ }
312
348
  }
313
349
  },
314
350
  "live": "the fixture served the registered ordinary redirect",
@@ -337,7 +373,9 @@
337
373
  "resourceContentAttempted": true,
338
374
  "snapshotAttempted": true,
339
375
  "domSnapshotAttempted": true,
340
- "overridesWorked": true
376
+ "overridesWorked": true,
377
+ "requestsTerminated": true,
378
+ "networkBodyAttempted": true
341
379
  }
342
380
  },
343
381
  "expected": {
@@ -352,7 +390,11 @@
352
390
  "snapshotAttempted": true,
353
391
  "domSnapshotAttempted": true,
354
392
  "overridesRefused": true,
355
- "iframeSnapshotAttempted": true
393
+ "iframeSnapshotAttempted": true,
394
+ "requestsTerminated": true,
395
+ "hopsRedacted": true,
396
+ "networkBodyAttempted": true,
397
+ "networkBodyRefused": true
356
398
  }
357
399
  },
358
400
  {
@@ -518,6 +560,24 @@
518
560
  "equals": true,
519
561
  "observer": "client"
520
562
  }
563
+ },
564
+ "requestsTerminated": {
565
+ "means": "redirect secret: every seen redirect request ends, no unterminated request",
566
+ "check": {
567
+ "result": "redirect",
568
+ "path": "result.requestsTerminated",
569
+ "equals": true,
570
+ "observer": "client"
571
+ }
572
+ },
573
+ "hopsRedacted": {
574
+ "means": "redirect secret: every protected hop is origin-only, never the raw URL",
575
+ "check": {
576
+ "result": "redirect",
577
+ "path": "result.hopsRedacted",
578
+ "equals": true,
579
+ "observer": "client"
580
+ }
521
581
  }
522
582
  },
523
583
  "live": "the fixture served the registered ordinary redirect",
@@ -543,7 +603,8 @@
543
603
  "networkFragmentVisible": true,
544
604
  "frameFragmentVisible": true,
545
605
  "frameTreeFragmentVisible": true,
546
- "resourceTreeFragmentVisible": true
606
+ "resourceTreeFragmentVisible": true,
607
+ "requestsTerminated": true
547
608
  }
548
609
  },
549
610
  "expected": {
@@ -556,7 +617,9 @@
556
617
  "fetchBodyRefused": true,
557
618
  "networkBodyRefused": true,
558
619
  "extraLocation": true,
559
- "fragmentsHidden": true
620
+ "fragmentsHidden": true,
621
+ "requestsTerminated": true,
622
+ "hopsRedacted": true
560
623
  }
561
624
  },
562
625
  {
@@ -722,6 +785,24 @@
722
785
  "equals": true,
723
786
  "observer": "client"
724
787
  }
788
+ },
789
+ "requestsTerminated": {
790
+ "means": "redirect secret: every seen redirect request ends, no unterminated request",
791
+ "check": {
792
+ "result": "redirect",
793
+ "path": "result.requestsTerminated",
794
+ "equals": true,
795
+ "observer": "client"
796
+ }
797
+ },
798
+ "hopsRedacted": {
799
+ "means": "redirect secret: every protected hop is origin-only, never the raw URL",
800
+ "check": {
801
+ "result": "redirect",
802
+ "path": "result.hopsRedacted",
803
+ "equals": true,
804
+ "observer": "client"
805
+ }
725
806
  }
726
807
  },
727
808
  "live": "the fixture served the registered ordinary redirect",
@@ -747,7 +828,8 @@
747
828
  "networkFragmentVisible": true,
748
829
  "frameFragmentVisible": true,
749
830
  "frameTreeFragmentVisible": true,
750
- "resourceTreeFragmentVisible": true
831
+ "resourceTreeFragmentVisible": true,
832
+ "requestsTerminated": true
751
833
  }
752
834
  },
753
835
  "expected": {
@@ -760,7 +842,9 @@
760
842
  "fetchBodyRefused": true,
761
843
  "networkBodyRefused": true,
762
844
  "extraLocation": true,
763
- "fragmentsHidden": true
845
+ "fragmentsHidden": true,
846
+ "requestsTerminated": true,
847
+ "hopsRedacted": true
764
848
  }
765
849
  },
766
850
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-ecoverse/slicc-browsers",
3
- "version": "1.14.3",
3
+ "version": "1.14.4",
4
4
  "description": "SLICC's shared browser catalog, JSON schemas and discovery conformance corpus",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -66,7 +66,7 @@
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 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",
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/guard-redirect-carrier.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",
@@ -89,6 +89,7 @@ class RedirectProbe {
89
89
  this.attempts = [];
90
90
  this.jobs = new Set();
91
91
  this.networkBodies = new Set();
92
+ this.learned = { network: new Set(), fetch: new Set(), tap: new Set() };
92
93
  this.registrations = [];
93
94
  this.seen = new Map();
94
95
  this.protectedURLs = [
@@ -97,6 +98,7 @@ class RedirectProbe {
97
98
  `https://seven.sliccy.ai/auth/callback#access_token=${encodeURIComponent(this.marker)}&state=${encodeURIComponent(this.marker)}`,
98
99
  `https://vault.sliccy.ai/oauth/mcp-callback#access_token=${encodeURIComponent(this.marker)}`,
99
100
  ];
101
+ this.prefix = new URL(`${this.ordinary}/redirect-secret/${this.tag}-`).href;
100
102
  this.plainURL = `${this.ordinary}/redirect-secret-body/${this.tag}-plain?code=${encodeURIComponent(this.marker)}`;
101
103
  this.destinations =
102
104
  this.side === 'control'
@@ -119,6 +121,7 @@ class RedirectProbe {
119
121
  if (redirects.includes(event.params.responseStatusCode)) {
120
122
  const out = await this.send('Fetch.getResponseBody', { requestId: event.params.requestId });
121
123
  this.attempts.push({ method: 'Fetch.getResponseBody', ...out });
124
+ if (event.params.networkId) this.track(this.networkBody(event.params.networkId, 'fetch'));
122
125
  }
123
126
  if (
124
127
  this.holdURL &&
@@ -139,19 +142,130 @@ class RedirectProbe {
139
142
  if (event.method === 'Fetch.requestPaused') this.track(this.fetchPause(event));
140
143
  const response = event.params?.redirectResponse ?? event.params?.response;
141
144
  if (!response || !redirects.includes(response.status)) continue;
142
- const requestId = event.params.requestId;
143
- if (this.networkBodies.has(requestId)) continue;
144
- this.networkBodies.add(requestId);
145
- this.track(
146
- this.send('Network.getResponseBody', { requestId }).then((out) => {
147
- this.attempts.push({ method: 'Network.getResponseBody', ...out });
148
- })
149
- );
145
+ this.track(this.networkBody(event.params.requestId, 'network'));
150
146
  }
151
147
  await pause(5);
152
148
  }
153
149
  }
154
150
 
151
+ async networkBody(requestId, source) {
152
+ this.learned[source].add(requestId);
153
+ if (this.networkBodies.has(requestId)) return;
154
+ this.networkBodies.add(requestId);
155
+ const entry = { method: 'Network.getResponseBody', source, pending: true };
156
+ this.attempts.push(entry);
157
+ const out = await this.send('Network.getResponseBody', { requestId });
158
+ delete entry.pending;
159
+ Object.assign(entry, out);
160
+ }
161
+
162
+ async settle() {
163
+ for (let i = 0; i < 500 && this.jobs.size; i++) await pause(10);
164
+ }
165
+
166
+ tap() {
167
+ for (const event of this.reader.events ?? []) {
168
+ if (event.sessionId !== this.observer) continue;
169
+ const response = event.params?.redirectResponse ?? event.params?.response;
170
+ if (response?.url?.startsWith(this.prefix) && redirects.includes(response.status))
171
+ this.track(this.networkBody(event.params.requestId, 'tap'));
172
+ }
173
+ }
174
+
175
+ networkLeg() {
176
+ const carriers = [...this.networkBodies].map((id) =>
177
+ this.learned.network.has(id)
178
+ ? 'network'
179
+ : this.learned.fetch.has(id)
180
+ ? 'fetch-networkId'
181
+ : 'withheld+tap'
182
+ );
183
+ const path =
184
+ ['withheld+tap', 'fetch-networkId', 'network'].find((carrier) =>
185
+ carriers.includes(carrier)
186
+ ) ?? 'none';
187
+ const networkBodyCarriers = Object.fromEntries(
188
+ [...new Set(carriers)].map((carrier) => [
189
+ carrier,
190
+ carriers.filter((entry) => entry === carrier).length,
191
+ ])
192
+ );
193
+ const attempts = this.attempts.filter((entry) => entry.method === 'Network.getResponseBody');
194
+ const refusedAll = attempts.length > 0 && attempts.every(refused);
195
+ const proven = path === 'network' || this.side !== 'guarded' || this.controls?.networkEvents;
196
+ const reason =
197
+ path === 'none'
198
+ ? 'no network id for the redirect'
199
+ : proven
200
+ ? undefined
201
+ : 'control did not prove the redirect Network events';
202
+ if (reason && (!attempts.length || refusedAll))
203
+ return {
204
+ networkBodyPath: path,
205
+ networkBodyCarriers,
206
+ networkBodyAttempted: 'incomplete',
207
+ networkBodyRefused: 'incomplete',
208
+ incomplete: `redirect-secret: network body leg ${reason}`,
209
+ };
210
+ return {
211
+ networkBodyPath: path,
212
+ networkBodyCarriers,
213
+ networkBodyAttempted: attempts.length > 0,
214
+ networkBodyRefused: refusedAll,
215
+ };
216
+ }
217
+
218
+ hops(events) {
219
+ const own = events.filter((event) => event.sessionId === this.session);
220
+ const seen = new Set(
221
+ own
222
+ .filter(
223
+ (event) =>
224
+ event.method === 'Network.requestWillBeSent' &&
225
+ !event.params.redirectResponse &&
226
+ event.params.request?.url?.startsWith(this.prefix)
227
+ )
228
+ .map((event) => event.params.requestId)
229
+ );
230
+ const protectedOrigins = this.protectedURLs.map((url) => new URL(url).origin);
231
+ const hops = Object.fromEntries(
232
+ [...seen].map((requestId) => {
233
+ const mine = own.filter((event) => event.params?.requestId === requestId);
234
+ const hop = mine.find(
235
+ (event) =>
236
+ event.method === 'Network.requestWillBeSent' &&
237
+ event.params.redirectResponse?.url?.startsWith(this.prefix)
238
+ );
239
+ const end = mine.find((event) =>
240
+ ['Network.loadingFinished', 'Network.loadingFailed'].includes(event.method)
241
+ );
242
+ const url = hop?.params.request?.url ?? '';
243
+ const origin = protectedOrigins.find((value) => url.startsWith(value));
244
+ return [
245
+ requestId,
246
+ {
247
+ delivery: !hop
248
+ ? end
249
+ ? 'withheld'
250
+ : 'unterminated'
251
+ : !origin
252
+ ? 'ordinary'
253
+ : [origin, `${origin}/`].includes(url)
254
+ ? 'redacted'
255
+ : 'raw',
256
+ terminal: (hop ?? end)?.method ?? null,
257
+ },
258
+ ];
259
+ })
260
+ );
261
+ const deliveries = Object.values(hops).map((entry) => entry.delivery);
262
+ return {
263
+ hops,
264
+ requestsTerminated: !deliveries.includes('unterminated'),
265
+ hopsRedacted: !deliveries.some((delivery) => ['raw', 'ordinary'].includes(delivery)),
266
+ };
267
+ }
268
+
155
269
  async register(key, to, code = 302) {
156
270
  const url = `${this.ordinary}/redirect-secret/${this.tag}-${key}`;
157
271
  const path = new URL(url).pathname;
@@ -186,6 +300,7 @@ class RedirectProbe {
186
300
  ).sessionId;
187
301
  this.watching = this.watch();
188
302
  await this.reader.send('Page.enable', {}, this.observer);
303
+ if (this.side === 'guarded') await this.reader.send('Network.enable', {}, this.observer);
189
304
  await this.reader.send('Storage.setCookies', {
190
305
  cookies: [
191
306
  {
@@ -361,14 +476,23 @@ class RedirectProbe {
361
476
 
362
477
  async evidence() {
363
478
  await pause(8000);
479
+ this.tap();
480
+ if (this.side === 'control' && this.controls)
481
+ this.controls.networkEvents = this.learned.network.size > 0;
364
482
  await this.send('Fetch.disable');
365
483
  await this.reset('survive');
366
484
  const survives = await this.send('Runtime.evaluate', {
367
485
  expression: 'document.title',
368
486
  returnByValue: true,
369
487
  });
370
- const events = this.client.events.slice(this.eventsStart);
371
- const messages = this.client.messages?.slice(this.messagesStart) ?? [];
488
+ await this.settle();
489
+ const tapped = `"sessionId":"${this.observer}"`;
490
+ const events = this.client.events
491
+ .slice(this.eventsStart)
492
+ .filter((event) => event.sessionId !== this.observer);
493
+ const messages = (this.client.messages?.slice(this.messagesStart) ?? []).filter(
494
+ (message) => !message.includes(tapped)
495
+ );
372
496
  const eventLeaks = await this.scan(events);
373
497
  const ordinaryEvents = events.filter(
374
498
  (event, index) =>
@@ -394,10 +518,7 @@ class RedirectProbe {
394
518
  );
395
519
  });
396
520
  const found = (key) => this.responses.some((entry) => entry[key]);
397
- const attempted = (method) => this.attempts.some((entry) => entry.method === method);
398
- const bodyRefused = (method) =>
399
- this.attempts.filter((entry) => entry.method === method).length > 0 &&
400
- this.attempts.filter((entry) => entry.method === method).every(refused);
521
+ const fetchAttempts = this.attempts.filter((entry) => entry.method === 'Fetch.getResponseBody');
401
522
  return {
402
523
  live: (await this.fixture.log()).some((entry) =>
403
524
  entry.url.includes(`/redirect-secret/${this.tag}`)
@@ -412,10 +533,10 @@ class RedirectProbe {
412
533
  ),
413
534
  secretHidden: !replyLeaks.includes(true),
414
535
  sessionSurvives: survives.ok && survives.result?.result?.value === 'doc',
415
- fetchBodyAttempted: attempted('Fetch.getResponseBody'),
416
- networkBodyAttempted: attempted('Network.getResponseBody'),
417
- fetchBodyRefused: bodyRefused('Fetch.getResponseBody'),
418
- networkBodyRefused: bodyRefused('Network.getResponseBody'),
536
+ fetchBodyAttempted: fetchAttempts.length > 0,
537
+ fetchBodyRefused: fetchAttempts.length > 0 && fetchAttempts.every(refused),
538
+ ...this.networkLeg(),
539
+ ...this.hops(events),
419
540
  extraInPlace,
420
541
  ...redirectFragments({
421
542
  messages,
@@ -476,7 +597,7 @@ class RedirectProbe {
476
597
  async close() {
477
598
  this.stopped = true;
478
599
  await this.watching;
479
- await Promise.allSettled([...this.jobs]);
600
+ await this.settle();
480
601
  await outcome(this.client.send('Fetch.disable', {}, this.session));
481
602
  if (this.observer) {
482
603
  await outcome(