@ai-ecoverse/slicc-browsers 1.18.1 → 1.18.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 +1 -1
- package/guard-runner.mjs +9 -3
- package/guard-vectors.json +1 -1
- package/package.json +1 -1
- package/protected-error-page.mjs +32 -2
package/README.md
CHANGED
|
@@ -283,7 +283,7 @@ Popups are observed by `Target.targetCreated` with the opener's `openerId`, or b
|
|
|
283
283
|
|
|
284
284
|
`{ "extensionWorld": "hidden" | "select" | "debugger" | "drivable", "as": "world", "requires": ["extensionWorld"] }` runs the C9 compound probe on the current ordinary page. The runner attaches an independent observer to discover actual context ids, unique ids and names, and sends probes through the client session. Discovery matches the expected fixture extension’s exact origin, never the first extension world: `run()`’s optional `extensionWorldId` defaults to shared-web’s harness id `ankmbodmgimjemnojmeddfcdlbocolob`. Multiple unrelated content-script worlds are ignored for fixture selection, while guards must still protect foreign worlds. A custom fixture must provide the same content-script marker contract. Object handles are minted and validated on the unguarded control session, then replayed as guesses on the guarded leg; Chrome object handles and DOM node ids are session-local, so observer handles must never be mistaken for client handles. Numeric ids from the control are also replayed alongside fresh observer-discovered selectors, but numeric ids are local to a tab: a control id may name the next tab’s exposed default context, which must work and return no extension marker. The same-tab hidden world’s actual numeric id and every extension unique-id guess still require a matched refusal. Unknown numeric ids and stale object handles (`not exposed`) need explicit refusals rather than Chrome’s invalid-id errors. `guessesRefused` aggregates only required refusals; `guessesReached` must be false on the guarded leg, and `exposedContextsWork` must remain true. Its result contains boolean evidence consumed by ordinary `result` checks. The unguarded leg validates those selectors against Chrome itself before guarded evidence can pass.
|
|
285
285
|
|
|
286
|
-
`{ "errorPage": "recover", "secret"?: "all" | "query" | "fragment", "as": "errorPage" }` (also modes `restore` and `sticky`; `secret` defaults to `all`) runs the protected-error-page compound probe with independent commit evidence, discovery redaction and recovery or sticky-lock checks. The unguarded observer seeds the protected history and failed commit, so the rows run on page-session-only transports too. The guarded client uses only `Target.getTargetInfo` with a `targetId`, `Target.getTargets`, `Target.setDiscoverTargets`, `Target.attachToTarget`, `Target.detachFromTarget` and `Target.closeTarget` without a session, which page-session transports answer themselves, and everything else on its page session; the rows require no whole-vector capability. Only `eventsRedacted` requires `discovery`: without it the probe does not call `Target.setDiscoverTargets`, that one field is `not-supported` in both legs, and the notes say `not-supported:discovery`; info, list, attach, recovery and lock checks still run. Discovery absence is structural: a guarded transport delivering either `Target.targetCreated` or `Target.targetInfoChanged` during such a row without declaring `discovery` fails the row, even when the event names an ordinary target. `Target.attachedToTarget` remains independently checked by `attachRedacted`. The runner leaves this probe’s independent Fetch sessions to the probe rather than answering their pauses itself. Each of the probe’s waits is bounded at 5000 ms, scaled by `settleScale`. When the probe’s own evidence is missing, it returns `incomplete` with `protected-error-page: <reason>` instead of throwing: the observer target could not be created or attached (an `openerId` included, after cleaning the target up), the observer’s history seed or allowed document did not commit (or its setup failed), the client’s `Page.navigate` to the trigger failed or was refused, the control’s own Fetch failure injection left no error commit, the protected history entry is missing, no real `BackForwardCacheRestore` arrived, or the observer’s sticky failure left no error page. The guarded leg’s missing error commit is not incomplete: it is the guard’s, and stays `live: false`.
|
|
286
|
+
`{ "errorPage": "recover", "secret"?: "all" | "query" | "fragment", "as": "errorPage" }` (also modes `restore` and `sticky`; `secret` defaults to `all`) runs the protected-error-page compound probe with independent commit evidence, discovery redaction and recovery or sticky-lock checks. The unguarded observer seeds the protected history and failed commit, so the rows run on page-session-only transports too. The guarded client uses only `Target.getTargetInfo` with a `targetId`, `Target.getTargets`, `Target.setDiscoverTargets`, `Target.attachToTarget`, `Target.detachFromTarget` and `Target.closeTarget` without a session, which page-session transports answer themselves, and everything else on its page session; the rows require no whole-vector capability. Only `eventsRedacted` requires `discovery`: without it the probe does not call `Target.setDiscoverTargets`, that one field is `not-supported` in both legs, and the notes say `not-supported:discovery`; info, list, attach, recovery and lock checks still run. Discovery absence is structural: a guarded transport delivering either `Target.targetCreated` or `Target.targetInfoChanged` during such a row without declaring `discovery` fails the row, even when the event names an ordinary target. `Target.attachedToTarget` remains independently checked by `attachRedacted`. The runner leaves this probe’s independent Fetch sessions to the probe rather than answering their pauses itself. Each of the probe’s waits is bounded at 5000 ms, scaled by `settleScale`. Its `diagnostics` record the client’s `Target.attachToTarget` as `clientAttach`, whose `outcome` is `answered` (with `sessionId`, whether the reply carried one), `refused` or `error` (both with the `error` text), and, once the client has navigated, the error-commit wait as `errorCommit`: `seen`, `elapsedMs` when seen or at timeout, `budgetMs` with `settleScale` applied, and the last `frame` and `targetInfo` read. They change no verdict. The row carries them as `evidence.control.errorPage` and `evidence.guarded.errorPage`, and a guarded leg that is not live because the attach gave no session or the commit was not seen says why: `not live: client attach refused: <text>` (or `failed: <text>`, or `answered without a sessionId`), or `not live: error commit not seen after <elapsed>ms (budget <budget>ms): frame <url> unreachableUrl <url or none>, target <url>`. When the probe’s own evidence is missing, it returns `incomplete` with `protected-error-page: <reason>` instead of throwing: the observer target could not be created or attached (an `openerId` included, after cleaning the target up), the observer’s history seed or allowed document did not commit (or its setup failed), the client’s `Page.navigate` to the trigger failed or was refused, the control’s own Fetch failure injection left no error commit, the protected history entry is missing, no real `BackForwardCacheRestore` arrived, or the observer’s sticky failure left no error page. The guarded leg’s missing error commit is not incomplete: it is the guard’s, and stays `live: false`.
|
|
287
287
|
|
|
288
288
|
`{ "redirectSecret": "headers" | "mix" | "body", "as": "redirect" }` runs a C2 compound probe and owns its Fetch pauses. The client must expose `messages`, an append-only array of every raw received CDP text frame, recorded before parsing; an events-only or reply-only tap fails `rawRecorded`. Whenever a run includes a `C2.redirect-*` row or a `redirectSecret` step, `run()` retains an explicit `fixture.redirectSecret` hook or automatically installs a fetch-only `POST /redirect` bridge. Its base is `fixtureBase`, then `fixture.origin`, then `targets.ordinary`; a browser-page alias or reverse proxy must forward that endpoint to the bundled fixture. The bundled `fixtureServer()` always supplies both its direct hook and HTTP control endpoint, so no custom registration method is needed. Missing, unreachable or non-successful registration is an explicit row/summary error containing `redirectSecret fixture registration required`, never a pass or a skipped observation. Fixture logs must include headers for the cookie check. The guard’s events and raw messages must represent the same delivered stream. The leak scan memoizes each decoded string and yields to the event loop between items, so concurrent rows in the same process are not starved. When the probe’s own unguarded observer setup (attach, `Page.enable` and the seven cookie) fails before the client is used, the probe returns `incomplete` with `redirect-secret: observer setup failed: <reason>` instead of throwing.
|
|
289
289
|
|
package/guard-runner.mjs
CHANGED
|
@@ -1283,7 +1283,10 @@ class Execution {
|
|
|
1283
1283
|
if (!live && this.unmeasured(this.vector.liveCheck))
|
|
1284
1284
|
return { live: 'incomplete', liveWhy: 'incomplete' };
|
|
1285
1285
|
if (!live && !unseen.length) live = await this.poll(alive, 1500);
|
|
1286
|
-
if (!unseen.length || (live && !incomplete.length))
|
|
1286
|
+
if (!unseen.length || (live && !incomplete.length))
|
|
1287
|
+
return live
|
|
1288
|
+
? { live }
|
|
1289
|
+
: { live, liveWhy: this.calls[this.vector.liveCheck.result]?.result?.liveWhy };
|
|
1287
1290
|
const detail = incomplete.length ? ` (${incomplete.join(', ')} incomplete)` : '';
|
|
1288
1291
|
return { live: false, liveWhy: `pre beacon not observed for ${unseen.join(', ')}${detail}` };
|
|
1289
1292
|
}
|
|
@@ -1321,8 +1324,10 @@ class Execution {
|
|
|
1321
1324
|
incomplete: this.incomplete,
|
|
1322
1325
|
evidence: Object.fromEntries(
|
|
1323
1326
|
Object.entries(this.calls)
|
|
1324
|
-
.filter(
|
|
1325
|
-
|
|
1327
|
+
.filter(
|
|
1328
|
+
([, out]) => (out?.result?.delay && out.result.probes) || out?.result?.diagnostics
|
|
1329
|
+
)
|
|
1330
|
+
.map(([name, out]) => [name, out.result.diagnostics ?? out.result])
|
|
1326
1331
|
),
|
|
1327
1332
|
};
|
|
1328
1333
|
} finally {
|
|
@@ -1746,6 +1751,7 @@ function settleIncomplete(row, vector, base, legs) {
|
|
|
1746
1751
|
control: fill('control'),
|
|
1747
1752
|
guarded: fill('guarded'),
|
|
1748
1753
|
live: legs.control.live === true && [true, 'incomplete'].includes(legs.guarded.live),
|
|
1754
|
+
liveWhy: legs.guarded.liveWhy,
|
|
1749
1755
|
});
|
|
1750
1756
|
for (const side of ['control', 'guarded'])
|
|
1751
1757
|
if (legs[side].incomplete?.length && filled[side] === 'pass') row[side] = 'incomplete';
|
package/guard-vectors.json
CHANGED
package/package.json
CHANGED
package/protected-error-page.mjs
CHANGED
|
@@ -4,6 +4,22 @@ import { createObserverTarget } from './observer-target.mjs';
|
|
|
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
|
+
function attachOutcome(out) {
|
|
8
|
+
if (out.ok) return { outcome: 'answered', sessionId: Boolean(out.result?.sessionId) };
|
|
9
|
+
return {
|
|
10
|
+
outcome: refused(out) ? 'refused' : 'error',
|
|
11
|
+
error: String(out.error?.message ?? out.error),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const attachWhy = ({ outcome: kind, error }) =>
|
|
16
|
+
kind === 'answered'
|
|
17
|
+
? 'client attach answered without a sessionId'
|
|
18
|
+
: `client attach ${kind === 'refused' ? 'refused' : 'failed'}: ${error}`;
|
|
19
|
+
|
|
20
|
+
const commitWhy = ({ elapsedMs, budgetMs, frame, targetInfo }) =>
|
|
21
|
+
`error commit not seen after ${elapsedMs}ms (budget ${budgetMs}ms): frame ${frame.url} unreachableUrl ${frame.unreachableUrl ?? 'none'}, target ${targetInfo.url}`;
|
|
22
|
+
|
|
7
23
|
async function loadedDocument(reader, observer, url, until) {
|
|
8
24
|
return until(async () => {
|
|
9
25
|
const { frameTree } = await reader.send('Page.getFrameTree', {}, observer);
|
|
@@ -197,7 +213,14 @@ export async function protectedErrorPage({
|
|
|
197
213
|
client.send('Target.attachToTarget', { targetId, flatten: true })
|
|
198
214
|
);
|
|
199
215
|
session = attached.result?.sessionId;
|
|
200
|
-
|
|
216
|
+
result.diagnostics = { clientAttach: attachOutcome(attached) };
|
|
217
|
+
if (!session)
|
|
218
|
+
return {
|
|
219
|
+
...result,
|
|
220
|
+
live: false,
|
|
221
|
+
attached: false,
|
|
222
|
+
liveWhy: attachWhy(result.diagnostics.clientAttach),
|
|
223
|
+
};
|
|
201
224
|
await client.send('Page.enable', {}, session);
|
|
202
225
|
await client.send('Runtime.enable', {}, session);
|
|
203
226
|
if (discovery) await client.send('Target.setDiscoverTargets', { discover: true });
|
|
@@ -212,6 +235,7 @@ export async function protectedErrorPage({
|
|
|
212
235
|
const unsent = await scripted(client, session, ordinary, failedURL);
|
|
213
236
|
if (unsent) return missing(unsent);
|
|
214
237
|
let evidence;
|
|
238
|
+
const waited = Date.now();
|
|
215
239
|
const live = await until(async () => {
|
|
216
240
|
const f = await frame();
|
|
217
241
|
const { targetInfo } = await reader.send('Target.getTargetInfo', { targetId });
|
|
@@ -224,10 +248,16 @@ export async function protectedErrorPage({
|
|
|
224
248
|
);
|
|
225
249
|
});
|
|
226
250
|
result.evidence = evidence;
|
|
251
|
+
result.diagnostics.errorCommit = {
|
|
252
|
+
seen: live,
|
|
253
|
+
elapsedMs: Date.now() - waited,
|
|
254
|
+
budgetMs: budget,
|
|
255
|
+
...evidence,
|
|
256
|
+
};
|
|
227
257
|
if (!live && side === 'control')
|
|
228
258
|
return missing(`missing error commit ${JSON.stringify(evidence)}`);
|
|
229
259
|
result.live = live;
|
|
230
|
-
if (!live) return result;
|
|
260
|
+
if (!live) return { ...result, liveWhy: commitWhy(result.diagnostics.errorCommit) };
|
|
231
261
|
await redaction({ client, targetId, failedURL, protectedOrigin, start, result, discovery });
|
|
232
262
|
const beforeReload = (await frame()).loaderId;
|
|
233
263
|
result.reload = (await outcome(client.send('Page.reload', {}, session))).ok;
|