mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-28 18:33:37 +00:00
refactor: add zod helpers for json file readers
This commit is contained in:
24
src/config/sessions/store-read.test.ts
Normal file
24
src/config/sessions/store-read.test.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { withTempDir } from "../../test-utils/temp-dir.js";
|
||||
import { readSessionStoreReadOnly } from "./store-read.js";
|
||||
|
||||
describe("readSessionStoreReadOnly", () => {
|
||||
it("returns an empty store for malformed or non-object JSON", async () => {
|
||||
await withTempDir("openclaw-session-store-", async (dir) => {
|
||||
const storePath = path.join(dir, "sessions.json");
|
||||
|
||||
await fs.writeFile(storePath, '["not-an-object"]\n', "utf8");
|
||||
expect(readSessionStoreReadOnly(storePath)).toEqual({});
|
||||
|
||||
await fs.writeFile(storePath, '{"session-1":{"sessionId":"s1","updatedAt":1}}\n', "utf8");
|
||||
expect(readSessionStoreReadOnly(storePath)).toMatchObject({
|
||||
"session-1": {
|
||||
sessionId: "s1",
|
||||
updatedAt: 1,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,9 +1,11 @@
|
||||
import fs from "node:fs";
|
||||
import { z } from "zod";
|
||||
import { safeParseJsonWithSchema } from "../../utils/zod-parse.js";
|
||||
import type { SessionEntry } from "./types.js";
|
||||
|
||||
function isSessionStoreRecord(value: unknown): value is Record<string, SessionEntry | undefined> {
|
||||
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
const SessionStoreSchema = z.record(z.string(), z.unknown()) as z.ZodType<
|
||||
Record<string, SessionEntry | undefined>
|
||||
>;
|
||||
|
||||
export function readSessionStoreReadOnly(
|
||||
storePath: string,
|
||||
@@ -13,8 +15,7 @@ export function readSessionStoreReadOnly(
|
||||
if (!raw.trim()) {
|
||||
return {};
|
||||
}
|
||||
const parsed = JSON.parse(raw);
|
||||
return isSessionStoreRecord(parsed) ? parsed : {};
|
||||
return safeParseJsonWithSchema(SessionStoreSchema, raw) ?? {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { z } from "zod";
|
||||
import { resolveStateDir } from "../config/paths.js";
|
||||
import {
|
||||
clearDeviceAuthTokenFromStore,
|
||||
@@ -8,8 +9,14 @@ import {
|
||||
storeDeviceAuthTokenInStore,
|
||||
} from "../shared/device-auth-store.js";
|
||||
import type { DeviceAuthStore } from "../shared/device-auth.js";
|
||||
import { safeParseJsonWithSchema } from "../utils/zod-parse.js";
|
||||
|
||||
const DEVICE_AUTH_FILE = "device-auth.json";
|
||||
const DeviceAuthStoreSchema = z.object({
|
||||
version: z.literal(1),
|
||||
deviceId: z.string(),
|
||||
tokens: z.record(z.string(), z.unknown()),
|
||||
}) as z.ZodType<DeviceAuthStore>;
|
||||
|
||||
function resolveDeviceAuthPath(env: NodeJS.ProcessEnv = process.env): string {
|
||||
return path.join(resolveStateDir(env), "identity", DEVICE_AUTH_FILE);
|
||||
@@ -21,14 +28,7 @@ function readStore(filePath: string): DeviceAuthStore | null {
|
||||
return null;
|
||||
}
|
||||
const raw = fs.readFileSync(filePath, "utf8");
|
||||
const parsed = JSON.parse(raw) as DeviceAuthStore;
|
||||
if (parsed?.version !== 1 || typeof parsed.deviceId !== "string") {
|
||||
return null;
|
||||
}
|
||||
if (!parsed.tokens || typeof parsed.tokens !== "object") {
|
||||
return null;
|
||||
}
|
||||
return parsed;
|
||||
return safeParseJsonWithSchema(DeviceAuthStoreSchema, raw);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -3,8 +3,10 @@ import fsSync from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
import net from "node:net";
|
||||
import path from "node:path";
|
||||
import { z } from "zod";
|
||||
import { resolveConfigPath, resolveGatewayLockDir, resolveStateDir } from "../config/paths.js";
|
||||
import { isPidAlive } from "../shared/pid-alive.js";
|
||||
import { safeParseJsonWithSchema } from "../utils/zod-parse.js";
|
||||
import { isGatewayArgv, parseProcCmdline } from "./gateway-process-argv.js";
|
||||
|
||||
const DEFAULT_TIMEOUT_MS = 5000;
|
||||
@@ -19,6 +21,13 @@ type LockPayload = {
|
||||
startTime?: number;
|
||||
};
|
||||
|
||||
const LockPayloadSchema = z.object({
|
||||
pid: z.number(),
|
||||
createdAt: z.string(),
|
||||
configPath: z.string(),
|
||||
startTime: z.number().optional(),
|
||||
}) as z.ZodType<LockPayload>;
|
||||
|
||||
export type GatewayLockHandle = {
|
||||
lockPath: string;
|
||||
configPath: string;
|
||||
@@ -142,23 +151,7 @@ async function resolveGatewayOwnerStatus(
|
||||
async function readLockPayload(lockPath: string): Promise<LockPayload | null> {
|
||||
try {
|
||||
const raw = await fs.readFile(lockPath, "utf8");
|
||||
const parsed = JSON.parse(raw) as Partial<LockPayload>;
|
||||
if (typeof parsed.pid !== "number") {
|
||||
return null;
|
||||
}
|
||||
if (typeof parsed.createdAt !== "string") {
|
||||
return null;
|
||||
}
|
||||
if (typeof parsed.configPath !== "string") {
|
||||
return null;
|
||||
}
|
||||
const startTime = typeof parsed.startTime === "number" ? parsed.startTime : undefined;
|
||||
return {
|
||||
pid: parsed.pid,
|
||||
createdAt: parsed.createdAt,
|
||||
configPath: parsed.configPath,
|
||||
startTime,
|
||||
};
|
||||
return safeParseJsonWithSchema(LockPayloadSchema, raw);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { z } from "zod";
|
||||
import { runPassiveAccountLifecycle } from "./channel-lifecycle.js";
|
||||
import { createLoggerBackedRuntime } from "./runtime.js";
|
||||
export { safeParseJsonWithSchema, safeParseWithSchema } from "../utils/zod-parse.js";
|
||||
|
||||
type PassiveChannelStatusSnapshot = {
|
||||
configured?: boolean;
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { z } from "zod";
|
||||
import { safeParseJsonWithSchema } from "../utils/zod-parse.js";
|
||||
|
||||
export type TailscaleStatusCommandResult = {
|
||||
code: number | null;
|
||||
stdout: string;
|
||||
@@ -13,30 +16,31 @@ const TAILSCALE_STATUS_COMMAND_CANDIDATES = [
|
||||
"/Applications/Tailscale.app/Contents/MacOS/Tailscale",
|
||||
];
|
||||
|
||||
function parsePossiblyNoisyJsonObject(raw: string): Record<string, unknown> {
|
||||
const TailscaleStatusSchema = z.object({
|
||||
Self: z
|
||||
.object({
|
||||
DNSName: z.string().optional(),
|
||||
TailscaleIPs: z.array(z.string()).optional(),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
function parsePossiblyNoisyStatus(raw: string): z.infer<typeof TailscaleStatusSchema> | null {
|
||||
const start = raw.indexOf("{");
|
||||
const end = raw.lastIndexOf("}");
|
||||
if (start === -1 || end <= start) {
|
||||
return {};
|
||||
}
|
||||
try {
|
||||
return JSON.parse(raw.slice(start, end + 1)) as Record<string, unknown>;
|
||||
} catch {
|
||||
return {};
|
||||
return null;
|
||||
}
|
||||
return safeParseJsonWithSchema(TailscaleStatusSchema, raw.slice(start, end + 1));
|
||||
}
|
||||
|
||||
function extractTailnetHostFromStatusJson(raw: string): string | null {
|
||||
const parsed = parsePossiblyNoisyJsonObject(raw);
|
||||
const self =
|
||||
typeof parsed.Self === "object" && parsed.Self !== null
|
||||
? (parsed.Self as Record<string, unknown>)
|
||||
: undefined;
|
||||
const dns = typeof self?.DNSName === "string" ? self.DNSName : undefined;
|
||||
const parsed = parsePossiblyNoisyStatus(raw);
|
||||
const dns = parsed?.Self?.DNSName;
|
||||
if (dns && dns.length > 0) {
|
||||
return dns.replace(/\.$/, "");
|
||||
}
|
||||
const ips = Array.isArray(self?.TailscaleIPs) ? (self.TailscaleIPs as string[]) : [];
|
||||
const ips = parsed?.Self?.TailscaleIPs ?? [];
|
||||
return ips.length > 0 ? (ips[0] ?? null) : null;
|
||||
}
|
||||
|
||||
|
||||
14
src/utils/zod-parse.ts
Normal file
14
src/utils/zod-parse.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import type { ZodType } from "zod";
|
||||
|
||||
export function safeParseWithSchema<T>(schema: ZodType<T>, value: unknown): T | null {
|
||||
const parsed = schema.safeParse(value);
|
||||
return parsed.success ? parsed.data : null;
|
||||
}
|
||||
|
||||
export function safeParseJsonWithSchema<T>(schema: ZodType<T>, raw: string): T | null {
|
||||
try {
|
||||
return safeParseWithSchema(schema, JSON.parse(raw));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user