refactor(server): replace JSON.parse type assertions with Zod validation (#691)

Replace unsafe 'JSON.parse(content) as Type' patterns with proper Zod schema
validation. This fixes type-aware lint errors and provides runtime safety.

- pid-lock.ts: Add pidLockInfoSchema and parsePidLockInfo helper
- package-version.ts: Add packageJsonSchema and parsePackageJson helper
- relay-transport.ts: Add isRecord type guard to avoid unsafe type assertion
This commit is contained in:
Mohamed Boudra
2026-05-05 19:56:51 +07:00
parent 44d13919d7
commit 946152d820
3 changed files with 56 additions and 24 deletions
+14 -4
View File
@@ -1,15 +1,23 @@
import { existsSync, readFileSync } from "node:fs"; import { existsSync, readFileSync } from "node:fs";
import path from "node:path"; import path from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import { z } from "zod";
interface ResolvePackageVersionParams { interface ResolvePackageVersionParams {
moduleUrl?: string; moduleUrl?: string;
packageName: string; packageName: string;
} }
interface PackageJson { export const packageJsonSchema = z.object({
name?: unknown; name: z.string().optional(),
version?: unknown; version: z.string().optional(),
});
export type PackageJson = z.infer<typeof packageJsonSchema>;
function parsePackageJson(raw: unknown): PackageJson | null {
const result = packageJsonSchema.safeParse(raw);
return result.success ? result.data : null;
} }
export class PackageVersionResolutionError extends Error { export class PackageVersionResolutionError extends Error {
@@ -26,7 +34,9 @@ function readMatchingPackageVersion(
): string | null { ): string | null {
let packageJson: PackageJson; let packageJson: PackageJson;
try { try {
packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8")) as PackageJson; const parsed = parsePackageJson(JSON.parse(readFileSync(packageJsonPath, "utf8")));
if (!parsed) return null;
packageJson = parsed;
} catch { } catch {
return null; return null;
} }
+36 -18
View File
@@ -2,14 +2,26 @@ import { open, readFile, unlink, mkdir } from "node:fs/promises";
import { existsSync } from "node:fs"; import { existsSync } from "node:fs";
import { join } from "node:path"; import { join } from "node:path";
import { hostname } from "node:os"; import { hostname } from "node:os";
import { z } from "zod";
export interface PidLockInfo { export const pidLockInfoSchema = z.object({
pid: number; pid: z.number(),
startedAt: string; startedAt: z.string(),
hostname: string; hostname: z.string(),
uid: number; uid: z.number(),
listen: string | null; listen: z.string().nullable(),
desktopManaged?: boolean; desktopManaged: z.boolean().optional(),
});
export interface PidLockInfo extends z.infer<typeof pidLockInfoSchema> {}
function parsePidLockInfo(raw: unknown): PidLockInfo | null {
const result = pidLockInfoSchema.safeParse(raw);
return result.success ? result.data : null;
}
function isErrnoException(err: unknown): err is NodeJS.ErrnoException {
return err instanceof Error && "code" in err;
} }
export class PidLockError extends Error { export class PidLockError extends Error {
@@ -58,7 +70,7 @@ export async function acquirePidLock(
let existingLock: PidLockInfo | null = null; let existingLock: PidLockInfo | null = null;
try { try {
const content = await readFile(pidPath, "utf-8"); const content = await readFile(pidPath, "utf-8");
existingLock = JSON.parse(content) as PidLockInfo; existingLock = parsePidLockInfo(JSON.parse(content));
} catch { } catch {
// No existing lock or invalid JSON - that's fine // No existing lock or invalid JSON - that's fine
} }
@@ -95,16 +107,19 @@ export async function acquirePidLock(
fd = await open(pidPath, "wx"); fd = await open(pidPath, "wx");
await fd.write(JSON.stringify(lockInfo)); await fd.write(JSON.stringify(lockInfo));
} catch (err) { } catch (err) {
if ((err as NodeJS.ErrnoException).code === "EEXIST") { if (isErrnoException(err) && err.code === "EEXIST") {
// Race condition - another process created the file // Race condition - another process created the file
// Re-read and check // Re-read and check
try { try {
const content = await readFile(pidPath, "utf-8"); const content = await readFile(pidPath, "utf-8");
const raceLock = JSON.parse(content) as PidLockInfo; const raceLock = parsePidLockInfo(JSON.parse(content));
throw new PidLockError( if (raceLock) {
`Another Paseo daemon is already running (PID ${raceLock.pid})`, throw new PidLockError(
raceLock, `Another Paseo daemon is already running (PID ${raceLock.pid})`,
); raceLock,
);
}
throw new PidLockError("Failed to acquire PID lock due to race condition");
} catch (innerErr) { } catch (innerErr) {
if (innerErr instanceof PidLockError) throw innerErr; if (innerErr instanceof PidLockError) throw innerErr;
throw new PidLockError("Failed to acquire PID lock due to race condition"); throw new PidLockError("Failed to acquire PID lock due to race condition");
@@ -124,7 +139,10 @@ export async function updatePidLock(
const pidPath = getPidFilePath(paseoHome); const pidPath = getPidFilePath(paseoHome);
const lockOwnerPid = resolveOwnerPid(options?.ownerPid); const lockOwnerPid = resolveOwnerPid(options?.ownerPid);
const content = await readFile(pidPath, "utf-8"); const content = await readFile(pidPath, "utf-8");
const existingLock = JSON.parse(content) as PidLockInfo; const existingLock = parsePidLockInfo(JSON.parse(content));
if (!existingLock) {
throw new PidLockError("Cannot update PID lock: invalid lock file");
}
if (existingLock.pid !== lockOwnerPid) { if (existingLock.pid !== lockOwnerPid) {
throw new PidLockError(`Cannot update PID lock owned by PID ${existingLock.pid}`, existingLock); throw new PidLockError(`Cannot update PID lock owned by PID ${existingLock.pid}`, existingLock);
@@ -153,8 +171,8 @@ export async function releasePidLock(
try { try {
// Only remove if it's our lock // Only remove if it's our lock
const content = await readFile(pidPath, "utf-8"); const content = await readFile(pidPath, "utf-8");
const lock = JSON.parse(content) as PidLockInfo; const lock = parsePidLockInfo(JSON.parse(content));
if (lock.pid === lockOwnerPid) { if (lock?.pid === lockOwnerPid) {
await unlink(pidPath); await unlink(pidPath);
} }
} catch { } catch {
@@ -166,7 +184,7 @@ export async function getPidLockInfo(paseoHome: string): Promise<PidLockInfo | n
const pidPath = getPidFilePath(paseoHome); const pidPath = getPidFilePath(paseoHome);
try { try {
const content = await readFile(pidPath, "utf-8"); const content = await readFile(pidPath, "utf-8");
return JSON.parse(content) as PidLockInfo; return parsePidLockInfo(JSON.parse(content));
} catch { } catch {
return null; return null;
} }
@@ -57,6 +57,10 @@ function normalizeRelaySendPayload(data: string | Uint8Array | ArrayBuffer): str
return String(data); return String(data);
} }
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null;
}
function tryParseControlMessage(raw: unknown): ControlMessage | null { function tryParseControlMessage(raw: unknown): ControlMessage | null {
try { try {
let text: string; let text: string;
@@ -67,8 +71,8 @@ function tryParseControlMessage(raw: unknown): ControlMessage | null {
} else { } else {
text = String(raw); text = String(raw);
} }
const parsed = JSON.parse(text) as Record<string, unknown>; const parsed = JSON.parse(text);
if (!parsed || typeof parsed !== "object") return null; if (!isRecord(parsed)) return null;
if (parsed.type === "ping") return { type: "ping" }; if (parsed.type === "ping") return { type: "ping" };
if (parsed.type === "pong") return { type: "pong" }; if (parsed.type === "pong") return { type: "pong" };
if (parsed.type === "sync" && Array.isArray(parsed.connectionIds)) { if (parsed.type === "sync" && Array.isArray(parsed.connectionIds)) {