104 lines
4.5 KiB
TypeScript
104 lines
4.5 KiB
TypeScript
/**
|
|
* require-live-session-smoke — the locked → welcome-back-unlock redirect guard
|
|
* (cp342; cp356 retargets it from the homepage to /login?next=).
|
|
*
|
|
* `<RequireLiveSession />` (lib/components/RequireLiveSession.svelte) is a
|
|
* render-nothing guard for session-required pages. On a refresh / direct-nav
|
|
* while FULLY locked (the in-memory session is wiped on every reload —
|
|
* decrypted keys never persist; the security posture), the page's core action
|
|
* is unusable, so it sends the user to the welcome-back UNLOCK screen (/login),
|
|
* carrying the page they were trying to reach as `?next=…`; after they unlock
|
|
* with their password the login page forwards them to that destination instead
|
|
* of the homepage (cp356).
|
|
*
|
|
* • Redirects ONLY when fully locked — `!isUnlocked && !isPairedReadOnly`.
|
|
* A paired-readonly session is a LIVE read-only session (keys on the phone)
|
|
* and KEEPS access; the page shows its own read-only / write-blocked
|
|
* affordance instead.
|
|
* • Runs ONCE on mount (not an `$effect`), so a later idle auto-lock while
|
|
* the user is actively on the page does NOT yank them away.
|
|
*
|
|
* cp340 introduced this redirect inline on /settings; cp342 generalized it into
|
|
* this shared component and applied it to settings, 2FA, backup-keys, post, and
|
|
* post/edit; cp343 extended coverage to /chat and /chat/[peer] (both require a
|
|
* live chat identity), so the behaviour is uniform across every login-required
|
|
* page regardless of which one you landed on.
|
|
*
|
|
* Pages with a genuine locked-but-useful READ-ONLY view (e.g. /my/orders, which
|
|
* lists your on-chain order history from the cached account name) deliberately
|
|
* do NOT use the guard — they're not stranded.
|
|
*
|
|
* Usage (from apps/web): tsx scripts/require-live-session-smoke.ts
|
|
*/
|
|
import { readFileSync, existsSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
|
|
const root = join(import.meta.dirname, '..');
|
|
const read = (...p: string[]): string => readFileSync(join(root, ...p), 'utf-8');
|
|
const route = (...p: string[]): string => read('src', 'routes', '[lang]', ...p);
|
|
|
|
const componentPath = join(root, 'src', 'lib', 'components', 'RequireLiveSession.svelte');
|
|
const component = existsSync(componentPath) ? readFileSync(componentPath, 'utf-8') : '';
|
|
|
|
let failures = 0;
|
|
let checks = 0;
|
|
function check(name: string, cond: boolean): void {
|
|
checks++;
|
|
if (cond) {
|
|
console.log(` ✓ ${name}`);
|
|
} else {
|
|
failures++;
|
|
console.log(` ✗ ${name}`);
|
|
}
|
|
}
|
|
|
|
// ── 1. The guard component does the right thing ──────────────────────────────
|
|
check('RequireLiveSession.svelte exists', component.length > 0);
|
|
check(
|
|
'redirects to the welcome-back unlock screen carrying ?next= (cp356)',
|
|
/gotoLocale\(\s*['"]\/login\?next=/.test(component)
|
|
);
|
|
check(
|
|
'redirects ONLY when fully locked: !isUnlocked && !isPairedReadOnly (paired keeps access)',
|
|
/!get\(isUnlocked\)[\s\S]{0,40}!get\(isPairedReadOnly\)/.test(component)
|
|
);
|
|
check(
|
|
'runs once in onMount (NOT an $effect that would re-fire on idle auto-lock)',
|
|
/onMount\(/.test(component) && !/\$effect\(/.test(component)
|
|
);
|
|
|
|
// ── 2. Every session-required landing page USES the guard ────────────────────
|
|
const usingPages: ReadonlyArray<readonly [string, string]> = [
|
|
['settings', route('settings', '+page.svelte')],
|
|
['settings/security/2fa', route('settings', 'security', '2fa', '+page.svelte')],
|
|
['backup-keys', route('backup-keys', '+page.svelte')],
|
|
['post', route('post', '+page.svelte')],
|
|
['post/edit', route('post', 'edit', '[permlink]', '+page.svelte')],
|
|
['chat', route('chat', '+page.svelte')],
|
|
['chat/[peer]', route('chat', '[peer=account]', '+page.svelte')]
|
|
];
|
|
for (const [name, src] of usingPages) {
|
|
check(
|
|
`${name} renders <RequireLiveSession /> and imports it`,
|
|
/<RequireLiveSession\s*\/>/.test(src) && /RequireLiveSession\.svelte/.test(src)
|
|
);
|
|
}
|
|
|
|
// ── 3. Intentional exclusions: read-only-useful / public pages must NOT use it─
|
|
const excludedPages: ReadonlyArray<readonly [string, string]> = [
|
|
['my/orders (shows read-only order history when locked)', route('my', 'orders', '+page.svelte')],
|
|
['orderbook (public)', route('orderbook', '+page.svelte')],
|
|
['faq (public)', route('faq', '+page.svelte')]
|
|
];
|
|
for (const [name, src] of excludedPages) {
|
|
check(`${name} does NOT use the guard`, !/RequireLiveSession/.test(src));
|
|
}
|
|
|
|
console.log('');
|
|
if (failures === 0) {
|
|
console.log(`✓ all ${checks} require-live-session scenarios passed`);
|
|
process.exit(0);
|
|
} else {
|
|
console.log(`✗ ${failures} check(s) failed`);
|
|
process.exit(1);
|
|
}
|