diff --git a/packages/server/src/server/package-version.ts b/packages/server/src/server/package-version.ts index 6b890aaad..e30844693 100644 --- a/packages/server/src/server/package-version.ts +++ b/packages/server/src/server/package-version.ts @@ -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; + +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; } diff --git a/packages/server/src/server/pid-lock.ts b/packages/server/src/server/pid-lock.ts index a90389677..96d663e89 100644 --- a/packages/server/src/server/pid-lock.ts +++ b/packages/server/src/server/pid-lock.ts @@ -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 {} + +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 { + 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; - 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)) {