mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
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 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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
Reference in New Issue
Block a user