Milestone v1 (v2.0.0): Mokosh — Session Capture #1
@@ -3660,6 +3660,262 @@ async function assertA30(): Promise<AssertionResult> {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ─── Plan 03-03 — A31 (password-filter PARTIAL; SPEC §10 #8) ────────
|
||||||
|
*
|
||||||
|
* A31 — D-P3-02 PARTIAL: verify the existing minimum filter at
|
||||||
|
* src/content/index.ts:82 (`if (target.type === 'password') return;`)
|
||||||
|
* fires when the operator types into a password input.
|
||||||
|
* Negative-assertion contract: SENTINEL value MUST be absent
|
||||||
|
* from logs/events.json.
|
||||||
|
*
|
||||||
|
* Charter alignment (per CONTEXT.md D-P3-02 + REQUIREMENTS.md line 271):
|
||||||
|
* - REQ-password-confidentiality moved Out of Scope v1 per 2026-05-20
|
||||||
|
* charter "we don't care about privacy hardening. At least here."
|
||||||
|
* - Full rrweb v2 maskInputFn + data-sensitive HTML attribute guards
|
||||||
|
* DEFERRED to Phase 4 if charter reverses.
|
||||||
|
* - A31 verifies the EXISTING minimum (the line-82 filter) — does
|
||||||
|
* NOT expand scope.
|
||||||
|
*
|
||||||
|
* Implementation note — cs-injection-world adaptation (Rule 3 blocking
|
||||||
|
* auto-fix; mirrors Plan 03-02 architectural fix):
|
||||||
|
* The plan as written drove `document.querySelector('#probe-password')`
|
||||||
|
* on the harness page (chrome-extension://...harness.html). That
|
||||||
|
* matches Plan 03-02's `<all_urls>` content_scripts assumption
|
||||||
|
* which is empirically WRONG (Chrome match-pattern spec: `<all_urls>`
|
||||||
|
* covers http/https/file/ftp/urn only — NOT chrome-extension). With
|
||||||
|
* no content script attached to the harness page, the production
|
||||||
|
* setupInputLogging at src/content/index.ts:78 never sees the
|
||||||
|
* harness-page input event AT ALL, so A31.2 (absence-of-sentinel)
|
||||||
|
* and A31.3 (absence-of-#probe-password-target) would pass
|
||||||
|
* tautologically — neither check empirically verifies the line-82
|
||||||
|
* filter "fires", only that NO event is captured on the harness page
|
||||||
|
* regardless of input type. That is NOT a valid §10 #8 PARTIAL
|
||||||
|
* verification (the filter could be deleted and the test would
|
||||||
|
* still pass).
|
||||||
|
*
|
||||||
|
* A31 therefore reuses the Plan 03-02 cs-injection-world pattern:
|
||||||
|
* open a fresh https://example.com probe tab where the content
|
||||||
|
* script DOES attach, inject a `<input type="password">` element +
|
||||||
|
* type the SENTINEL value + dispatch input event in the
|
||||||
|
* content-script's ISOLATED world, SAVE while the probe tab is
|
||||||
|
* active, finally-cleanup the probe tab. For a control case
|
||||||
|
* (verifies the wiring is operational), the same injection also
|
||||||
|
* types a control sentinel into a `<input type="text">` element
|
||||||
|
* — the production setupInputLogging MUST capture that event,
|
||||||
|
* PROVING the path that would have fired for the password input
|
||||||
|
* IS active. The control case is host-side-only check
|
||||||
|
* A31.4 (control event present); the production filter at
|
||||||
|
* src/content/index.ts:82 early-returns BEFORE addUserEvent so
|
||||||
|
* the password event NEVER lands in userEvents[] (A31.2 + A31.3).
|
||||||
|
*
|
||||||
|
* This satisfies the plan's binding contract literally:
|
||||||
|
* - artifact "types sentinel into the probe-page password input
|
||||||
|
* via setupFreshRecording + SAVE" — done (just in a different
|
||||||
|
* page that has the content script alive)
|
||||||
|
* - truths #1/#2/#3 (sentinel-value-absent + zero-events-targeting-
|
||||||
|
* password) — empirically verified because the input event IS
|
||||||
|
* seen by the production listener + filter
|
||||||
|
* - threat T-03-03-04 (defense-in-depth) — A31.4 control case is
|
||||||
|
* the third orthogonal path proving the listeners are alive
|
||||||
|
*
|
||||||
|
* FORBIDDEN_HOOK_STRINGS impact: NONE. A31 rides production
|
||||||
|
* setupInputLogging at src/content/index.ts:78 + the line-82 filter
|
||||||
|
* + chrome.tabs.* + chrome.scripting.executeScript + existing helpers.
|
||||||
|
* Tier-1 inventory stays at 12 entries.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** SAVE_ARCHIVE dispatch timeout for A31 — matches A24/A25/A27/A29/A30. */
|
||||||
|
const A31_SAVE_ARCHIVE_TIMEOUT_MS = 15_000;
|
||||||
|
/** Pre-SAVE segment-settle window (10s rotation + 1s slack). */
|
||||||
|
const A31_SEGMENT_SETTLE_MS = 11_000;
|
||||||
|
/** Settle after sentinel-typing trigger so the synchronous handler completes. */
|
||||||
|
const A31_TRIGGER_SETTLE_MS = 1_000;
|
||||||
|
/** Wait after chrome.tabs.create for the tab navigation to complete so
|
||||||
|
* the content script attaches + production listeners are set up
|
||||||
|
* (mirrors A30_TAB_NAVIGATION_WAIT_MS = 1.5s). */
|
||||||
|
const A31_TAB_NAVIGATION_WAIT_MS = 1_500;
|
||||||
|
/** Probe tab URL — example.com is RFC 2606 reserved + serves stable
|
||||||
|
* HTML under headless Chrome (Plan 02-04 A27 + Plan 03-02 A30 fixture parity). */
|
||||||
|
const A31_PROBE_TAB_URL = 'https://example.com/';
|
||||||
|
/** Fixed test sentinel — distinctive string the negative-assertion
|
||||||
|
* searches for in events.json. Per RESEARCH §"Security Domain":
|
||||||
|
* this is a probe sentinel, NOT a real secret; logging it would
|
||||||
|
* itself trigger an explicit RED. */
|
||||||
|
const A31_PASSWORD_SENTINEL = 'secret-do-not-log-123';
|
||||||
|
/** Control sentinel — distinctive string typed into a `<input type="text">`
|
||||||
|
* element in the same injection. Production setupInputLogging at
|
||||||
|
* src/content/index.ts:78 MUST capture this — driveA31's A31.4
|
||||||
|
* check verifies its presence as proof that the listener is alive
|
||||||
|
* (defense-in-depth against T-03-03-04 — if the production listener
|
||||||
|
* weren't running at all, A31.2/A31.3 would pass tautologically;
|
||||||
|
* A31.4 GREEN proves the listener IS running, so A31.2/A31.3 GREEN
|
||||||
|
* actually mean the filter fired). */
|
||||||
|
const A31_CONTROL_SENTINEL = 'control-event-must-be-logged-a31';
|
||||||
|
/** Production CSS selector returned by getSelector() at
|
||||||
|
* src/content/index.ts:241 for the password input (which has id).
|
||||||
|
* Drives A31.3 (target-absence check). */
|
||||||
|
const A31_PASSWORD_SELECTOR = '#probe-password';
|
||||||
|
/** Synthetic password-input element id (matches A31_PASSWORD_SELECTOR
|
||||||
|
* after the leading `#` is stripped). Injected into the probe tab DOM
|
||||||
|
* by chrome.scripting.executeScript. */
|
||||||
|
const A31_PASSWORD_INPUT_ID = 'probe-password';
|
||||||
|
/** Synthetic control-input element id — referenced by driveA31's
|
||||||
|
* A31.4 check via the same `#probe-control` selector. */
|
||||||
|
const A31_CONTROL_INPUT_ID = 'probe-control';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A31 — Password-filter PARTIAL empirical (SPEC §10 #8 PARTIAL per D-P3-02).
|
||||||
|
*
|
||||||
|
* Creates a fresh `https://example.com` probe tab (where the content
|
||||||
|
* script attaches normally per Plan 03-02 cs-injection-world insight),
|
||||||
|
* injects two `<input>` elements (a control `type="text"` + a sentinel
|
||||||
|
* `type="password"`) + types the corresponding sentinels + dispatches
|
||||||
|
* input events in the content-script's ISOLATED world, settles a
|
||||||
|
* segment, SAVEs while the probe tab is active, finally-cleanup the
|
||||||
|
* tab. Host-side driveA31 inspects logs/events.json and asserts:
|
||||||
|
* - The password SENTINEL is ABSENT from any UserEvent.value field
|
||||||
|
* (proves the line-82 filter early-returned before addUserEvent)
|
||||||
|
* - Zero UserEvent entries have target === '#probe-password'
|
||||||
|
* (proves the same filter via the orthogonal selector path)
|
||||||
|
* - At least one UserEvent contains the control sentinel
|
||||||
|
* (proves the listener was alive — defense-in-depth against
|
||||||
|
* the trivial "no events at all" tautology)
|
||||||
|
*
|
||||||
|
* @returns AssertionResult with 1 page-side check (SAVE ack); host-side
|
||||||
|
* driveA31 appends host-side checks for sentinel absence
|
||||||
|
* (A31.2 + A31.3) + control presence (A31.4).
|
||||||
|
*/
|
||||||
|
async function assertA31(): Promise<AssertionResult> {
|
||||||
|
const result: AssertionResult = {
|
||||||
|
passed: false,
|
||||||
|
name: 'A31 — password filter fires (SPEC §10 #8 PARTIAL per D-P3-02)',
|
||||||
|
checks: [],
|
||||||
|
diagnostics: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
let probeTabId: number | undefined;
|
||||||
|
|
||||||
|
try {
|
||||||
|
diag(result, 'Step 1: setupFreshRecording (A31 owns its recording — clean event-log window)');
|
||||||
|
const setupResp = await setupFreshRecording();
|
||||||
|
if (!setupResp.ok) {
|
||||||
|
throw new Error(`setupFreshRecording failed: ${setupResp.error ?? '(no error)'}`);
|
||||||
|
}
|
||||||
|
diag(result, 'Step 1 OK — REC state established');
|
||||||
|
|
||||||
|
diag(result, `Step 2: chrome.tabs.create(${A31_PROBE_TAB_URL}, active:true) — content script ISOLATED world is alive on https://, not on chrome-extension:// (Plan 03-02 lesson)`);
|
||||||
|
const probeTab = await chrome.tabs.create({ url: A31_PROBE_TAB_URL, active: true });
|
||||||
|
probeTabId = probeTab.id;
|
||||||
|
diag(result, `Step 2 result: probeTab.id=${probeTabId}, probeTab.url=${probeTab.url ?? '<pending>'}`);
|
||||||
|
if (probeTabId === undefined) {
|
||||||
|
throw new Error('chrome.tabs.create returned undefined tab.id');
|
||||||
|
}
|
||||||
|
|
||||||
|
diag(result, `Step 3: wait ${A31_TAB_NAVIGATION_WAIT_MS}ms for navigation + content script attach`);
|
||||||
|
await new Promise((r) => setTimeout(r, A31_TAB_NAVIGATION_WAIT_MS));
|
||||||
|
|
||||||
|
diag(result, `Step 4: settle ${A31_SEGMENT_SETTLE_MS}ms for first segment rotation`);
|
||||||
|
await new Promise((r) => setTimeout(r, A31_SEGMENT_SETTLE_MS));
|
||||||
|
|
||||||
|
diag(result, 'Step 5: chrome.scripting.executeScript — inject password+control inputs + dispatch input events in ISOLATED world (production setupInputLogging at src/content/index.ts:78 sees BOTH, line-82 filter early-returns on the password input only)');
|
||||||
|
const injectionResults = await chrome.scripting.executeScript({
|
||||||
|
target: { tabId: probeTabId },
|
||||||
|
world: 'ISOLATED',
|
||||||
|
func: (
|
||||||
|
passwordInputId: string,
|
||||||
|
passwordSentinel: string,
|
||||||
|
controlInputId: string,
|
||||||
|
controlSentinel: string,
|
||||||
|
): {
|
||||||
|
passwordTyped: boolean;
|
||||||
|
controlTyped: boolean;
|
||||||
|
passwordDispatched: boolean;
|
||||||
|
controlDispatched: boolean;
|
||||||
|
} => {
|
||||||
|
// Create the synthetic password input. Production
|
||||||
|
// setupInputLogging at src/content/index.ts:78 attaches via
|
||||||
|
// document.addEventListener('input', ...), so the production
|
||||||
|
// path covers any element added to document.body — including
|
||||||
|
// ones created and dispatched synchronously by us.
|
||||||
|
const passwordInput = document.createElement('input');
|
||||||
|
passwordInput.type = 'password';
|
||||||
|
passwordInput.id = passwordInputId;
|
||||||
|
passwordInput.value = passwordSentinel;
|
||||||
|
document.body.appendChild(passwordInput);
|
||||||
|
const passwordDispatched = passwordInput.dispatchEvent(
|
||||||
|
new Event('input', { bubbles: true }),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create the synthetic control input. setupInputLogging will
|
||||||
|
// see this one too — but because target.type !== 'password',
|
||||||
|
// the line-82 filter does NOT early-return; addUserEvent fires
|
||||||
|
// and the event lands in userEvents[] with type='input' and
|
||||||
|
// value containing the control sentinel. A31.4 host-side
|
||||||
|
// verifies this (defense-in-depth proving the listener IS
|
||||||
|
// alive — without it, A31.2/A31.3 would pass tautologically).
|
||||||
|
const controlInput = document.createElement('input');
|
||||||
|
controlInput.type = 'text';
|
||||||
|
controlInput.id = controlInputId;
|
||||||
|
controlInput.value = controlSentinel;
|
||||||
|
document.body.appendChild(controlInput);
|
||||||
|
const controlDispatched = controlInput.dispatchEvent(
|
||||||
|
new Event('input', { bubbles: true }),
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
passwordTyped: passwordInput.value === passwordSentinel,
|
||||||
|
controlTyped: controlInput.value === controlSentinel,
|
||||||
|
passwordDispatched,
|
||||||
|
controlDispatched,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
args: [
|
||||||
|
A31_PASSWORD_INPUT_ID,
|
||||||
|
A31_PASSWORD_SENTINEL,
|
||||||
|
A31_CONTROL_INPUT_ID,
|
||||||
|
A31_CONTROL_SENTINEL,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const injectionSummary = injectionResults[0]?.result ?? null;
|
||||||
|
diag(result, `Step 5 result: ${JSON.stringify(injectionSummary)}`);
|
||||||
|
|
||||||
|
diag(result, `Step 6: settle ${A31_TRIGGER_SETTLE_MS}ms so synchronous handlers complete + userEvents[] populates`);
|
||||||
|
await new Promise((r) => setTimeout(r, A31_TRIGGER_SETTLE_MS));
|
||||||
|
|
||||||
|
diag(result, 'Step 7: dispatch SAVE_ARCHIVE (probe tab is the active tab; SW will harvest from there)');
|
||||||
|
const ack = await sendMessageWithTimeout<{ success: boolean; error?: string }>(
|
||||||
|
{ type: 'SAVE_ARCHIVE' },
|
||||||
|
A31_SAVE_ARCHIVE_TIMEOUT_MS,
|
||||||
|
'SAVE_ARCHIVE (A31)',
|
||||||
|
);
|
||||||
|
diag(result, `Step 7 result: ${JSON.stringify(ack)}`);
|
||||||
|
|
||||||
|
result.checks.push({
|
||||||
|
name: 'A31.1: SAVE_ARCHIVE ack received with success=true',
|
||||||
|
expected: true,
|
||||||
|
actual: ack.success,
|
||||||
|
passed: ack.success === true,
|
||||||
|
});
|
||||||
|
|
||||||
|
result.passed = result.checks.every((c) => c.passed);
|
||||||
|
} catch (err) {
|
||||||
|
result.error = err instanceof Error ? err.message : String(err);
|
||||||
|
diag(result, `THREW: ${result.error}`);
|
||||||
|
} finally {
|
||||||
|
// T-02-04-04 mitigation parity (Plan 03-02 precedent): cleanup probe
|
||||||
|
// tab with silent-ignore on already-closed.
|
||||||
|
if (probeTabId !== undefined) {
|
||||||
|
try {
|
||||||
|
await chrome.tabs.remove(probeTabId);
|
||||||
|
} catch (rmErr) {
|
||||||
|
diag(result, `(probe tab cleanup ignored: ${String(rmErr)})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read `chrome.runtime.getManifest().version`. Used by the host-side
|
* Read `chrome.runtime.getManifest().version`. Used by the host-side
|
||||||
* orchestrator at startup to capture the expected version for A13's
|
* orchestrator at startup to capture the expected version for A13's
|
||||||
@@ -3715,6 +3971,8 @@ declare global {
|
|||||||
assertA29: () => Promise<AssertionResult>;
|
assertA29: () => Promise<AssertionResult>;
|
||||||
// Plan 03-02 — event-log verification (SPEC §10 #5)
|
// Plan 03-02 — event-log verification (SPEC §10 #5)
|
||||||
assertA30: () => Promise<AssertionResult>;
|
assertA30: () => Promise<AssertionResult>;
|
||||||
|
// Plan 03-03 — password-filter PARTIAL (SPEC §10 #8 PARTIAL per D-P3-02)
|
||||||
|
assertA31: () => Promise<AssertionResult>;
|
||||||
getManifestVersion: () => Promise<string>;
|
getManifestVersion: () => Promise<string>;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -3751,14 +4009,15 @@ window.__mokoshHarness = {
|
|||||||
assertA28,
|
assertA28,
|
||||||
assertA29,
|
assertA29,
|
||||||
assertA30,
|
assertA30,
|
||||||
|
assertA31,
|
||||||
getManifestVersion,
|
getManifestVersion,
|
||||||
};
|
};
|
||||||
|
|
||||||
const statusEl = document.getElementById('status');
|
const statusEl = document.getElementById('status');
|
||||||
if (statusEl !== null) {
|
if (statusEl !== null) {
|
||||||
statusEl.textContent = 'Harness ready. window.__mokoshHarness.{assertA1..A30, getManifestVersion} available.';
|
statusEl.textContent = 'Harness ready. window.__mokoshHarness.{assertA1..A31, getManifestVersion} available.';
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[harness-page] ready — window.__mokoshHarness installed (Plan 01-13 Task 9: A1..A14 + Plan 01-10 Wave 3: A15..A17 + Plan 01-12 Wave 6: A18..A22 + Plan 01-14: A23 + Plan 02-04 Tasks 1-2: A24+A25 + Plan 02-04 Task 3: A26+A27+A28 + Plan 03-01: A29 + Plan 03-02: A30 + getManifestVersion)');
|
console.log('[harness-page] ready — window.__mokoshHarness installed (Plan 01-13 Task 9: A1..A14 + Plan 01-10 Wave 3: A15..A17 + Plan 01-12 Wave 6: A18..A22 + Plan 01-14: A23 + Plan 02-04 Tasks 1-2: A24+A25 + Plan 02-04 Task 3: A26+A27+A28 + Plan 03-01: A29 + Plan 03-02: A30 + Plan 03-03: A31 + getManifestVersion)');
|
||||||
|
|
||||||
export {};
|
export {};
|
||||||
|
|||||||
Reference in New Issue
Block a user