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:
parent
1e6a2825d4
commit
4c30dc6e2e
1 changed files with 42 additions and 23 deletions
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue