aboutsummaryrefslogtreecommitdiff
path: root/src/profile/store.ts
diff options
context:
space:
mode:
authoramankumarsingh77 <amankumarsingh7702@gmail.com>2026-07-28 00:53:19 +0530
committerAman Kumar <145339995+amankumarsingh77@users.noreply.github.com>2026-08-02 19:12:46 +0530
commitb9c77bb6f9b8b83574489344ef6efd207a611e98 (patch)
tree3c59bf53bcce3ac5c6bbf9a22884b6e6f787dc91 /src/profile/store.ts
parent1c73f4af643aa60ca554a3770264791d299e64e2 (diff)
refactor: validate JSON at every parse boundary
Ten sites cast or implicitly typed JSON.parse output; JSON.parse("null") would have satisfied all of them. parseJson returns a Result and validates against a typebox schema. js.ts keeps unknown — user script output has no fixed shape. daemon/bridge.ts assigned JSON.parse straight to CdpRawMessage with no guard at all; it now uses isCdpRawMessage like the transport does. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'src/profile/store.ts')
-rw-r--r--src/profile/store.ts59
1 files changed, 34 insertions, 25 deletions
diff --git a/src/profile/store.ts b/src/profile/store.ts
index c8b8952..e38ff2e 100644
--- a/src/profile/store.ts
+++ b/src/profile/store.ts
@@ -18,6 +18,9 @@
import { mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
import { randomUUID } from "node:crypto";
import { dirname } from "node:path";
+import { Type } from "typebox";
+import { Compile } from "typebox/compile";
+import { parseJson } from "../schemas/parse";
import { type Result, err, ok } from "../util/result";
import { pinFilePath } from "./paths";
@@ -39,22 +42,26 @@ type PinFile = {
const CURRENT_VERSION = 1;
-const parsePin = (value: unknown): ProfilePin | null => {
- if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
- const o = value as Record<string, unknown>;
- const userDataDir = o["userDataDir"];
- const profileDir = o["profileDir"];
- const label = o["label"];
- const savedAt = o["savedAt"];
- if (typeof userDataDir !== "string" || userDataDir.length === 0) return null;
- if (typeof profileDir !== "string" || profileDir.length === 0) return null;
- return {
- userDataDir,
- profileDir,
- label: typeof label === "string" && label.length > 0 ? label : profileDir,
- savedAt: typeof savedAt === "string" ? savedAt : "",
- };
-};
+const PinFileSchema = Type.Object(
+ {
+ version: Type.Literal(CURRENT_VERSION),
+ profile: Type.Union([
+ Type.Object(
+ {
+ userDataDir: Type.String({ minLength: 1 }),
+ profileDir: Type.String({ minLength: 1 }),
+ label: Type.Optional(Type.Unknown()),
+ savedAt: Type.Optional(Type.Unknown()),
+ },
+ { additionalProperties: true },
+ ),
+ Type.Null(),
+ ]),
+ },
+ { additionalProperties: true },
+);
+
+const pinFileValidator = Compile(PinFileSchema);
/**
* The persisted pin, or null when nothing is pinned, the file is missing, or
@@ -68,15 +75,17 @@ export const readPin = async (): Promise<ProfilePin | null> => {
} catch {
return null;
}
- try {
- const parsed: unknown = JSON.parse(raw);
- if (typeof parsed !== "object" || parsed === null) return null;
- const file = parsed as Record<string, unknown>;
- if (file["version"] !== CURRENT_VERSION) return null;
- return parsePin(file["profile"]);
- } catch {
- return null;
- }
+ const parsed = parseJson(raw, pinFileValidator);
+ if (!parsed.success) return null;
+ const profile = parsed.data.profile;
+ if (profile === null) return null;
+ const { userDataDir, profileDir, label, savedAt } = profile;
+ return {
+ userDataDir,
+ profileDir,
+ label: typeof label === "string" && label.length > 0 ? label : profileDir,
+ savedAt: typeof savedAt === "string" ? savedAt : "",
+ };
};
/**