morphit/apps/web/scripts/import-remember-me-smoke.ts

150 lines
6.2 KiB
TypeScript

#!/usr/bin/env tsx
/**
* Smoke: post-seed-import "remember me on this device" step is wired.
*
* Anchor: cp137 deep-deep walkthrough H-1.
*
* Pre-fix behavior: seed-mode import encrypted the envelope with a
* random session password, never persisted it, and immediately
* redirected to /settings. When the user closed the browser, the
* envelope was gone and they had to re-paste their 12 words from
* scratch. Grandma-hostile UX trap.
*
* Fix shipped cp137: after a successful seed-mode import, the page
* transitions to a `remember_me_choice` stage that asks the user
* whether to persist the envelope behind a password on this device,
* with a checkbox UNCHECKED BY DEFAULT. Privacy-positive default
* preserved; explicit opt-in to persistence.
*
* What this smoke asserts (against the on-disk page source):
* 1. The `importStage` state type exists with both
* `'form'` and `'remember_me_choice'` variants.
* 2. The page imports the persistence helpers it actually needs:
* `writeKeystoreMode`, `writeEnvelope`, and `decryptIdentity`.
* 3. The `rememberMe` reactive state is declared with `$state(false)`
* — the explicit `false` default is load-bearing for the
* privacy-positive-by-default property.
* 4. The `finalizeImportChoice` function exists, branches on
* `rememberMe`, and writes the keystore mode + envelope when
* the user opts in.
* 5. The template uses `importStage === 'form'` to gate the
* tab/form section and `importStage === 'remember_me_choice'`
* to render the new step.
*
* What this smoke does NOT assert:
* - The localized checkbox text (locale parity smoke covers that).
* - The behavior of the keyfile / posting-only paths (untouched
* by H-1; they have their own password capture).
*
* Tamper tests:
* - Change `$state(false)` to `$state(true)` on `rememberMe` →
* fails #3. This protects the privacy-positive default.
* - Remove the `writeEnvelope(persistedEnv)` call →
* fails #4 because persistence wouldn't actually happen.
* - Drop the `{#if importStage === 'form'}` gate → fails #5.
*/
import { readFileSync, existsSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const REPO_ROOT = join(__dirname, '..', '..', '..');
const PAGE = join(REPO_ROOT, 'apps/web/src/routes/[lang]/onboarding/import/+page.svelte');
let passes = 0;
let failures = 0;
function pass(msg: string): void {
passes += 1;
console.log(`${msg}`);
}
function fail(msg: string, detail = ''): void {
failures += 1;
console.error(`${msg}${detail ? `\n ${detail}` : ''}`);
}
console.log('import-remember-me-smoke\n');
if (!existsSync(PAGE)) {
fail(`onboarding/import page not found at ${PAGE}`);
process.exit(1);
}
const src = readFileSync(PAGE, 'utf8');
// #1: state type includes both stages.
if (
src.includes("type ImportStage = 'form' | 'remember_me_choice'") ||
src.includes("type ImportStage = 'remember_me_choice' | 'form'")
) {
pass(`ImportStage type declares both 'form' and 'remember_me_choice' variants`);
} else {
fail(
`ImportStage type does not include both required stages`,
`Expected: type ImportStage = 'form' | 'remember_me_choice';`
);
}
// #2: persistence helpers imported.
const requiredImports = [
'writeKeystoreMode',
'writeEnvelope',
'decryptIdentity'
];
const missingImports = requiredImports.filter((n) => !new RegExp(`\\b${n}\\b`).test(src));
if (missingImports.length === 0) {
pass(`page imports all persistence helpers (${requiredImports.join(', ')})`);
} else {
fail(
`page is missing required persistence helpers: ${missingImports.join(', ')}`,
`These are needed to encrypt-with-user-password and persist the envelope when ` +
`the user opts in to "remember me on this device."`
);
}
// #3: rememberMe defaults to false — load-bearing for the privacy-
// positive default. Match either `$state(false)` or
// `$state<boolean>(false)`.
const rememberMeFalseRe = /let\s+rememberMe\s*=\s*\$state\s*(?:<\s*boolean\s*>\s*)?\(\s*false\s*\)/;
if (rememberMeFalseRe.test(src)) {
pass(`rememberMe defaults to UNCHECKED ($state(false)) — privacy-positive default preserved`);
} else {
fail(
`rememberMe is not defaulted to false`,
`The checkbox MUST be unchecked by default so the privacy-positive ` +
`session-only behavior remains the default. Look for: ` +
`'let rememberMe = $state(false);' in the page source.`
);
}
// #4: finalizeImportChoice exists, branches on rememberMe, and persists.
const hasFinalize = /async function finalizeImportChoice\b/.test(src);
const hasBranch = /if\s*\(\s*!rememberMe\s*\)|if\s*\(\s*rememberMe\s*\)/.test(src);
const hasWriteEnvelope = /writeEnvelope\s*\(\s*persistedEnv\s*\)/.test(src);
const hasWriteKeystoreMode = /writeKeystoreMode\s*\(\s*['"]password['"]\s*\)/.test(src);
if (hasFinalize && hasBranch && hasWriteEnvelope && hasWriteKeystoreMode) {
pass(`finalizeImportChoice exists, branches on rememberMe, persists envelope + keystore mode when opted in`);
} else {
const missing: string[] = [];
if (!hasFinalize) missing.push('finalizeImportChoice function');
if (!hasBranch) missing.push('branch on rememberMe');
if (!hasWriteEnvelope) missing.push('writeEnvelope(persistedEnv) call');
if (!hasWriteKeystoreMode) missing.push("writeKeystoreMode('password') call");
fail(`finalizeImportChoice is incomplete`, `Missing: ${missing.join('; ')}`);
}
// #5: template gates both stages.
const hasFormGate = /\{#if\s+importStage\s*===\s*['"]form['"]\s*\}/.test(src);
const hasRememberMeStage = /\{:else if\s+importStage\s*===\s*['"]remember_me_choice['"]\s*\}/.test(src);
if (hasFormGate && hasRememberMeStage) {
pass(`template renders both importStage branches (form + remember_me_choice)`);
} else {
const missing: string[] = [];
if (!hasFormGate) missing.push("{#if importStage === 'form'} gate");
if (!hasRememberMeStage) missing.push("{:else if importStage === 'remember_me_choice'} branch");
fail(`template stage gating is incomplete`, `Missing: ${missing.join('; ')}`);
}
console.log(`\n${passes} passed, ${failures} failed`);
if (failures > 0) process.exit(1);
console.log(`✓ all ${passes} import-remember-me scenarios passed`);