@jevitate/cli 0.10.0 → 0.12.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/dist/approval-provenance.d.ts +6 -0
- package/dist/approval-provenance.js +9 -1
- package/dist/approval-provenance.js.map +1 -1
- package/dist/approved-demo.d.ts +86 -0
- package/dist/approved-demo.js +184 -0
- package/dist/approved-demo.js.map +1 -0
- package/dist/author-journey-cli.js +2 -1
- package/dist/author-journey-cli.js.map +1 -1
- package/dist/bin.js +16525 -13523
- package/dist/build-info.generated.js +2 -2
- package/dist/campaign-api.d.ts +15 -0
- package/dist/campaign-api.js +67 -6
- package/dist/campaign-api.js.map +1 -1
- package/dist/campaign-cli.js +7 -2
- package/dist/campaign-cli.js.map +1 -1
- package/dist/campaign-preflight.d.ts +76 -0
- package/dist/campaign-preflight.js +117 -0
- package/dist/campaign-preflight.js.map +1 -0
- package/dist/catalog-bundle-api.d.ts +15 -5
- package/dist/catalog-bundle-api.js +117 -14
- package/dist/catalog-bundle-api.js.map +1 -1
- package/dist/catalog-bundle-findings.d.ts +10 -0
- package/dist/catalog-bundle-findings.js +75 -1
- package/dist/catalog-bundle-findings.js.map +1 -1
- package/dist/catalog-bundle.d.ts +111 -7
- package/dist/catalog-bundle.js +208 -9
- package/dist/catalog-bundle.js.map +1 -1
- package/dist/catalog-cli.js +3 -2
- package/dist/catalog-cli.js.map +1 -1
- package/dist/check-api.js +20 -2
- package/dist/check-api.js.map +1 -1
- package/dist/check-cli.d.ts +2 -0
- package/dist/check-cli.js +25 -0
- package/dist/check-cli.js.map +1 -1
- package/dist/check-execute.d.ts +6 -0
- package/dist/check-execute.js +1 -0
- package/dist/check-execute.js.map +1 -1
- package/dist/check-types.d.ts +7 -0
- package/dist/check-types.js.map +1 -1
- package/dist/cli-output.js +15 -1
- package/dist/cli-output.js.map +1 -1
- package/dist/cold-start-report.d.ts +32 -0
- package/dist/cold-start-report.js +31 -0
- package/dist/cold-start-report.js.map +1 -0
- package/dist/defect-evidence.js +10 -5
- package/dist/defect-evidence.js.map +1 -1
- package/dist/demo-aspect-api.d.ts +11 -0
- package/dist/demo-aspect-api.js +27 -2
- package/dist/demo-aspect-api.js.map +1 -1
- package/dist/demo-aspect-cli.js +7 -1
- package/dist/demo-aspect-cli.js.map +1 -1
- package/dist/demo-capture.d.ts +79 -2
- package/dist/demo-capture.js +504 -12
- package/dist/demo-capture.js.map +1 -1
- package/dist/environments.d.ts +8 -1
- package/dist/environments.js +14 -3
- package/dist/environments.js.map +1 -1
- package/dist/exit-codes.js +1 -0
- package/dist/exit-codes.js.map +1 -1
- package/dist/explore-adversarial.js +2 -2
- package/dist/explore-adversarial.js.map +1 -1
- package/dist/explore-cli.js +3 -2
- package/dist/explore-cli.js.map +1 -1
- package/dist/explore-goal.js +4 -3
- package/dist/explore-goal.js.map +1 -1
- package/dist/explore-shared.d.ts +11 -0
- package/dist/explore-shared.js +9 -0
- package/dist/explore-shared.js.map +1 -1
- package/dist/findings-filing.js +7 -3
- package/dist/findings-filing.js.map +1 -1
- package/dist/github-issue-filer.js +15 -9
- package/dist/github-issue-filer.js.map +1 -1
- package/dist/journeeze-api.d.ts +8 -2
- package/dist/journeeze-api.js +105 -19
- package/dist/journeeze-api.js.map +1 -1
- package/dist/journey-anchor-api.d.ts +37 -0
- package/dist/journey-anchor-api.js +58 -0
- package/dist/journey-anchor-api.js.map +1 -0
- package/dist/journey-anchor-cli.d.ts +17 -0
- package/dist/journey-anchor-cli.js +104 -0
- package/dist/journey-anchor-cli.js.map +1 -0
- package/dist/journey-api.d.ts +38 -1
- package/dist/journey-api.js +107 -10
- package/dist/journey-api.js.map +1 -1
- package/dist/journey-cli.d.ts +0 -1
- package/dist/journey-cli.js +124 -22
- package/dist/journey-cli.js.map +1 -1
- package/dist/journey-demo-api.d.ts +29 -0
- package/dist/journey-demo-api.js +29 -7
- package/dist/journey-demo-api.js.map +1 -1
- package/dist/journey-labels-migrate-api.d.ts +100 -0
- package/dist/journey-labels-migrate-api.js +194 -0
- package/dist/journey-labels-migrate-api.js.map +1 -0
- package/dist/journey-lint-sarif.js +6 -0
- package/dist/journey-lint-sarif.js.map +1 -1
- package/dist/journey-network-checks.d.ts +11 -0
- package/dist/journey-network-checks.js +36 -13
- package/dist/journey-network-checks.js.map +1 -1
- package/dist/journey-review.d.ts +12 -2
- package/dist/journey-review.js +522 -108
- package/dist/journey-review.js.map +1 -1
- package/dist/load-api.js +4 -2
- package/dist/load-api.js.map +1 -1
- package/dist/mcp-api.d.ts +3 -1
- package/dist/mcp-api.js +21 -2
- package/dist/mcp-api.js.map +1 -1
- package/dist/mcp-cli-tools.d.ts +1 -0
- package/dist/mcp-cli-tools.js +32 -9
- package/dist/mcp-cli-tools.js.map +1 -1
- package/dist/openrouter-call.d.ts +23 -10
- package/dist/openrouter-call.js +14 -11
- package/dist/openrouter-call.js.map +1 -1
- package/dist/png-pixels.d.ts +12 -0
- package/dist/png-pixels.js +96 -0
- package/dist/png-pixels.js.map +1 -0
- package/dist/regression-api.js +8 -5
- package/dist/regression-api.js.map +1 -1
- package/dist/review-render-options.d.ts +17 -0
- package/dist/review-render-options.js +17 -0
- package/dist/review-render-options.js.map +1 -0
- package/dist/server-log-verify.js +2 -1
- package/dist/server-log-verify.js.map +1 -1
- package/dist/signal-triage.js +7 -3
- package/dist/signal-triage.js.map +1 -1
- package/dist/source-run-api.js +2 -2
- package/dist/source-run-api.js.map +1 -1
- package/dist/ux-usability.js +5 -2
- package/dist/ux-usability.js.map +1 -1
- package/dist/verify-fix-api.d.ts +29 -0
- package/dist/verify-fix-api.js +45 -2
- package/dist/verify-fix-api.js.map +1 -1
- package/dist/verify-fix-cli.js +4 -0
- package/dist/verify-fix-cli.js.map +1 -1
- package/dist/zip-store.d.ts +15 -0
- package/dist/zip-store.js +93 -0
- package/dist/zip-store.js.map +1 -0
- package/package.json +14 -14
- package/skills/jevitate-demo/SKILL.md +4 -1
- package/skills/jevitate-run-journey/SKILL.md +4 -0
- package/skills/jevitate-test-campaign/SKILL.md +13 -0
- package/skills/jevitate-verify-fix/SKILL.md +5 -0
package/dist/demo-capture.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
|
-
import { rm } from "node:fs/promises";
|
|
2
|
+
import { readFile, rm } from "node:fs/promises";
|
|
3
3
|
import { REVEALED_SECRET_SELECTORS, REVEALED_SECRET_SHAPES, revealedSecretsIn, secretForms } from "@jevitate/ai-core";
|
|
4
|
-
import { DEMO_OVERLAY_HIDE_STYLE, hideDemoOverlayForCapture } from "@jevitate/explore";
|
|
4
|
+
import { DEMO_OVERLAY_ATTR, DEMO_OVERLAY_HIDE_STYLE, hideDemoOverlayForCapture } from "@jevitate/explore";
|
|
5
|
+
import { decodePng } from "./png-pixels.js";
|
|
5
6
|
import { descriptorToLocator } from "@jevitate/recorder";
|
|
6
7
|
import { clock } from "@jevitate/domain";
|
|
7
8
|
/**
|
|
@@ -52,7 +53,7 @@ export async function captureStepScreenshot(page, path, ctx, layers = [], clip)
|
|
|
52
53
|
}
|
|
53
54
|
try {
|
|
54
55
|
for (const layer of layers)
|
|
55
|
-
await layer.confirm?.(page, ctx);
|
|
56
|
+
await layer.confirm?.(page, ctx, { path, ...(clip === undefined ? {} : { clip }) });
|
|
56
57
|
}
|
|
57
58
|
catch (err) {
|
|
58
59
|
await rm(path, { force: true });
|
|
@@ -124,8 +125,14 @@ const MASK_RUNTIME = String.raw `((cfg) => {
|
|
|
124
125
|
};
|
|
125
126
|
const SKIP = new Set(["SCRIPT", "STYLE", "NOSCRIPT", "TEMPLATE", "HEAD", "TITLE", "META", "LINK"]);
|
|
126
127
|
const PAD = 2;
|
|
128
|
+
// Captured at install (an init script runs before any page script), so a page that later replaces
|
|
129
|
+
// requestAnimationFrame cannot stop the mask from re-measuring before each paint.
|
|
130
|
+
const raf = window.requestAnimationFrame.bind(window);
|
|
127
131
|
const CSS = ":host{all:initial}*{pointer-events:none !important;box-sizing:border-box}[hidden]{display:none !important}" +
|
|
128
132
|
".m{position:fixed;background:" + cfg.fill + ";border-radius:2px}" +
|
|
133
|
+
".jm{position:fixed;background:" + cfg.jzFill.mask + "}" +
|
|
134
|
+
".jp{position:fixed;background:" + cfg.jzFill.placeholder + "}" +
|
|
135
|
+
".jb{position:fixed;background:" + cfg.jzFill.block + "}" +
|
|
129
136
|
".h{position:fixed;border:3px solid #f59e0b;border-radius:6px;box-shadow:0 0 0 4px rgba(245,158,11,.35)}";
|
|
130
137
|
let host = null;
|
|
131
138
|
let layer = null;
|
|
@@ -244,6 +251,178 @@ const MASK_RUNTIME = String.raw `((cfg) => {
|
|
|
244
251
|
}
|
|
245
252
|
return out;
|
|
246
253
|
};
|
|
254
|
+
// ── #471: jz-mask-v1 (journeeze-saas docs/contract/journey-import-v1.md "Recording references and
|
|
255
|
+
// masking"), top document only — every frame below it is an iframe/frame the policy replaces whole.
|
|
256
|
+
// - masked: input, textarea, select, every editable element (contenteditable in any spelling, or
|
|
257
|
+
// inherited; designMode) and [data-jz-mask], each with its subtree;
|
|
258
|
+
// - placeholder: iframe, frame, object, embed, video, audio, canvas;
|
|
259
|
+
// - left out: [data-jz-block] and its subtree (an empty box of its size).
|
|
260
|
+
// A region is its element's box; a descendant painting outside that box gets a box of its own, so
|
|
261
|
+
// "with its subtree" holds for overflowing content too. Content is never read: only geometry.
|
|
262
|
+
const JZ = cfg.jz === true && window === window.top;
|
|
263
|
+
const LATE = document.readyState !== "loading";
|
|
264
|
+
const JZ_FIELDS = new Set(["input", "textarea", "select"]);
|
|
265
|
+
const JZ_PLACEHOLDERS = new Set(["iframe", "frame", "object", "embed", "video", "audio", "canvas"]);
|
|
266
|
+
const JZ_SKIP = new Set(["script", "style", "noscript", "template", "head", "title", "meta", "link"]);
|
|
267
|
+
const JZ_MAX_NODES = 50000;
|
|
268
|
+
const JZ_CLASS = { mask: "jm", placeholder: "jp", block: "jb" };
|
|
269
|
+
let jzTargets = [];
|
|
270
|
+
let jzTopLayer = [];
|
|
271
|
+
let jzDrawn = [];
|
|
272
|
+
let jzTooBig = false;
|
|
273
|
+
let jzPseudo = false;
|
|
274
|
+
let jzFirst = true;
|
|
275
|
+
let breach = "";
|
|
276
|
+
const own = (el) => el === host || (el.hasAttribute && (el.hasAttribute(cfg.attr) || el.hasAttribute(cfg.overlayAttr)));
|
|
277
|
+
const jzCollect = () => {
|
|
278
|
+
const out = [];
|
|
279
|
+
const top = [];
|
|
280
|
+
let nodes = 0;
|
|
281
|
+
jzTooBig = false;
|
|
282
|
+
jzPseudo = false;
|
|
283
|
+
// A positioned ::before/::after can paint generated content outside every box measured here.
|
|
284
|
+
const escapes = (el) => {
|
|
285
|
+
for (const pseudo of ["::before", "::after"]) {
|
|
286
|
+
const cs = getComputedStyle(el, pseudo);
|
|
287
|
+
if (cs.content !== "none" && cs.content !== "normal" && (cs.position === "absolute" || cs.position === "fixed")) return true;
|
|
288
|
+
}
|
|
289
|
+
return false;
|
|
290
|
+
};
|
|
291
|
+
// Every descendant (elements, text, open shadow trees) of a region root, as overflow candidates.
|
|
292
|
+
const subtree = (rootEl, kind) => {
|
|
293
|
+
out.push({ kind, el: rootEl });
|
|
294
|
+
// A placeholder's fallback children are never rendered: only masked and left-out subtrees paint.
|
|
295
|
+
const painted = kind !== "placeholder";
|
|
296
|
+
if (painted && escapes(rootEl)) jzPseudo = true;
|
|
297
|
+
const stack = [rootEl];
|
|
298
|
+
if (rootEl.shadowRoot) stack.push(rootEl.shadowRoot);
|
|
299
|
+
while (stack.length > 0) {
|
|
300
|
+
const n = stack.pop();
|
|
301
|
+
for (let c = n.firstChild; c !== null; c = c.nextSibling) {
|
|
302
|
+
if (++nodes > JZ_MAX_NODES) { jzTooBig = true; return; }
|
|
303
|
+
if (c.nodeType === 3) { if (c.nodeValue && c.nodeValue.trim() !== "") out.push({ kind, node: c, within: rootEl }); continue; }
|
|
304
|
+
if (c.nodeType !== 1 || own(c)) continue;
|
|
305
|
+
out.push({ kind, el: c, within: rootEl });
|
|
306
|
+
// A modal dialog or popover inside a region leaves its box for the top layer.
|
|
307
|
+
if (c.localName === "dialog" || c.hasAttribute("popover")) top.push(c);
|
|
308
|
+
if (painted && escapes(c)) jzPseudo = true;
|
|
309
|
+
stack.push(c);
|
|
310
|
+
if (c.shadowRoot) stack.push(c.shadowRoot);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
if (document.designMode === "on" && document.documentElement) subtree(document.documentElement, "mask");
|
|
315
|
+
else {
|
|
316
|
+
const stack = [document];
|
|
317
|
+
while (stack.length > 0 && !jzTooBig) {
|
|
318
|
+
const n = stack.pop();
|
|
319
|
+
for (let c = n.firstChild; c !== null && !jzTooBig; c = c.nextSibling) {
|
|
320
|
+
if (c.nodeType !== 1) continue;
|
|
321
|
+
if (++nodes > JZ_MAX_NODES) { jzTooBig = true; break; }
|
|
322
|
+
if (own(c)) continue;
|
|
323
|
+
const name = (c.localName || "").toLowerCase();
|
|
324
|
+
if (JZ_SKIP.has(name)) continue;
|
|
325
|
+
if (name === "dialog" || c.hasAttribute("popover")) top.push(c);
|
|
326
|
+
if (c.hasAttribute("data-jz-block")) { subtree(c, "block"); continue; }
|
|
327
|
+
if (c.hasAttribute("data-jz-mask") || JZ_FIELDS.has(name) || c.isContentEditable === true) { subtree(c, "mask"); continue; }
|
|
328
|
+
if (JZ_PLACEHOLDERS.has(name)) { subtree(c, "placeholder"); continue; }
|
|
329
|
+
stack.push(c);
|
|
330
|
+
if (c.shadowRoot) stack.push(c.shadowRoot);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
jzTopLayer = top;
|
|
335
|
+
return out;
|
|
336
|
+
};
|
|
337
|
+
const covers = (b, r) => b.left <= r.left + 0.5 && b.top <= r.top + 0.5 && b.right >= r.right - 0.5 && b.bottom >= r.bottom - 0.5;
|
|
338
|
+
const nodeRects = (node) => {
|
|
339
|
+
if (!node.isConnected) return null;
|
|
340
|
+
const r = document.createRange();
|
|
341
|
+
try { r.selectNodeContents(node); } catch (e) { return null; }
|
|
342
|
+
return Array.from(r.getClientRects());
|
|
343
|
+
};
|
|
344
|
+
// This frame's jz regions: [{ r, kind }] (a target that left the document has no box).
|
|
345
|
+
const jzRegions = () => {
|
|
346
|
+
const out = [];
|
|
347
|
+
const rootBox = new Map();
|
|
348
|
+
for (const t of jzTargets) {
|
|
349
|
+
let rs;
|
|
350
|
+
if (t.node !== undefined) rs = nodeRects(t.node);
|
|
351
|
+
else rs = t.el.isConnected ? [t.el.getBoundingClientRect()] : null;
|
|
352
|
+
if (rs === null) continue;
|
|
353
|
+
let within = null;
|
|
354
|
+
if (t.within !== undefined) {
|
|
355
|
+
within = rootBox.get(t.within);
|
|
356
|
+
if (within === undefined) { within = t.within.isConnected ? t.within.getBoundingClientRect() : null; rootBox.set(t.within, within); }
|
|
357
|
+
}
|
|
358
|
+
for (const r of rs) {
|
|
359
|
+
if (!(r.width > 0 || r.height > 0)) continue;
|
|
360
|
+
if (within !== null && within !== undefined && covers(within, r)) continue;
|
|
361
|
+
out.push({ r, kind: t.kind });
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return out;
|
|
365
|
+
};
|
|
366
|
+
const topLayerOpen = () => {
|
|
367
|
+
if (document.fullscreenElement) return true;
|
|
368
|
+
for (const el of jzTopLayer) {
|
|
369
|
+
try { if (el.isConnected && el.matches(":modal, :popover-open")) return true; } catch (e) { return true; }
|
|
370
|
+
}
|
|
371
|
+
return false;
|
|
372
|
+
};
|
|
373
|
+
// An animation on a region, an ancestor or a descendant may move it on the compositor between two
|
|
374
|
+
// main-thread frames: the video cannot be proven for that stretch. One that only changes paint
|
|
375
|
+
// (a colour, an outline, a shadow, opacity) moves nothing and is ignored.
|
|
376
|
+
const PAINT_ONLY = /^(color|background-color|border(-top|-right|-bottom|-left)?-color|outline(-color|-offset|-width|-style)?|box-shadow|opacity|text-decoration-color|fill|stroke|caret-color|accent-color|visibility)$/;
|
|
377
|
+
const kebab = (k) => k.replace(/[A-Z]/g, (c) => "-" + c.toLowerCase());
|
|
378
|
+
const movesGeometry = (a) => {
|
|
379
|
+
try {
|
|
380
|
+
if (typeof a.transitionProperty === "string") return !PAINT_ONLY.test(a.transitionProperty);
|
|
381
|
+
const frames = a.effect && typeof a.effect.getKeyframes === "function" ? a.effect.getKeyframes() : null;
|
|
382
|
+
if (frames === null) return true;
|
|
383
|
+
for (const f of frames) for (const k of Object.keys(f)) {
|
|
384
|
+
if (k === "offset" || k === "computedOffset" || k === "easing" || k === "composite") continue;
|
|
385
|
+
if (!PAINT_ONLY.test(kebab(k))) return true;
|
|
386
|
+
}
|
|
387
|
+
return false;
|
|
388
|
+
} catch (e) { return true; }
|
|
389
|
+
};
|
|
390
|
+
const animatedRegion = () => {
|
|
391
|
+
if (typeof document.getAnimations !== "function") return false;
|
|
392
|
+
const els = jzTargets.filter((t) => t.el !== undefined && t.within === undefined).map((t) => t.el);
|
|
393
|
+
if (els.length === 0) return false;
|
|
394
|
+
for (const a of document.getAnimations()) {
|
|
395
|
+
if (a.playState !== "running" || !movesGeometry(a)) continue;
|
|
396
|
+
const tg = a.effect && a.effect.target;
|
|
397
|
+
if (!tg) continue;
|
|
398
|
+
for (const el of els) if (tg === el || (tg.contains && tg.contains(el)) || (el.contains && el.contains(tg))) return true;
|
|
399
|
+
}
|
|
400
|
+
return false;
|
|
401
|
+
};
|
|
402
|
+
const report = (reason) => {
|
|
403
|
+
if (breach !== "") return;
|
|
404
|
+
breach = String(reason).slice(0, 200);
|
|
405
|
+
try { const f = window[cfg.report]; if (typeof f === "function") f(breach); } catch (e) { /* the sticky breach is read at the next verify */ }
|
|
406
|
+
};
|
|
407
|
+
const jzWatch = () => {
|
|
408
|
+
if (!JZ) return;
|
|
409
|
+
if (jzFirst) {
|
|
410
|
+
jzFirst = false;
|
|
411
|
+
if (LATE && jzDrawn.length > 0) report("the jz-mask-v1 layer was installed after the page had painted");
|
|
412
|
+
}
|
|
413
|
+
if (jzTooBig) report("the page is too large to prove jz-mask-v1");
|
|
414
|
+
if (jzPseudo) report("a positioned ::before/::after in a jz-mask-v1 region can paint outside it");
|
|
415
|
+
if (jzDrawn.length > 0 && topLayerOpen()) report("a top-layer element (modal dialog, popover or fullscreen) could paint over jz-mask-v1 regions");
|
|
416
|
+
if (jzDrawn.length > 0 && animatedRegion()) report("an animation moved a jz-mask-v1 region");
|
|
417
|
+
if (lastError !== "") report("the mask loop failed");
|
|
418
|
+
};
|
|
419
|
+
if (JZ) {
|
|
420
|
+
window.addEventListener("scroll", (e) => {
|
|
421
|
+
if (jzDrawn.length === 0) return;
|
|
422
|
+
const t = e.target === document || e.target === window ? document.scrollingElement : e.target;
|
|
423
|
+
try { if (t && getComputedStyle(t).scrollBehavior === "smooth") report("a smooth scroll can move jz-mask-v1 regions on the compositor"); } catch (e2) { report("a scroll could not be checked"); }
|
|
424
|
+
}, true);
|
|
425
|
+
}
|
|
247
426
|
const rectsOf = (t) => {
|
|
248
427
|
if (t.kind === "range") {
|
|
249
428
|
if (!t.node.isConnected) return null;
|
|
@@ -279,7 +458,15 @@ const MASK_RUNTIME = String.raw `((cfg) => {
|
|
|
279
458
|
if (rs === null) { dirty = true; continue; }
|
|
280
459
|
for (const r of rs) if (r.width > 0 || r.height > 0) rects.push(r);
|
|
281
460
|
}
|
|
282
|
-
|
|
461
|
+
let jz = [];
|
|
462
|
+
if (JZ) {
|
|
463
|
+
// Re-collected on EVERY frame, not only on a mutation: a field can appear where no observer
|
|
464
|
+
// sees it (inside a shadow root attached later, an element upgraded by customElements.define).
|
|
465
|
+
jzTargets = jzCollect();
|
|
466
|
+
jz = jzRegions();
|
|
467
|
+
}
|
|
468
|
+
const all = rects.map((r) => ({ r, cls: "m" })).concat(jz.map((x) => ({ r: x.r, cls: JZ_CLASS[x.kind] })));
|
|
469
|
+
while (boxes.length < all.length) {
|
|
283
470
|
const d = document.createElement("div");
|
|
284
471
|
d.className = "m";
|
|
285
472
|
layer.appendChild(d);
|
|
@@ -287,9 +474,14 @@ const MASK_RUNTIME = String.raw `((cfg) => {
|
|
|
287
474
|
}
|
|
288
475
|
for (let i = 0; i < boxes.length; i++) {
|
|
289
476
|
const d = boxes[i];
|
|
290
|
-
if (i <
|
|
477
|
+
if (i < all.length) {
|
|
478
|
+
if (d.className !== all[i].cls) d.className = all[i].cls;
|
|
479
|
+
place(d, all[i].r, PAD);
|
|
480
|
+
d.hidden = false;
|
|
481
|
+
} else d.hidden = true;
|
|
291
482
|
}
|
|
292
483
|
drawn = rects;
|
|
484
|
+
jzDrawn = jz;
|
|
293
485
|
if (highlightEl !== null && highlightEl.isConnected) {
|
|
294
486
|
place(hiBox, highlightEl.getBoundingClientRect(), 5);
|
|
295
487
|
hiBox.hidden = false;
|
|
@@ -297,7 +489,8 @@ const MASK_RUNTIME = String.raw `((cfg) => {
|
|
|
297
489
|
};
|
|
298
490
|
const tick = () => {
|
|
299
491
|
try { update(); lastError = ""; } catch (e) { lastError = String((e && e.message) || e).slice(0, 200); }
|
|
300
|
-
|
|
492
|
+
try { jzWatch(); } catch (e) { report("the jz-mask-v1 watch failed"); }
|
|
493
|
+
raf(tick);
|
|
301
494
|
};
|
|
302
495
|
const mo = new MutationObserver((records) => {
|
|
303
496
|
for (const r of records) {
|
|
@@ -312,12 +505,12 @@ const MASK_RUNTIME = String.raw `((cfg) => {
|
|
|
312
505
|
const mark = () => { dirty = true; };
|
|
313
506
|
window.addEventListener("input", mark, true);
|
|
314
507
|
window.addEventListener("change", mark, true);
|
|
315
|
-
const covers = (b, r) => b.left <= r.left + 0.5 && b.top <= r.top + 0.5 && b.right >= r.right - 0.5 && b.bottom >= r.bottom - 0.5;
|
|
316
508
|
const api = {
|
|
317
509
|
verify() {
|
|
318
510
|
dirty = true;
|
|
319
511
|
try { update(); } catch (e) {
|
|
320
|
-
return { ok: false, occurrences: 0, masked: 0, reason: "the mask update failed: " + String((e && e.message) || e).slice(0, 200), rects: []
|
|
512
|
+
return { ok: false, occurrences: 0, masked: 0, reason: "the mask update failed: " + String((e && e.message) || e).slice(0, 200), rects: [],
|
|
513
|
+
...(JZ ? { jz: { ok: false, regions: 0, rects: [], reason: "the mask update failed", breach: breach || "the mask update failed", viewport: { width: window.innerWidth, height: window.innerHeight } } } : {}) };
|
|
321
514
|
}
|
|
322
515
|
const problems = [];
|
|
323
516
|
{
|
|
@@ -338,6 +531,24 @@ const MASK_RUNTIME = String.raw `((cfg) => {
|
|
|
338
531
|
if (lastError !== "") problems.push("the mask loop failed: " + lastError);
|
|
339
532
|
const out = { ok: problems.length === 0, occurrences: drawn.length, masked, rects: drawn.map((r) => ({ x: r.left, y: r.top, width: r.width, height: r.height })) };
|
|
340
533
|
if (problems.length > 0) out.reason = problems.join("; ");
|
|
534
|
+
if (JZ) {
|
|
535
|
+
const jp = problems.filter((p) => p.indexOf("mask layer") >= 0);
|
|
536
|
+
if (jzTooBig) jp.push("the page is too large to prove jz-mask-v1");
|
|
537
|
+
if (jzPseudo) jp.push("a positioned ::before/::after in a jz-mask-v1 region can paint outside it");
|
|
538
|
+
if (jzDrawn.length > 0 && topLayerOpen()) jp.push("a top-layer element (modal dialog, popover or fullscreen) could paint over jz-mask-v1 regions");
|
|
539
|
+
let jzMasked = 0;
|
|
540
|
+
for (const x of jzDrawn) if (live.some((b) => covers(b, x.r))) jzMasked++;
|
|
541
|
+
if (jzMasked < jzDrawn.length) jp.push((jzDrawn.length - jzMasked) + " jz-mask-v1 region(s) not covered by a box");
|
|
542
|
+
if (lastError !== "") jp.push("the mask loop failed");
|
|
543
|
+
out.jz = {
|
|
544
|
+
ok: jp.length === 0,
|
|
545
|
+
regions: jzDrawn.length,
|
|
546
|
+
rects: jzDrawn.map((x) => ({ x: x.r.left, y: x.r.top, width: x.r.width, height: x.r.height, kind: x.kind })),
|
|
547
|
+
viewport: { width: window.innerWidth, height: window.innerHeight },
|
|
548
|
+
breach,
|
|
549
|
+
};
|
|
550
|
+
if (jp.length > 0) out.jz.reason = jp.join("; ");
|
|
551
|
+
}
|
|
341
552
|
return out;
|
|
342
553
|
},
|
|
343
554
|
learn(values) { if (Array.isArray(values)) for (const v of values) learn(v); dirty = true; return true; },
|
|
@@ -348,9 +559,19 @@ const MASK_RUNTIME = String.raw `((cfg) => {
|
|
|
348
559
|
clearHighlight() { highlightEl = null; try { update(); } catch (e) { /* presentation only */ } return true; },
|
|
349
560
|
};
|
|
350
561
|
Object.defineProperty(window, cfg.name, { value: Object.freeze(api), enumerable: false, configurable: false, writable: false });
|
|
351
|
-
|
|
562
|
+
raf(tick);
|
|
352
563
|
return true;
|
|
353
564
|
})(__CFG__)`;
|
|
565
|
+
/** #471: the jz-mask-v1 fills — masked, placeholder, left out (opaque, never a page colour by intent). */
|
|
566
|
+
export const JZ_MASK_FILLS = { mask: "#4B5563", placeholder: "#9CA3AF", block: "#E5E7EB" };
|
|
567
|
+
/** #471: a jz-mask-v1 capture could not be proven: that screenshot is left out (and the demo's video with it). */
|
|
568
|
+
export class JzMaskUnprovenError extends Error {
|
|
569
|
+
code = "E_JZ_MASK";
|
|
570
|
+
constructor(message) {
|
|
571
|
+
super(message);
|
|
572
|
+
this.name = "JzMaskUnprovenError";
|
|
573
|
+
}
|
|
574
|
+
}
|
|
354
575
|
/** Bound on one mask round-trip (ms). */
|
|
355
576
|
const MASK_CALL_MS = 5_000;
|
|
356
577
|
async function within(p, what) {
|
|
@@ -393,9 +614,14 @@ export class SecretPixelMask {
|
|
|
393
614
|
* element, a credential-shaped value) is masked, so every capture installs and proves the mask.
|
|
394
615
|
*/
|
|
395
616
|
active = true;
|
|
396
|
-
|
|
617
|
+
/** #471: the mask also enforces jz-mask-v1 on the top document (fields, editables, embeds, marked regions). */
|
|
618
|
+
jzMaskV1;
|
|
619
|
+
/** #471: frames of a top document the jz layer could not prove (reported live by the page). */
|
|
620
|
+
#jzBreaches = new Set();
|
|
621
|
+
constructor(secrets, opts = {}) {
|
|
397
622
|
const forms = [...new Set(secrets.filter((s) => typeof s === "string" && s.trim() !== "").flatMap((s) => [...secretForms(s)]))];
|
|
398
623
|
this.#name = `__jevitateMask_${randomBytes(8).toString("hex")}`;
|
|
624
|
+
this.jzMaskV1 = opts.jzMaskV1 === true;
|
|
399
625
|
// JSON inside a JS expression (valid JS since ES2019); `<` escaped so no value can close a script context.
|
|
400
626
|
const cfg = JSON.stringify({
|
|
401
627
|
name: this.#name,
|
|
@@ -404,15 +630,124 @@ export class SecretPixelMask {
|
|
|
404
630
|
markers: REVEALED_SECRET_SELECTORS,
|
|
405
631
|
fill: PIXEL_MASK_COLOR,
|
|
406
632
|
attr: PIXEL_MASK_ATTR,
|
|
633
|
+
overlayAttr: DEMO_OVERLAY_ATTR,
|
|
634
|
+
jz: this.jzMaskV1,
|
|
635
|
+
jzFill: JZ_MASK_FILLS,
|
|
636
|
+
report: `${this.#name}_jz`,
|
|
407
637
|
}).replace(/</g, "\\u003c");
|
|
408
638
|
this.#source = MASK_RUNTIME.replace("__CFG__", () => cfg);
|
|
409
639
|
}
|
|
640
|
+
/**
|
|
641
|
+
* #471: every closed shadow root that appears in `page` while it is open — at load, attached
|
|
642
|
+
* later to a known node, or inside a subtree inserted with one — that is not one of jevitate's own
|
|
643
|
+
* layers is a video breach. Uses the DevTools DOM domain (Chromium); a watch that cannot start or
|
|
644
|
+
* that errors is a breach too (fail closed). Frames below the top document are included
|
|
645
|
+
* (conservative: they are placeholders, but the watch does not tell them apart).
|
|
646
|
+
*/
|
|
647
|
+
async #watchClosedRoots(page) {
|
|
648
|
+
const breach = (why) => void this.#jzBreaches.add(why.slice(0, 200));
|
|
649
|
+
let session;
|
|
650
|
+
try {
|
|
651
|
+
session = await within(page.context().newCDPSession(page), "watching shadow roots");
|
|
652
|
+
}
|
|
653
|
+
catch (e) {
|
|
654
|
+
breach(`closed shadow roots cannot be watched: ${errText(e)}`);
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
let closed = false;
|
|
658
|
+
page.once("close", () => {
|
|
659
|
+
closed = true;
|
|
660
|
+
});
|
|
661
|
+
/** Known nodes' attributes (by nodeId): what tells jevitate's own layer hosts apart. */
|
|
662
|
+
const attrs = new Map();
|
|
663
|
+
const ours = (a) => {
|
|
664
|
+
for (let i = 0; a !== undefined && i < a.length; i += 2)
|
|
665
|
+
if (a[i] === PIXEL_MASK_ATTR || a[i] === DEMO_OVERLAY_ATTR)
|
|
666
|
+
return true;
|
|
667
|
+
return false;
|
|
668
|
+
};
|
|
669
|
+
const scan = (n) => {
|
|
670
|
+
if (n.nodeId !== undefined && n.attributes !== undefined)
|
|
671
|
+
attrs.set(n.nodeId, n.attributes);
|
|
672
|
+
for (const sr of (n.shadowRoots ?? [])) {
|
|
673
|
+
if (sr.shadowRootType === "closed" && !ours(n.attributes))
|
|
674
|
+
breach("a closed shadow root appeared that jz-mask-v1 cannot see into");
|
|
675
|
+
scan(sr);
|
|
676
|
+
}
|
|
677
|
+
for (const c of (n.children ?? []))
|
|
678
|
+
scan(c);
|
|
679
|
+
if (n.contentDocument !== undefined)
|
|
680
|
+
scan(n.contentDocument);
|
|
681
|
+
};
|
|
682
|
+
const fail = (e) => {
|
|
683
|
+
if (!closed)
|
|
684
|
+
breach(`the shadow-root watch failed: ${errText(e)}`);
|
|
685
|
+
};
|
|
686
|
+
const load = async () => {
|
|
687
|
+
const { root } = (await session.send("DOM.getDocument", { depth: -1, pierce: true }));
|
|
688
|
+
attrs.clear();
|
|
689
|
+
scan(root);
|
|
690
|
+
};
|
|
691
|
+
const s = session;
|
|
692
|
+
s.on("DOM.documentUpdated", () => void load().catch(fail));
|
|
693
|
+
s.on("DOM.setChildNodes", (p) => {
|
|
694
|
+
for (const n of (p.nodes ?? []))
|
|
695
|
+
scan(n);
|
|
696
|
+
});
|
|
697
|
+
s.on("DOM.childNodeInserted", (p) => {
|
|
698
|
+
const n = p.node;
|
|
699
|
+
scan(n);
|
|
700
|
+
// Learn the inserted subtree whole (pierce), so a root deep inside it is reported too.
|
|
701
|
+
if (n.nodeId !== undefined)
|
|
702
|
+
void session.send("DOM.requestChildNodes", { nodeId: n.nodeId, depth: -1, pierce: true }).catch(fail);
|
|
703
|
+
});
|
|
704
|
+
s.on("DOM.attributeModified", (p) => {
|
|
705
|
+
const id = p.nodeId;
|
|
706
|
+
const a = [...(attrs.get(id) ?? [])];
|
|
707
|
+
const at = a.findIndex((x, i) => i % 2 === 0 && x === p.name);
|
|
708
|
+
if (at >= 0)
|
|
709
|
+
a[at + 1] = String(p.value);
|
|
710
|
+
else
|
|
711
|
+
a.push(String(p.name), String(p.value));
|
|
712
|
+
attrs.set(id, a);
|
|
713
|
+
});
|
|
714
|
+
s.on("DOM.shadowRootPushed", (p) => {
|
|
715
|
+
const root = p.root;
|
|
716
|
+
if (root.shadowRootType === "closed" && !ours(attrs.get(p.hostId)))
|
|
717
|
+
breach("a closed shadow root appeared that jz-mask-v1 cannot see into");
|
|
718
|
+
scan(root);
|
|
719
|
+
});
|
|
720
|
+
try {
|
|
721
|
+
await within(session.send("DOM.enable"), "watching shadow roots");
|
|
722
|
+
await within(load(), "watching shadow roots");
|
|
723
|
+
}
|
|
724
|
+
catch (e) {
|
|
725
|
+
breach(`closed shadow roots cannot be watched: ${errText(e)}`);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
410
728
|
/** Installs the live mask in `page`'s context (every future document/frame) and every current frame. Throws when it cannot. */
|
|
411
729
|
async install(page) {
|
|
412
730
|
const ctx = page.context();
|
|
413
731
|
if (!this.#contexts.has(ctx)) {
|
|
414
732
|
try {
|
|
733
|
+
// #471: a frame the jz layer cannot prove is reported as it happens (a document may be gone by the next check).
|
|
734
|
+
if (this.jzMaskV1) {
|
|
735
|
+
await within(ctx.exposeBinding(`${this.#name}_jz`, ({ frame }, reason) => {
|
|
736
|
+
if (frame === frame.page().mainFrame())
|
|
737
|
+
this.#jzBreaches.add(String(reason).slice(0, 200));
|
|
738
|
+
}), "installing the jz-mask-v1 report");
|
|
739
|
+
}
|
|
415
740
|
await within(ctx.addInitScript({ content: this.#source }), "installing the pixel mask");
|
|
741
|
+
// #471: reduced motion, so a site's smooth scrolling and motion (which can move a region
|
|
742
|
+
// on the compositor between two checked frames, voiding the video) mostly stays off.
|
|
743
|
+
if (this.jzMaskV1)
|
|
744
|
+
for (const p of ctx.pages())
|
|
745
|
+
await within(p.emulateMedia({ reducedMotion: "reduce" }), "emulating reduced motion");
|
|
746
|
+
// #471: a closed shadow root is content the page layer cannot see into — watched for the
|
|
747
|
+
// whole recording through the DevTools protocol, however briefly it exists.
|
|
748
|
+
if (this.jzMaskV1)
|
|
749
|
+
for (const p of ctx.pages())
|
|
750
|
+
await this.#watchClosedRoots(p);
|
|
416
751
|
}
|
|
417
752
|
catch (e) {
|
|
418
753
|
throw new MaskUnavailableError(`could not install the pixel mask: ${errText(e)}`);
|
|
@@ -513,6 +848,7 @@ export class SecretPixelMask {
|
|
|
513
848
|
let occurrences = 0;
|
|
514
849
|
let masked = 0;
|
|
515
850
|
let rects = [];
|
|
851
|
+
let jz;
|
|
516
852
|
const reasons = [];
|
|
517
853
|
for (const frame of page.frames()) {
|
|
518
854
|
if (frame.isDetached())
|
|
@@ -527,8 +863,14 @@ export class SecretPixelMask {
|
|
|
527
863
|
}
|
|
528
864
|
occurrences += r.occurrences;
|
|
529
865
|
masked += r.masked;
|
|
530
|
-
if (frame === page.mainFrame())
|
|
866
|
+
if (frame === page.mainFrame()) {
|
|
531
867
|
rects = r.rects;
|
|
868
|
+
if (r.jz !== undefined) {
|
|
869
|
+
jz = r.jz;
|
|
870
|
+
if (r.jz.breach !== "")
|
|
871
|
+
this.#jzBreaches.add(r.jz.breach);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
532
874
|
if (!r.ok)
|
|
533
875
|
reasons.push(r.reason ?? "the mask could not be proven");
|
|
534
876
|
await this.#sync(frame);
|
|
@@ -539,7 +881,10 @@ export class SecretPixelMask {
|
|
|
539
881
|
reasons.push(errText(e));
|
|
540
882
|
}
|
|
541
883
|
}
|
|
542
|
-
|
|
884
|
+
if (this.jzMaskV1 && jz === undefined) {
|
|
885
|
+
jz = { ok: false, regions: 0, rects: [], viewport: { width: 0, height: 0 }, reason: `the jz-mask-v1 layer is not running in the page${reasons.length === 0 ? "" : `: ${reasons.join("; ")}`}`, breach: "" };
|
|
886
|
+
}
|
|
887
|
+
return { ok: reasons.length === 0, occurrences, masked, rects, ...(reasons.length === 0 ? {} : { reason: reasons.join("; ") }), ...(jz === undefined ? {} : { jz }) };
|
|
543
888
|
}
|
|
544
889
|
/** Throws {@link MaskUnavailableError} unless {@link verify} proves the mask. */
|
|
545
890
|
async assertMasked(page) {
|
|
@@ -560,6 +905,63 @@ export class SecretPixelMask {
|
|
|
560
905
|
},
|
|
561
906
|
};
|
|
562
907
|
}
|
|
908
|
+
/**
|
|
909
|
+
* #471: what the jz layer could not prove in a top document during the run, in order — a video
|
|
910
|
+
* recorded over such a frame is never exported. Empty when every frame was proven (or jz is off).
|
|
911
|
+
*/
|
|
912
|
+
jzBreaches() {
|
|
913
|
+
return [...this.#jzBreaches];
|
|
914
|
+
}
|
|
915
|
+
/**
|
|
916
|
+
* #471: the jz-mask-v1 capture layer (method `dom-before-capture`: the regions are painted over in
|
|
917
|
+
* the page, before the screenshot). Fails closed with {@link JzMaskUnprovenError}, which leaves out
|
|
918
|
+
* that screenshot — never the run. Proof per capture:
|
|
919
|
+
* 1. before: the live layer proves every region covered by a box, nothing in the top layer, no
|
|
920
|
+
* closed shadow root it cannot see into (Chromium DevTools protocol, the page's own roots only);
|
|
921
|
+
* 2. after: the same regions at the same places (nothing moved in between), and every pixel of
|
|
922
|
+
* every region in the written PNG is one of the jz-mask-v1 / secret fills.
|
|
923
|
+
* `regions(step)` is the proven region count of that step's capture.
|
|
924
|
+
*/
|
|
925
|
+
jzLayer() {
|
|
926
|
+
const before = new Map();
|
|
927
|
+
const proven = new Map();
|
|
928
|
+
const fail = (step, why) => {
|
|
929
|
+
throw new JzMaskUnprovenError(`step ${step}: jz-mask-v1 could not be proven: ${why}`);
|
|
930
|
+
};
|
|
931
|
+
return {
|
|
932
|
+
name: "jz-mask-v1",
|
|
933
|
+
regions: (step) => proven.get(step),
|
|
934
|
+
prepare: async (page, ctx) => {
|
|
935
|
+
proven.delete(ctx.step);
|
|
936
|
+
if (!this.jzMaskV1)
|
|
937
|
+
fail(ctx.step, "the mask was made without jz-mask-v1");
|
|
938
|
+
const r = await this.verify(page);
|
|
939
|
+
if (r.jz === undefined || !r.jz.ok)
|
|
940
|
+
fail(ctx.step, r.jz?.reason ?? "no proof");
|
|
941
|
+
const hidden = await closedShadowRoots(page);
|
|
942
|
+
if (hidden !== 0)
|
|
943
|
+
fail(ctx.step, hidden < 0 ? "the page's shadow roots could not be inspected" : `${hidden} closed shadow root(s) it cannot see into`);
|
|
944
|
+
before.set(ctx.step, r.jz);
|
|
945
|
+
},
|
|
946
|
+
confirm: async (page, ctx, shot) => {
|
|
947
|
+
const first = before.get(ctx.step);
|
|
948
|
+
before.delete(ctx.step);
|
|
949
|
+
if (first === undefined)
|
|
950
|
+
return fail(ctx.step, "no proof before the capture");
|
|
951
|
+
if (shot === undefined || shot.clip !== undefined)
|
|
952
|
+
return fail(ctx.step, "only a whole-viewport capture can be pixel-checked");
|
|
953
|
+
const r = await this.verify(page);
|
|
954
|
+
if (r.jz === undefined || !r.jz.ok)
|
|
955
|
+
return fail(ctx.step, r.jz?.reason ?? "no proof after the capture");
|
|
956
|
+
if (!sameRegions(first, r.jz))
|
|
957
|
+
return fail(ctx.step, "a region moved or changed during the capture");
|
|
958
|
+
const problem = jzPixelProblem(await readFile(shot.path), first);
|
|
959
|
+
if (problem !== null)
|
|
960
|
+
return fail(ctx.step, problem);
|
|
961
|
+
proven.set(ctx.step, first.regions);
|
|
962
|
+
},
|
|
963
|
+
};
|
|
964
|
+
}
|
|
563
965
|
/** Draws a capture highlight box around `target`'s first match (display-only). True when shown. */
|
|
564
966
|
async highlight(page, target) {
|
|
565
967
|
return this.highlightLocator(page, descriptorToLocator(page, target));
|
|
@@ -601,4 +1003,94 @@ export function maskingPort(port, mask) {
|
|
|
601
1003
|
},
|
|
602
1004
|
};
|
|
603
1005
|
}
|
|
1006
|
+
// ── #471: jz-mask-v1 proof helpers ──────────────────────────────────────────────────────────────
|
|
1007
|
+
/** Same regions, same kinds, same places (within half a CSS pixel). */
|
|
1008
|
+
function sameRegions(a, b) {
|
|
1009
|
+
if (a.rects.length !== b.rects.length)
|
|
1010
|
+
return false;
|
|
1011
|
+
const close = (x, y) => Math.abs(x - y) <= 0.5;
|
|
1012
|
+
return a.rects.every((r, i) => {
|
|
1013
|
+
const o = b.rects[i];
|
|
1014
|
+
return o !== undefined && o.kind === r.kind && close(o.x, r.x) && close(o.y, r.y) && close(o.width, r.width) && close(o.height, r.height);
|
|
1015
|
+
});
|
|
1016
|
+
}
|
|
1017
|
+
function hexRgb(hex) {
|
|
1018
|
+
const n = Number.parseInt(hex.slice(1), 16);
|
|
1019
|
+
return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
|
|
1020
|
+
}
|
|
1021
|
+
/** Every fill a mask box can have: the jz-mask-v1 kinds and the secret mask (boxes may overlap). */
|
|
1022
|
+
const MASK_FILLS_RGB = [...Object.values(JZ_MASK_FILLS), PIXEL_MASK_COLOR].map(hexRgb);
|
|
1023
|
+
/**
|
|
1024
|
+
* Why `png` (a whole-viewport capture) does not show a mask fill on every pixel inside every region
|
|
1025
|
+
* of `check`, or null. Regions are in CSS pixels; the image is the viewport at the device scale.
|
|
1026
|
+
*/
|
|
1027
|
+
export function jzPixelProblem(png, check) {
|
|
1028
|
+
let img;
|
|
1029
|
+
try {
|
|
1030
|
+
img = decodePng(png);
|
|
1031
|
+
}
|
|
1032
|
+
catch (e) {
|
|
1033
|
+
return `the capture could not be decoded: ${errText(e)}`;
|
|
1034
|
+
}
|
|
1035
|
+
if (check.rects.length === 0)
|
|
1036
|
+
return null;
|
|
1037
|
+
if (!(check.viewport.width > 0))
|
|
1038
|
+
return "the viewport size is unknown";
|
|
1039
|
+
const scale = img.width / check.viewport.width;
|
|
1040
|
+
let bad = 0;
|
|
1041
|
+
for (const r of check.rects) {
|
|
1042
|
+
const x0 = Math.max(0, Math.ceil(r.x * scale));
|
|
1043
|
+
const y0 = Math.max(0, Math.ceil(r.y * scale));
|
|
1044
|
+
const x1 = Math.min(img.width, Math.floor((r.x + r.width) * scale));
|
|
1045
|
+
const y1 = Math.min(img.height, Math.floor((r.y + r.height) * scale));
|
|
1046
|
+
for (let y = y0; y < y1; y++) {
|
|
1047
|
+
for (let x = x0; x < x1; x++) {
|
|
1048
|
+
const i = (y * img.width + x) * 4;
|
|
1049
|
+
const [pr, pg, pb, pa] = [img.rgba[i], img.rgba[i + 1], img.rgba[i + 2], img.rgba[i + 3]];
|
|
1050
|
+
if (pa !== 255 || !MASK_FILLS_RGB.some(([fr, fg, fb]) => fr === pr && fg === pg && fb === pb))
|
|
1051
|
+
bad++;
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
return bad === 0 ? null : `${bad} pixel(s) inside jz-mask-v1 regions are not a mask fill`;
|
|
1056
|
+
}
|
|
1057
|
+
/**
|
|
1058
|
+
* #471: closed shadow roots in the top document that are not jevitate's own layers — content the
|
|
1059
|
+
* jz layer cannot see into, so it cannot prove a field there masked. -1 when the page cannot be
|
|
1060
|
+
* inspected (not Chromium, a protocol failure): unprovable. Frames below (contentDocument) are
|
|
1061
|
+
* skipped: each is covered whole by a placeholder.
|
|
1062
|
+
*/
|
|
1063
|
+
export async function closedShadowRoots(page) {
|
|
1064
|
+
let session;
|
|
1065
|
+
try {
|
|
1066
|
+
session = await within(page.context().newCDPSession(page), "inspecting shadow roots");
|
|
1067
|
+
const { root } = (await within(session.send("DOM.getDocument", { depth: -1, pierce: true }), "inspecting shadow roots"));
|
|
1068
|
+
let closed = 0;
|
|
1069
|
+
const ours = (n) => {
|
|
1070
|
+
const a = n.attributes ?? [];
|
|
1071
|
+
for (let i = 0; i < a.length; i += 2)
|
|
1072
|
+
if (a[i] === PIXEL_MASK_ATTR || a[i] === DEMO_OVERLAY_ATTR)
|
|
1073
|
+
return true;
|
|
1074
|
+
return false;
|
|
1075
|
+
};
|
|
1076
|
+
const stack = [root];
|
|
1077
|
+
while (stack.length > 0) {
|
|
1078
|
+
const n = stack.pop();
|
|
1079
|
+
for (const sr of n.shadowRoots ?? []) {
|
|
1080
|
+
if (sr.shadowRootType === "closed" && !ours(n))
|
|
1081
|
+
closed++;
|
|
1082
|
+
stack.push(sr);
|
|
1083
|
+
}
|
|
1084
|
+
for (const c of n.children ?? [])
|
|
1085
|
+
stack.push(c);
|
|
1086
|
+
}
|
|
1087
|
+
return closed;
|
|
1088
|
+
}
|
|
1089
|
+
catch {
|
|
1090
|
+
return -1;
|
|
1091
|
+
}
|
|
1092
|
+
finally {
|
|
1093
|
+
await session?.detach().catch(() => undefined);
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
604
1096
|
//# sourceMappingURL=demo-capture.js.map
|