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:
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)) {
|
||||||
|
|||||||
Reference in New Issue
Block a user