test(auth): make the browser-safe guard transitive

Extend the Node-free guard beyond oauth-persist.ts to every module
reachable from the browser's RemoteOAuthStorage (storage-remote,
oauth-storage, store, oauth-persist, store-serialize), so a forbidden
store-io/node:/atomically import added one hop away is caught too — not
just a direct import in oauth-persist.ts. Addresses the non-blocking
hardening note on Claude review of #1612.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
cliffhall 2026-07-08 16:25:14 -04:00
commit 4c30dc6e2e

View file

@ -4,17 +4,35 @@ import { fileURLToPath } from "node:url";
import { dirname, resolve } from "node:path";
/**
* Regression guard for the browser bundle. `core/auth/oauth-persist.ts` is
* imported by browser code (App.tsx -> RemoteOAuthStorage -> oauth-persist),
* so it must stay Node-free. It previously imported `store-io.js`, which pulls
* `node:fs` + `atomically` (-> `stubborn-fs` -> `node:process.getuid`) into the
* browser bundle and blanked the app at runtime. The Node-only file backend
* now lives in `core/auth/node/oauth-persist-file.ts`; the isomorphic module
* must only import the Node-free `store-serialize` helpers.
* Regression guard for the browser bundle. The browser's `RemoteOAuthStorage`
* (App.tsx -> lib/remoteOAuthStorage.ts -> remote/storage-remote.ts) chains
* through a handful of `core/auth` + `core/storage` modules, so ALL of them
* must stay Node-free. `oauth-persist.ts` previously value-imported
* `store-io.js`, which pulls `node:fs` + `atomically` (-> `stubborn-fs` ->
* `node:process.getuid`) into the browser bundle and blanked the app at
* runtime. The Node-only file backend now lives in
* `core/auth/node/oauth-persist-file.ts`.
*
* This guard is transitive: it scans every module reachable from
* `storage-remote.ts` (short of the `node/` boundary), so a forbidden import
* added one hop away — e.g. through `store-serialize.ts` or `store.ts` — is
* caught too, not just a direct import in `oauth-persist.ts`.
*/
const here = dirname(fileURLToPath(import.meta.url));
const coreAuth = resolve(here, "../../../../../../core/auth");
const coreDir = resolve(here, "../../../../../../core");
// Browser-reachable modules (value-import graph from storage-remote.ts,
// excluding the intentional `node/` backend which is never bundled).
const BROWSER_REACHABLE = [
"auth/remote/storage-remote.ts",
"auth/oauth-storage.ts",
"auth/store.ts",
"auth/oauth-persist.ts",
"storage/store-serialize.ts",
];
const FORBIDDEN = [/(^|\/)store-io(\.js)?$/, /^node:/, /^atomically$/];
function importSpecifiers(source: string): string[] {
const specs: string[] = [];
@ -26,22 +44,23 @@ function importSpecifiers(source: string): string[] {
return specs;
}
describe("oauth-persist.ts stays browser-safe", () => {
const source = readFileSync(resolve(coreAuth, "oauth-persist.ts"), "utf-8");
const specs = importSpecifiers(source);
describe("browser-reachable OAuth storage modules stay Node-free", () => {
it.each(BROWSER_REACHABLE)(
"%s imports no Node built-ins, atomically, or store-io",
(relPath) => {
const source = readFileSync(resolve(coreDir, relPath), "utf-8");
const offenders = importSpecifiers(source).filter((spec) =>
FORBIDDEN.some((re) => re.test(spec)),
);
expect(offenders).toEqual([]);
},
);
it("does not import the Node-only store-io module", () => {
expect(specs).not.toContain("../storage/store-io.js");
});
it("does not import node: builtins or atomically", () => {
const forbidden = specs.filter(
(s) => s.startsWith("node:") || s === "atomically",
it("oauth-persist.ts sources its (de)serializers from store-serialize", () => {
const source = readFileSync(
resolve(coreDir, "auth/oauth-persist.ts"),
"utf-8",
);
expect(forbidden).toEqual([]);
});
it("gets its (de)serializers from the Node-free store-serialize module", () => {
expect(specs).toContain("../storage/store-serialize.js");
expect(importSpecifiers(source)).toContain("../storage/store-serialize.js");
});
});