@ai-ecoverse/slicc-browsers 1.16.1 → 1.16.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/README.md +2 -0
- package/guard-vectors.json +1 -1
- package/package.json +2 -2
- package/redirect-secret.mjs +132 -59
package/README.md
CHANGED
|
@@ -197,6 +197,8 @@ The integration launcher loads shared-web 1.25.2’s harness extension exactly o
|
|
|
197
197
|
|
|
198
198
|
`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.
|
|
199
199
|
|
|
200
|
+
Every C2 redirect probe reports `sessionEnded`. It becomes true when the guarded client’s session ends during the probe: a `Target.detachedFromTarget` for that session, or a CDP reply that the session is closed or not found, or that the target, websocket or transport is closed. `Inspected target navigated or closed` is an ordinary reply on a live session, not a session end. When an expected Fetch pause or XHR event is missing, the probe sends one `Runtime.evaluate` to check that the session is still alive. Once the session has ended, the probe sends nothing more on it, skips the eight-second tail and the termination wait, and evaluates what the client received before the end. `secretHidden`, `protectedBodyHidden`, `cookieHidden`, `fragmentsHidden` and `hopsRedacted` are measured on those messages, replies, bodies and fixture logs, so a guard that leaked before the end still fails them. A clean end passes them. `sessionSurvives` reads false. Checks that need evidence from after the end read `incomplete`, with `redirect-secret: session ended (<how>)` in the probe’s `incomplete`. If the end cuts the probe’s action matrix short, or leaves any body read pending or unanswered, then `fetchBodyAttempted`, `fetchBodyRefused` and the network-body leg read `incomplete`, even after some refused reads. Any answer that isn’t a refusal still fails its refusal check. `requestsTerminated` reads `incomplete` when requests are unterminated or the matrix was cut short. A short but origin-only `extraLocation` reads `incomplete`. So does each attempt or override check whose command never reached the client: it wasn’t sent because the session had already ended, or its reply was the session end. On the unguarded control, a session end is an error: the probe throws `redirect-secret: control session ended (<how>)`.
|
|
201
|
+
|
|
200
202
|
`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.
|
|
201
203
|
|
|
202
204
|
`C8.chrome-search` requests `chrome-search://most-visited/` through `Page.navigate` and requires an explicit guard refusal. Its unguarded control requires a CDP response, not a successful document commit: Chrome can answer with `errorText` when that internal data source is unavailable outside an Instant process. Browser-side load failure is not a guard refusal, and merely hiding the target must fail this row.
|
package/guard-vectors.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-ecoverse/slicc-browsers",
|
|
3
|
-
"version": "1.16.
|
|
3
|
+
"version": "1.16.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",
|
|
@@ -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/release-line.test.mjs test/integration/guard-redirect-carrier.test.mjs test/integration/guard-incomplete.test.mjs test/integration/guard-stall.test.mjs test/integration/guard-forged.test.mjs test/integration/guard-exit-steps.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/release-line.test.mjs test/integration/guard-redirect-carrier.test.mjs test/integration/guard-redirect-session-end.test.mjs test/integration/guard-incomplete.test.mjs test/integration/guard-stall.test.mjs test/integration/guard-forged.test.mjs test/integration/guard-exit-steps.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",
|
package/redirect-secret.mjs
CHANGED
|
@@ -20,6 +20,12 @@ const decodedBase64 = (value) =>
|
|
|
20
20
|
base64Slices(text).map(decodeBase64)
|
|
21
21
|
);
|
|
22
22
|
const refused = (out) => !out.ok && /^slicc: refused:/.test(out.error?.message ?? '');
|
|
23
|
+
const gone = (out) =>
|
|
24
|
+
!out.ok &&
|
|
25
|
+
!refused(out) &&
|
|
26
|
+
/session with given id not found|no session with given id|\bsession (?:has been |is |was )?(?:closed|detached)|\btarget closed|target page, context or browser has been closed|\b(?:cdp|connection|transport|websocket|socket) (?:has been |is |was )?closed|websocket is not open/i.test(
|
|
27
|
+
out.error?.message ?? ''
|
|
28
|
+
);
|
|
23
29
|
const redirects = [301, 302, 303, 307, 308];
|
|
24
30
|
const redirectHeaders = (to) => ({
|
|
25
31
|
location: to,
|
|
@@ -85,6 +91,7 @@ class RedirectProbe {
|
|
|
85
91
|
this.messagesStart = this.client.messages?.length;
|
|
86
92
|
this.eventsStart = this.client.events.length;
|
|
87
93
|
this.cursor = this.eventsStart;
|
|
94
|
+
this.detachScan = this.eventsStart;
|
|
88
95
|
this.responses = [];
|
|
89
96
|
this.attempts = [];
|
|
90
97
|
this.jobs = new Set();
|
|
@@ -107,8 +114,41 @@ class RedirectProbe {
|
|
|
107
114
|
: this.protectedURLs;
|
|
108
115
|
}
|
|
109
116
|
|
|
117
|
+
ended() {
|
|
118
|
+
const events = this.client.events;
|
|
119
|
+
for (; !this.end && this.detachScan < events.length; this.detachScan++) {
|
|
120
|
+
const event = events[this.detachScan];
|
|
121
|
+
if (event.method === 'Target.detachedFromTarget' && event.params?.sessionId === this.session)
|
|
122
|
+
this.end = 'Target.detachedFromTarget';
|
|
123
|
+
}
|
|
124
|
+
return Boolean(this.end);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
cut() {
|
|
128
|
+
return Boolean(this.end) && !this.complete;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async lost() {
|
|
132
|
+
if (!this.ended()) await this.send('Runtime.evaluate', { expression: 'void 0' });
|
|
133
|
+
return this.ended();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
answers(method) {
|
|
137
|
+
const all = this.attempts.filter((entry) => entry.method === method);
|
|
138
|
+
const answered = all.filter((entry) => !(this.end && (entry.pending || entry.ended)));
|
|
139
|
+
const short =
|
|
140
|
+
Boolean(this.end) && (!this.complete || !answered.length || answered.length < all.length);
|
|
141
|
+
return { answered, short };
|
|
142
|
+
}
|
|
143
|
+
|
|
110
144
|
async send(method, params = {}) {
|
|
145
|
+
if (this.ended())
|
|
146
|
+
return { ok: false, ended: true, error: { message: `session ended (${this.end})` } };
|
|
111
147
|
const out = await outcome(this.client.send(method, params, this.session));
|
|
148
|
+
if (gone(out)) {
|
|
149
|
+
this.end ??= out.error.message;
|
|
150
|
+
return { ...out, ended: true };
|
|
151
|
+
}
|
|
112
152
|
this.responses.push({ method, ...out });
|
|
113
153
|
return out;
|
|
114
154
|
}
|
|
@@ -151,7 +191,7 @@ class RedirectProbe {
|
|
|
151
191
|
|
|
152
192
|
async networkBody(requestId, source) {
|
|
153
193
|
this.learned[source].add(requestId);
|
|
154
|
-
if (this.networkBodies.has(requestId)) return;
|
|
194
|
+
if (this.networkBodies.has(requestId) || this.ended()) return;
|
|
155
195
|
this.networkBodies.add(requestId);
|
|
156
196
|
const entry = { method: 'Network.getResponseBody', source, pending: true };
|
|
157
197
|
this.attempts.push(entry);
|
|
@@ -191,15 +231,17 @@ class RedirectProbe {
|
|
|
191
231
|
carriers.filter((entry) => entry === carrier).length,
|
|
192
232
|
])
|
|
193
233
|
);
|
|
194
|
-
const attempts = this.
|
|
234
|
+
const { answered: attempts, short } = this.answers('Network.getResponseBody');
|
|
195
235
|
const refusedAll = attempts.length > 0 && attempts.every(refused);
|
|
196
236
|
const proven = path === 'network' || this.side !== 'guarded' || this.controls?.networkEvents;
|
|
197
237
|
const reason =
|
|
198
238
|
path === 'none'
|
|
199
239
|
? 'no network id for the redirect'
|
|
200
|
-
: proven
|
|
201
|
-
?
|
|
202
|
-
:
|
|
240
|
+
: !proven
|
|
241
|
+
? 'control did not prove the redirect Network events'
|
|
242
|
+
: short
|
|
243
|
+
? `session ended (${this.end}) before every network body read was answered`
|
|
244
|
+
: undefined;
|
|
203
245
|
if (reason && (!attempts.length || refusedAll))
|
|
204
246
|
return {
|
|
205
247
|
networkBodyPath: path,
|
|
@@ -254,11 +296,12 @@ class RedirectProbe {
|
|
|
254
296
|
async land(url, start) {
|
|
255
297
|
const path = new URL(url).pathname;
|
|
256
298
|
await pause(250);
|
|
257
|
-
if (this.stalled) return;
|
|
299
|
+
if (this.stalled || this.ended()) return;
|
|
258
300
|
const landed = await waitFor(
|
|
259
301
|
async () =>
|
|
260
|
-
|
|
261
|
-
|
|
302
|
+
this.ended() ||
|
|
303
|
+
((await this.fixture.log()).some((entry) => entry.url.includes(path)) &&
|
|
304
|
+
!this.open(this.client.events.slice(start), (seen) => seen === url).length),
|
|
262
305
|
Math.round(2000 * this.scale),
|
|
263
306
|
50
|
|
264
307
|
);
|
|
@@ -267,7 +310,7 @@ class RedirectProbe {
|
|
|
267
310
|
|
|
268
311
|
async terminated() {
|
|
269
312
|
await waitFor(
|
|
270
|
-
() => !this.open(this.client.events.slice(this.eventsStart)).length,
|
|
313
|
+
() => this.ended() || !this.open(this.client.events.slice(this.eventsStart)).length,
|
|
271
314
|
Math.round(5000 * this.scale),
|
|
272
315
|
50
|
|
273
316
|
);
|
|
@@ -300,13 +343,15 @@ class RedirectProbe {
|
|
|
300
343
|
);
|
|
301
344
|
const deliveries = Object.values(hops).map((entry) => entry.delivery);
|
|
302
345
|
const open = Object.keys(hops).filter((id) => hops[id].delivery === 'unterminated');
|
|
303
|
-
const missing = open.length > 0 && this.side === 'control';
|
|
346
|
+
const missing = (open.length > 0 && (this.side === 'control' || this.end)) || this.cut();
|
|
347
|
+
const who = this.end ? `session ended (${this.end}) with requests` : 'control requests';
|
|
348
|
+
const detail = open.length
|
|
349
|
+
? `${who} ${open.join(', ')} unterminated`
|
|
350
|
+
: `session ended (${this.end}) before every request was issued`;
|
|
304
351
|
return {
|
|
305
352
|
hops,
|
|
306
353
|
requestsTerminated: missing ? 'incomplete' : !open.length,
|
|
307
|
-
...(missing
|
|
308
|
-
? { incomplete: `redirect-secret: control requests ${open.join(', ')} unterminated` }
|
|
309
|
-
: {}),
|
|
354
|
+
...(missing ? { incomplete: `redirect-secret: ${detail}` } : {}),
|
|
310
355
|
hopsRedacted: !deliveries.some((delivery) => ['raw', 'ordinary'].includes(delivery)),
|
|
311
356
|
};
|
|
312
357
|
}
|
|
@@ -388,6 +433,7 @@ class RedirectProbe {
|
|
|
388
433
|
async headers() {
|
|
389
434
|
for (const [index, to] of this.destinations.entries()) {
|
|
390
435
|
for (const kind of ['navigate', 'href', 'fetch', 'xhr']) {
|
|
436
|
+
if (this.ended()) return;
|
|
391
437
|
const code = this.mode === 'mix' ? [301, 303, 307, 308][index] : 302;
|
|
392
438
|
const url = await this.register(`${index}-${kind}`, to, code);
|
|
393
439
|
const start = this.client.events.length;
|
|
@@ -439,9 +485,9 @@ class RedirectProbe {
|
|
|
439
485
|
const snapshot = await this.send('Page.captureSnapshot', { format: 'mhtml' });
|
|
440
486
|
const dom = await this.send('DOMSnapshot.captureSnapshot', { computedStyles: [] });
|
|
441
487
|
this.responses.push({
|
|
442
|
-
resourceContentAttempted:
|
|
443
|
-
snapshotAttempted:
|
|
444
|
-
domSnapshotAttempted:
|
|
488
|
+
resourceContentAttempted: !resource.ended,
|
|
489
|
+
snapshotAttempted: !snapshot.ended,
|
|
490
|
+
domSnapshotAttempted: !dom.ended,
|
|
445
491
|
ordinaryResource: this.contains(resource),
|
|
446
492
|
ordinarySnapshot: this.contains(snapshot),
|
|
447
493
|
ordinaryDOM: this.contains(dom),
|
|
@@ -455,16 +501,21 @@ class RedirectProbe {
|
|
|
455
501
|
await this.send('Runtime.evaluate', {
|
|
456
502
|
expression: `fetch(${JSON.stringify(this.holdURL)}).catch(()=>{});void 0`,
|
|
457
503
|
});
|
|
458
|
-
|
|
504
|
+
await waitFor(() => Boolean(this.held) || this.ended(), 5000, 10);
|
|
505
|
+
if (!this.held) {
|
|
506
|
+
this.holdURL = undefined;
|
|
507
|
+
if (await this.lost()) return;
|
|
459
508
|
throw new Error('redirect body: Fetch override pause missing');
|
|
509
|
+
}
|
|
460
510
|
const requestId = this.held.params.requestId;
|
|
461
511
|
const out = await this.send('Fetch.continueRequest', { requestId, url });
|
|
512
|
+
this.holdURL = undefined;
|
|
513
|
+
if (out.ended) return;
|
|
462
514
|
this.responses.push({
|
|
463
515
|
overrideMethod: 'Fetch.continueRequest',
|
|
464
516
|
overrideRefused: refused(out),
|
|
465
517
|
overrideWorked: out.ok,
|
|
466
518
|
});
|
|
467
|
-
this.holdURL = undefined;
|
|
468
519
|
if (!out.ok) await this.send('Fetch.continueRequest', { requestId });
|
|
469
520
|
await pause(500);
|
|
470
521
|
}
|
|
@@ -475,6 +526,15 @@ class RedirectProbe {
|
|
|
475
526
|
const tree = await this.send('Page.getFrameTree');
|
|
476
527
|
const frameId = tree.result?.frameTree.frame.id;
|
|
477
528
|
const start = (await this.fixture.log()).length;
|
|
529
|
+
await this.reads(url, to, frameId);
|
|
530
|
+
this.responses.push({
|
|
531
|
+
cookieSent: (await this.fixture.log())
|
|
532
|
+
.slice(start)
|
|
533
|
+
.some((entry) => entry.headers.cookie?.includes(`${this.cookie}=`)),
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
async reads(url, to, frameId) {
|
|
478
538
|
const resource = await this.send('Network.loadNetworkResource', {
|
|
479
539
|
frameId,
|
|
480
540
|
url,
|
|
@@ -496,18 +556,21 @@ class RedirectProbe {
|
|
|
496
556
|
event.params.type === 'XHR' &&
|
|
497
557
|
event.params.request.url === url
|
|
498
558
|
);
|
|
499
|
-
if (!xhr)
|
|
500
|
-
|
|
501
|
-
|
|
559
|
+
if (!xhr) {
|
|
560
|
+
if (await this.lost()) return;
|
|
561
|
+
throw new Error('redirect body: XHR request was not observed');
|
|
562
|
+
}
|
|
563
|
+
const replay = await this.send('Network.replayXHR', { requestId: xhr.params.requestId });
|
|
564
|
+
this.responses.push({ replayAttempted: !replay.ended });
|
|
502
565
|
await pause(500);
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
}
|
|
566
|
+
for (const step of [
|
|
567
|
+
() => this.snapshots(url, to, frameId),
|
|
568
|
+
() => this.iframe(),
|
|
569
|
+
() => this.override(url),
|
|
570
|
+
]) {
|
|
571
|
+
if (this.ended()) return;
|
|
572
|
+
await step();
|
|
573
|
+
}
|
|
511
574
|
}
|
|
512
575
|
|
|
513
576
|
async iframe() {
|
|
@@ -521,15 +584,15 @@ class RedirectProbe {
|
|
|
521
584
|
const snapshot = await this.send('Page.captureSnapshot', { format: 'mhtml' });
|
|
522
585
|
const dom = await this.send('DOMSnapshot.captureSnapshot', { computedStyles: [] });
|
|
523
586
|
this.responses.push({
|
|
524
|
-
iframeSnapshotAttempted:
|
|
587
|
+
iframeSnapshotAttempted: !snapshot.ended && !dom.ended,
|
|
525
588
|
ordinaryIframeSnapshot: this.contains(snapshot),
|
|
526
589
|
ordinaryIframeDOM: this.contains(dom),
|
|
527
590
|
});
|
|
528
591
|
}
|
|
529
592
|
|
|
530
593
|
async evidence() {
|
|
531
|
-
await pause(8000);
|
|
532
|
-
this.tap();
|
|
594
|
+
if (!this.ended()) await pause(8000);
|
|
595
|
+
if (!this.ended()) this.tap();
|
|
533
596
|
if (this.side === 'control' && this.controls)
|
|
534
597
|
this.controls.networkEvents = this.learned.network.size > 0;
|
|
535
598
|
await this.send('Fetch.disable');
|
|
@@ -560,20 +623,24 @@ class RedirectProbe {
|
|
|
560
623
|
event.method === 'Network.responseReceivedExtraInfo' &&
|
|
561
624
|
redirects.includes(event.params.statusCode)
|
|
562
625
|
);
|
|
626
|
+
const extraHeld = extra.every((event) => {
|
|
627
|
+
const headers = event.params.headers;
|
|
628
|
+
const origin = headers.location ?? headers.Location;
|
|
629
|
+
return (
|
|
630
|
+
this.protectedURLs.some((url) => new URL(url).origin === origin) &&
|
|
631
|
+
headersMatch(headers, origin) &&
|
|
632
|
+
headersTextMatch(event.params.headersText, origin)
|
|
633
|
+
);
|
|
634
|
+
});
|
|
563
635
|
const extraInPlace =
|
|
564
|
-
|
|
565
|
-
extra.
|
|
566
|
-
|
|
567
|
-
const origin = headers.location ?? headers.Location;
|
|
568
|
-
return (
|
|
569
|
-
this.protectedURLs.some((url) => new URL(url).origin === origin) &&
|
|
570
|
-
headersMatch(headers, origin) &&
|
|
571
|
-
headersTextMatch(event.params.headersText, origin)
|
|
572
|
-
);
|
|
573
|
-
});
|
|
636
|
+
extraHeld &&
|
|
637
|
+
(extra.length >= this.destinations.length * 4 || (this.end ? 'incomplete' : false));
|
|
638
|
+
const unseen = this.end ? 'incomplete' : false;
|
|
574
639
|
const found = (key) => this.responses.some((entry) => entry[key]);
|
|
575
|
-
const
|
|
576
|
-
|
|
640
|
+
const fetchReads = this.answers('Fetch.getResponseBody');
|
|
641
|
+
const fetchLeaked = fetchReads.answered.some((entry) => !refused(entry));
|
|
642
|
+
const overrides = this.responses.filter((entry) => entry.overrideMethod);
|
|
643
|
+
const evidence = {
|
|
577
644
|
live: (await this.fixture.log()).some((entry) =>
|
|
578
645
|
entry.url.includes(`/redirect-secret/${this.tag}`)
|
|
579
646
|
),
|
|
@@ -587,8 +654,13 @@ class RedirectProbe {
|
|
|
587
654
|
),
|
|
588
655
|
secretHidden: !replyLeaks.includes(true),
|
|
589
656
|
sessionSurvives: survives.ok && survives.result?.result?.value === 'doc',
|
|
590
|
-
|
|
591
|
-
|
|
657
|
+
sessionEnded: this.ended(),
|
|
658
|
+
fetchBodyAttempted: fetchReads.short ? 'incomplete' : fetchReads.answered.length > 0,
|
|
659
|
+
fetchBodyRefused: fetchLeaked
|
|
660
|
+
? false
|
|
661
|
+
: fetchReads.short
|
|
662
|
+
? 'incomplete'
|
|
663
|
+
: fetchReads.answered.length > 0,
|
|
592
664
|
...this.legs(events),
|
|
593
665
|
extraInPlace,
|
|
594
666
|
...redirectFragments({
|
|
@@ -632,26 +704,23 @@ class RedirectProbe {
|
|
|
632
704
|
'iframeSnapshotAttempted',
|
|
633
705
|
'ordinaryIframeSnapshot',
|
|
634
706
|
'ordinaryIframeDOM',
|
|
635
|
-
].map((key) => [key, found(key)])
|
|
707
|
+
].map((key) => [key, found(key) || unseen])
|
|
636
708
|
),
|
|
637
|
-
overridesRefused:
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
.every((entry) => entry.overrideRefused),
|
|
642
|
-
overridesWorked:
|
|
643
|
-
this.responses.some((entry) => entry.overrideMethod) &&
|
|
644
|
-
this.responses
|
|
645
|
-
.filter((entry) => entry.overrideMethod)
|
|
646
|
-
.every((entry) => entry.overrideWorked),
|
|
709
|
+
overridesRefused: overrides.length
|
|
710
|
+
? overrides.every((entry) => entry.overrideRefused)
|
|
711
|
+
: unseen,
|
|
712
|
+
overridesWorked: overrides.length ? overrides.every((entry) => entry.overrideWorked) : unseen,
|
|
647
713
|
};
|
|
714
|
+
if (!this.end || !Object.values(evidence).includes('incomplete')) return evidence;
|
|
715
|
+
const incomplete = [evidence.incomplete, `redirect-secret: session ended (${this.end})`];
|
|
716
|
+
return { ...evidence, incomplete: incomplete.filter(Boolean).join('; ') };
|
|
648
717
|
}
|
|
649
718
|
|
|
650
719
|
async close() {
|
|
651
720
|
this.stopped = true;
|
|
652
721
|
await this.watching;
|
|
653
722
|
await this.settle();
|
|
654
|
-
await outcome(this.client.send('Fetch.disable', {}, this.session));
|
|
723
|
+
if (!this.ended()) await outcome(this.client.send('Fetch.disable', {}, this.session));
|
|
655
724
|
if (this.observer) {
|
|
656
725
|
await outcome(
|
|
657
726
|
this.reader.send(
|
|
@@ -680,7 +749,11 @@ export async function redirectSecret(options) {
|
|
|
680
749
|
await probe.setup();
|
|
681
750
|
if (options.mode === 'body') await probe.body();
|
|
682
751
|
else await probe.headers();
|
|
683
|
-
|
|
752
|
+
probe.complete = !probe.ended();
|
|
753
|
+
const evidence = await probe.evidence();
|
|
754
|
+
if (probe.end && options.side !== 'guarded')
|
|
755
|
+
throw new Error(`redirect-secret: ${options.side} session ended (${probe.end})`);
|
|
756
|
+
return evidence;
|
|
684
757
|
} finally {
|
|
685
758
|
await probe.close();
|
|
686
759
|
}
|