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-04 16:53:40 +08:00
committed by GitHub
parent 7fc4dd1e86
commit f4dcb3954a
3 changed files with 56 additions and 24 deletions

View File

@@ -1,15 +1,23 @@
import { existsSync, readFileSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { z } from "zod";
interface ResolvePackageVersionParams {
moduleUrl?: string;
packageName: string;
}
interface PackageJson {
name?: unknown;
version?: unknown;
export const packageJsonSchema = z.object({
name: z.string().optional(),
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 {
@@ -26,7 +34,9 @@ function readMatchingPackageVersion(
): string | null {
let packageJson: PackageJson;
try {
packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8")) as PackageJson;
const parsed = parsePackageJson(JSON.parse(readFileSync(packageJsonPath, "utf8")));
if (!parsed) return null;
packageJson = parsed;
} catch {
return null;
}

View File

@@ -2,14 +2,26 @@ import { open, readFile, unlink, mkdir } from "node:fs/promises";
import { existsSync } from "node:fs";
import { join } from "node:path";
import { hostname } from "node:os";
import { z } from "zod";
export interface PidLockInfo {
pid: number;
startedAt: string;
hostname: string;
uid: number;
listen: string | null;
desktopManaged?: boolean;
export const pidLockInfoSchema = z.object({
pid: z.number(),
startedAt: z.string(),
hostname: z.string(),
uid: z.number(),
listen: z.string().nullable(),
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 {
@@ -58,7 +70,7 @@ export async function acquirePidLock(
let existingLock: PidLockInfo | null = null;
try {
const content = await readFile(pidPath, "utf-8");
existingLock = JSON.parse(content) as PidLockInfo;
existingLock = parsePidLockInfo(JSON.parse(content));
} catch {
// No existing lock or invalid JSON - that's fine
}
@@ -95,16 +107,19 @@ export async function acquirePidLock(
fd = await open(pidPath, "wx");
await fd.write(JSON.stringify(lockInfo));
} catch (err) {
if ((err as NodeJS.ErrnoException).code === "EEXIST") {
if (isErrnoException(err) && err.code === "EEXIST") {
// Race condition - another process created the file
// Re-read and check
try {
const content = await readFile(pidPath, "utf-8");
const raceLock = JSON.parse(content) as PidLockInfo;
throw new PidLockError(
`Another Paseo daemon is already running (PID ${raceLock.pid})`,
raceLock,
);
const raceLock = parsePidLockInfo(JSON.parse(content));
if (raceLock) {
throw new PidLockError(
`Another Paseo daemon is already running (PID ${raceLock.pid})`,
raceLock,
);
}
throw new PidLockError("Failed to acquire PID lock due to race condition");
} catch (innerErr) {
if (innerErr instanceof PidLockError) throw innerErr;
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 lockOwnerPid = resolveOwnerPid(options?.ownerPid);
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) {
throw new PidLockError(`Cannot update PID lock owned by PID ${existingLock.pid}`, existingLock);
@@ -153,8 +171,8 @@ export async function releasePidLock(
try {
// Only remove if it's our lock
const content = await readFile(pidPath, "utf-8");
const lock = JSON.parse(content) as PidLockInfo;
if (lock.pid === lockOwnerPid) {
const lock = parsePidLockInfo(JSON.parse(content));
if (lock?.pid === lockOwnerPid) {
await unlink(pidPath);
}
} catch {
@@ -166,7 +184,7 @@ export async function getPidLockInfo(paseoHome: string): Promise<PidLockInfo | n
const pidPath = getPidFilePath(paseoHome);
try {
const content = await readFile(pidPath, "utf-8");
return JSON.parse(content) as PidLockInfo;
return parsePidLockInfo(JSON.parse(content));
} catch {
return null;
}

View File

@@ -57,6 +57,10 @@ function normalizeRelaySendPayload(data: string | Uint8Array | ArrayBuffer): str
return String(data);
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null;
}
function tryParseControlMessage(raw: unknown): ControlMessage | null {
try {
let text: string;
@@ -67,8 +71,8 @@ function tryParseControlMessage(raw: unknown): ControlMessage | null {
} else {
text = String(raw);
}
const parsed = JSON.parse(text) as Record<string, unknown>;
if (!parsed || typeof parsed !== "object") return null;
const parsed = JSON.parse(text);
if (!isRecord(parsed)) return null;
if (parsed.type === "ping") return { type: "ping" };
if (parsed.type === "pong") return { type: "pong" };
if (parsed.type === "sync" && Array.isArray(parsed.connectionIds)) {