test(core): pin the shell's one use of browser storage, not ban it
CI / verify (pull_request) Successful in 1m12s
CI / infrastructure-smoke (pull_request) Successful in 42s
CI / package-release (pull_request) Successful in 48s
CI / migration-smoke (pull_request) Successful in 2m26s
CI / image-build (pull_request) Successful in 1m8s
CI / deploy-smoke (same-origin) (pull_request) Successful in 49s
CI / package-publication (pull_request) Successful in 57s
CI / deploy-smoke (split) (pull_request) Successful in 2m41s

CI failed in `identity-verification-boundaries.test.ts`: its guard that a
reset token never reaches browser storage was a blanket ban on the word
`localStorage` anywhere in the shell. The sidebar preference (ADR 0058)
legitimately reads it in the entry point.

The guarantee is unchanged: the reset token stays in React state for
the life of one form. The guard is now precise:

- `localStorage` appears only in `main.tsx`, only to hand it to the
  sidebar preference;
- the shell's only storage write is the sidebar's own key and a
  validated mode;
- the files that ever hold the token (`auth-protocol.ts`,
  `ResetPasswordForm.tsx`, `HostApp.tsx`) touch no storage API at all;
- `sessionStorage` and `document.cookie` stay banned everywhere.

A planted `localStorage.setItem("t", token)` in `auth-protocol.ts` fails
it. The runtime reset-token test in `auth-recovery.test.tsx` now runs
with the production localStorage-backed preference and asserts storage
holds nothing but the sidebar key.

It passed locally only because Turbo restored `@ekklesia/core:test` from
cache: that test reads shell source that Core's task hash does not
include.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
ekklesia
2026-10-14 17:55:07 -05:00
co-authored by Claude Opus 5.5
parent af31724cb4
commit cf2f847c8c
3 changed files with 48 additions and 6 deletions
@@ -169,13 +169,41 @@ describe("the browser shell reaches none of it", () => {
});
it("never puts a reset token in browser storage", async () => {
for (const file of await readTypeScriptFiles(path.join("surfaces", "shell", "src"))) {
const code = withoutComments(file.text);
// A reset token is a bearer credential for one account; the shell holds it in React state for
// the life of one form and nowhere else.
expect(code, file.path).not.toContain("localStorage");
// the life of one form and nowhere else. The shell's one use of browser storage is the desktop
// sidebar's expanded/collapsed mode (ADR 0058), so that use is pinned exactly: `localStorage` is
// named only by the entry point, and only to hand it to the sidebar preference, whose one write
// is its own fixed key and a validated mode.
const files = await readTypeScriptFiles(path.join("surfaces", "shell", "src"));
const writes: string[] = [];
for (const file of files) {
const code = withoutComments(file.text);
expect(code, file.path).not.toContain("sessionStorage");
expect(code, file.path).not.toContain("document.cookie");
writes.push(
...[...code.matchAll(/\.setItem\(([^)]*)\)/gu)].map(
(match) => `${path.relative(repositoryRoot, file.path)}: ${match[1]}`,
),
);
if (file.path.endsWith(path.join("browser", "main.tsx"))) {
expect(code.match(/localStorage/gu), file.path).toHaveLength(1);
expect(code).toMatch(
/createLocalSidebarPreference\(\s*\(\(\) => \{\s*try \{\s*return window\.localStorage;/u,
);
} else {
expect(code, file.path).not.toContain("localStorage");
}
}
expect(writes).toEqual([
`${path.join("surfaces", "shell", "src", "browser", "sidebar-preference.ts")}: SIDEBAR_PREFERENCE_KEY, mode`,
]);
// The files that ever hold the token touch no storage API at all.
for (const file of files.filter((candidate) =>
["auth-protocol.ts", "ResetPasswordForm.tsx", "HostApp.tsx"].some((name) =>
candidate.path.endsWith(name),
),
)) {
expect(withoutComments(file.text), file.path).not.toMatch(/\b(?:setItem|getItem|Storage)\b/u);
}
});
});
+4 -1
View File
@@ -403,7 +403,10 @@ parser is exercised as the pure function it is, including that it names no desti
no runtime assertion can make: that the identity feature names no SMTP detail, that no Ekklesia
schema gained a token column and no identity code hashes or compares a token, that nothing
interpolates a token, link, address, or body into a log line, that a device has no email, password,
or verification concept, and that no browser source imports Better Auth or Core's mail.
or verification concept, that no browser source imports Better Auth or Core's mail, and that the
shell's only use of browser storage is the sidebar's expanded/collapsed mode — `localStorage` named
only by the entry point, one write of one fixed key, and no storage API in any file that holds the
reset token ([ADR 0058](../architecture/decisions/0058-dashboard-shell-navigation-and-lucide-icons.md)).
Unit suites (`core/tests/module-storage.test.ts`, `core/tests/module-storage-lifecycle.test.ts`) use an
injected in-memory provider and never touch the network. They prove the parts the protocol cannot:
+12 -1
View File
@@ -4,6 +4,10 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { consumeAuthLanding, type HostAuthLanding, readAuthLanding } from "../src/auth-protocol.js";
import { HostApp } from "../src/browser/HostApp.js";
import { createMemoryHostHistory } from "../src/browser/host-history.js";
import {
createLocalSidebarPreference,
SIDEBAR_PREFERENCE_KEY,
} from "../src/browser/sidebar-preference.js";
import type { EkklesiaHostConfig } from "../src/host-protocol.js";
/**
@@ -99,6 +103,9 @@ async function render(config: EkklesiaHostConfig, authLanding?: HostAuthLanding)
<HostApp
config={config}
history={createMemoryHostHistory("/")}
// The production store over this browser's real storage, so the assertions below cover
// the shell's one use of it (ADR 0058) as well.
sidebarPreference={createLocalSidebarPreference(window.localStorage)}
{...(authLanding ? { authLanding } : {})}
/>,
);
@@ -463,7 +470,11 @@ describe("reset password", () => {
// A reset token is a bearer credential for one account. It lives in React state for the life of
// the form and nowhere else.
expect(localStorage.length).toBe(0);
// The only thing this browser's storage may ever hold is the sidebar's mode (ADR 0058).
for (let index = 0; index < localStorage.length; index += 1) {
expect(localStorage.key(index)).toBe(SIDEBAR_PREFERENCE_KEY);
}
expect(JSON.stringify({ ...localStorage })).not.toContain("reset-token-abc");
expect(sessionStorage.length).toBe(0);
expect(text()).not.toContain("reset-token-abc");
expect(container.innerHTML).not.toContain("reset-token-abc");