Skip to content

Commit b85b1c6

Browse files
authored
Refactor file access to use fs-safe primitives (#78255)
* refactor: use fs-safe primitives across file access * fix: preserve invalid managed npm manifests * fix: keep fs seams for startup metadata
1 parent 0d73f17 commit b85b1c6

56 files changed

Lines changed: 409 additions & 568 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

extensions/acpx/src/codex-auth-bridge.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import fsSync from "node:fs";
22
import fs from "node:fs/promises";
33
import { createRequire } from "node:module";
44
import path from "node:path";
5+
import { readJsonFileWithFallback } from "openclaw/plugin-sdk/json-store";
56
import { resolveAcpxPluginRoot } from "./config.js";
67
import type { ResolvedAcpxPluginConfig } from "./config.js";
78

@@ -113,7 +114,10 @@ async function resolveInstalledAcpPackageBinPath(
113114
): Promise<string | undefined> {
114115
try {
115116
const packageJsonPath = requireFromHere.resolve(`${packageName}/package.json`);
116-
const manifest = JSON.parse(await fs.readFile(packageJsonPath, "utf8")) as PackageManifest;
117+
const { value: manifest } = await readJsonFileWithFallback<PackageManifest>(
118+
packageJsonPath,
119+
{},
120+
);
117121
if (manifest.name !== packageName) {
118122
return undefined;
119123
}

extensions/browser/src/browser/chrome.profile-decoration.ts

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import fs from "node:fs";
22
import path from "node:path";
3+
import { loadJsonFile, saveJsonFile } from "openclaw/plugin-sdk/json-store";
34
import {
45
DEFAULT_OPENCLAW_BROWSER_COLOR,
56
DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME,
@@ -10,24 +11,14 @@ function decoratedMarkerPath(userDataDir: string) {
1011
}
1112

1213
function safeReadJson(filePath: string): Record<string, unknown> | null {
13-
try {
14-
if (!fs.existsSync(filePath)) {
15-
return null;
16-
}
17-
const raw = fs.readFileSync(filePath, "utf-8");
18-
const parsed = JSON.parse(raw) as unknown;
19-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
20-
return null;
21-
}
22-
return parsed as Record<string, unknown>;
23-
} catch {
24-
return null;
25-
}
14+
const parsed = loadJsonFile(filePath);
15+
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)
16+
? (parsed as Record<string, unknown>)
17+
: null;
2618
}
2719

2820
function safeWriteJson(filePath: string, data: Record<string, unknown>) {
29-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
30-
fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
21+
saveJsonFile(filePath, data);
3122
}
3223

3324
function asRecord(value: unknown): Record<string, unknown> | null {

extensions/codex/src/migration/helpers.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import fs from "node:fs/promises";
22
import os from "node:os";
33
import path from "node:path";
4+
import { readJsonFileWithFallback } from "openclaw/plugin-sdk/json-store";
45
import { pathExists } from "openclaw/plugin-sdk/security-runtime";
56

67
export async function exists(filePath: string): Promise<boolean> {
@@ -47,10 +48,8 @@ export async function readJsonObject(
4748
if (!filePath) {
4849
return {};
4950
}
50-
try {
51-
const parsed = JSON.parse(await fs.readFile(filePath, "utf8"));
52-
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
53-
} catch {
54-
return {};
55-
}
51+
const { value: parsed } = await readJsonFileWithFallback<unknown>(filePath, {});
52+
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
53+
? (parsed as Record<string, unknown>)
54+
: {};
5655
}

extensions/diffs/src/pierre-themes.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import fs from "node:fs/promises";
21
import { createRequire } from "node:module";
32
import type { ThemeRegistrationResolved } from "@pierre/diffs";
43
import { RegisteredCustomThemes, ResolvedThemes, ResolvingThemes } from "@pierre/diffs";
4+
import { readJsonFileWithFallback } from "openclaw/plugin-sdk/json-store";
55

66
type PierreThemeName = "pierre-dark" | "pierre-light";
77
const themeRequire = createRequire(import.meta.url);
@@ -20,8 +20,9 @@ function createThemeLoader(
2020
return cachedTheme;
2121
}
2222
const themePath = themeRequire.resolve(themeSpecifier);
23+
const { value: theme } = await readJsonFileWithFallback<Record<string, unknown>>(themePath, {});
2324
cachedTheme = {
24-
...(JSON.parse(await fs.readFile(themePath, "utf8")) as Record<string, unknown>),
25+
...theme,
2526
name: themeName,
2627
} as ThemeRegistrationResolved;
2728
return cachedTheme;

extensions/diffs/src/store.ts

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -174,13 +174,13 @@ export class DiffArtifactStore {
174174
}
175175

176176
async cleanupExpired(): Promise<void> {
177-
await this.ensureRoot();
178-
const entries = await fs.readdir(this.rootDir, { withFileTypes: true }).catch(() => []);
177+
const root = await this.artifactRoot();
178+
const entries = await root.list("", { withFileTypes: true }).catch(() => []);
179179
const now = Date.now();
180180

181181
await Promise.all(
182182
entries
183-
.filter((entry) => entry.isDirectory())
183+
.filter((entry) => entry.isDirectory)
184184
.map(async (entry) => {
185185
const id = entry.name;
186186
const meta = await this.readMeta(id);
@@ -199,12 +199,7 @@ export class DiffArtifactStore {
199199
return;
200200
}
201201

202-
const artifactPath = this.artifactDir(id);
203-
const stat = await fs.stat(artifactPath).catch(() => null);
204-
if (!stat) {
205-
return;
206-
}
207-
if (now - stat.mtimeMs > SWEEP_FALLBACK_AGE_MS) {
202+
if (now - entry.mtimeMs > SWEEP_FALLBACK_AGE_MS) {
208203
await this.deleteArtifact(id);
209204
}
210205
}),

extensions/matrix/src/legacy-crypto.ts

Lines changed: 13 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ import fs from "node:fs";
22
import os from "node:os";
33
import path from "node:path";
44
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
5-
import { writeJsonFileAtomically as writeJsonFileAtomicallyImpl } from "openclaw/plugin-sdk/json-store";
5+
import {
6+
loadJsonFile,
7+
writeJsonFileAtomically as writeJsonFileAtomicallyImpl,
8+
} from "openclaw/plugin-sdk/json-store";
69
import { resolveStateDir } from "openclaw/plugin-sdk/state-paths";
710
import { resolveConfiguredMatrixAccountIds } from "./account-selection.js";
811
import { isMatrixLegacyCryptoInspectorAvailable } from "./legacy-crypto-inspector-availability.js";
@@ -208,20 +211,13 @@ function resolveLegacyMatrixFlatStorePlan(params: {
208211
function loadLegacyBotSdkMetadata(cryptoRootDir: string): MatrixLegacyBotSdkMetadata {
209212
const metadataPath = path.join(cryptoRootDir, "bot-sdk.json");
210213
const fallback: MatrixLegacyBotSdkMetadata = { deviceId: null };
211-
try {
212-
if (!fs.existsSync(metadataPath)) {
213-
return fallback;
214-
}
215-
const parsed = JSON.parse(fs.readFileSync(metadataPath, "utf8")) as {
216-
deviceId?: unknown;
217-
};
218-
return {
219-
deviceId:
220-
typeof parsed.deviceId === "string" && parsed.deviceId.trim() ? parsed.deviceId : null,
221-
};
222-
} catch {
223-
return fallback;
224-
}
214+
const parsed = loadJsonFile<{ deviceId?: unknown }>(metadataPath);
215+
return {
216+
deviceId:
217+
typeof parsed?.deviceId === "string" && parsed.deviceId.trim()
218+
? parsed.deviceId
219+
: fallback.deviceId,
220+
};
225221
}
226222

227223
function resolveMatrixLegacyCryptoPlans(params: {
@@ -288,25 +284,11 @@ function resolveMatrixLegacyCryptoPlans(params: {
288284
}
289285

290286
function loadStoredRecoveryKey(filePath: string): MatrixStoredRecoveryKey | null {
291-
try {
292-
if (!fs.existsSync(filePath)) {
293-
return null;
294-
}
295-
return JSON.parse(fs.readFileSync(filePath, "utf8")) as MatrixStoredRecoveryKey;
296-
} catch {
297-
return null;
298-
}
287+
return loadJsonFile<MatrixStoredRecoveryKey>(filePath) ?? null;
299288
}
300289

301290
function loadLegacyCryptoMigrationState(filePath: string): MatrixLegacyCryptoMigrationState | null {
302-
try {
303-
if (!fs.existsSync(filePath)) {
304-
return null;
305-
}
306-
return JSON.parse(fs.readFileSync(filePath, "utf8")) as MatrixLegacyCryptoMigrationState;
307-
} catch {
308-
return null;
309-
}
291+
return loadJsonFile<MatrixLegacyCryptoMigrationState>(filePath) ?? null;
310292
}
311293

312294
async function persistLegacyMigrationState(params: {

extensions/matrix/src/matrix/client/storage.ts

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import os from "node:os";
33
import path from "node:path";
44
import { normalizeAccountId } from "openclaw/plugin-sdk/account-id";
55
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
6+
import { loadJsonFile, saveJsonFile } from "openclaw/plugin-sdk/json-store";
67
import {
78
requiresExplicitMatrixDefaultAccount,
89
resolveMatrixDefaultOrOnlyAccountId,
@@ -105,10 +106,10 @@ function resolveStorageRootMtimeMs(rootDir: string): number {
105106
function readStoredRootMetadata(rootDir: string): StoredRootMetadata {
106107
const metadata: StoredRootMetadata = {};
107108

108-
try {
109-
const parsed = JSON.parse(
110-
fs.readFileSync(path.join(rootDir, STORAGE_META_FILENAME), "utf8"),
111-
) as Partial<StoredRootMetadata>;
109+
const parsed = loadJsonFile<Partial<StoredRootMetadata>>(
110+
path.join(rootDir, STORAGE_META_FILENAME),
111+
);
112+
if (parsed) {
112113
if (typeof parsed.homeserver === "string" && parsed.homeserver.trim()) {
113114
metadata.homeserver = parsed.homeserver.trim();
114115
}
@@ -130,19 +131,17 @@ function readStoredRootMetadata(rootDir: string): StoredRootMetadata {
130131
if (typeof parsed.createdAt === "string" && parsed.createdAt.trim()) {
131132
metadata.createdAt = parsed.createdAt.trim();
132133
}
133-
} catch {
134-
// ignore missing or malformed storage metadata
135134
}
136135

137-
try {
138-
const parsed = JSON.parse(
139-
fs.readFileSync(path.join(rootDir, STARTUP_VERIFICATION_FILENAME), "utf8"),
140-
) as { deviceId?: unknown };
141-
if (!metadata.deviceId && typeof parsed.deviceId === "string" && parsed.deviceId.trim()) {
142-
metadata.deviceId = parsed.deviceId.trim();
143-
}
144-
} catch {
145-
// ignore missing or malformed verification state
136+
const verification = loadJsonFile<{ deviceId?: unknown }>(
137+
path.join(rootDir, STARTUP_VERIFICATION_FILENAME),
138+
);
139+
if (
140+
!metadata.deviceId &&
141+
typeof verification?.deviceId === "string" &&
142+
verification.deviceId.trim()
143+
) {
144+
metadata.deviceId = verification.deviceId.trim();
146145
}
147146

148147
return metadata;
@@ -473,8 +472,7 @@ function writeStoredRootMetadata(
473472
},
474473
): boolean {
475474
try {
476-
fs.mkdirSync(path.dirname(metaPath), { recursive: true });
477-
fs.writeFileSync(metaPath, JSON.stringify(payload, null, 2), "utf-8");
475+
saveJsonFile(metaPath, payload);
478476
return true;
479477
} catch {
480478
return false;

extensions/matrix/src/matrix/sdk/recovery-key-store.ts

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import fs from "node:fs";
2-
import path from "node:path";
31
import { decodeRecoveryKey } from "matrix-js-sdk/lib/crypto-api/recovery-key.js";
2+
import { loadJsonFile, saveJsonFile } from "openclaw/plugin-sdk/json-store";
43
import { formatMatrixErrorMessage, formatMatrixErrorReason } from "../errors.js";
54
import { LogService } from "./logger.js";
65
import type {
@@ -399,13 +398,9 @@ export class MatrixRecoveryKeyStore {
399398
return null;
400399
}
401400
try {
402-
if (!fs.existsSync(this.recoveryKeyPath)) {
403-
return null;
404-
}
405-
const raw = fs.readFileSync(this.recoveryKeyPath, "utf8");
406-
const parsed = JSON.parse(raw) as Partial<MatrixStoredRecoveryKey>;
401+
const parsed = loadJsonFile<Partial<MatrixStoredRecoveryKey>>(this.recoveryKeyPath);
407402
if (
408-
parsed.version !== 1 ||
403+
parsed?.version !== 1 ||
409404
typeof parsed.createdAt !== "string" ||
410405
typeof parsed.privateKeyBase64 !== "string" || // pragma: allowlist secret
411406
!parsed.privateKeyBase64.trim()
@@ -450,9 +445,7 @@ export class MatrixRecoveryKeyStore {
450445
}
451446
: undefined,
452447
};
453-
fs.mkdirSync(path.dirname(this.recoveryKeyPath), { recursive: true });
454-
fs.writeFileSync(this.recoveryKeyPath, JSON.stringify(payload, null, 2), "utf8");
455-
fs.chmodSync(this.recoveryKeyPath, 0o600);
448+
saveJsonFile(this.recoveryKeyPath, payload);
456449
} catch (err) {
457450
LogService.warn("MatrixClientLite", "Failed to persist recovery key:", err);
458451
}

extensions/memory-core/src/dreaming-phases.ts

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -396,10 +396,6 @@ type DailyIngestionState = {
396396
files: Record<string, DailyIngestionFileState>;
397397
};
398398

399-
function resolveDailyIngestionStatePath(workspaceDir: string): string {
400-
return path.join(workspaceDir, DAILY_INGESTION_STATE_RELATIVE_PATH);
401-
}
402-
403399
function normalizeDailyIngestionState(raw: unknown): DailyIngestionState {
404400
const record = asRecord(raw);
405401
const filesRaw = asRecord(record?.files);
@@ -442,10 +438,9 @@ function normalizeMemoryDay(value: unknown): string | undefined {
442438
}
443439

444440
async function readDailyIngestionState(workspaceDir: string): Promise<DailyIngestionState> {
445-
const statePath = resolveDailyIngestionStatePath(workspaceDir);
446441
try {
447442
return normalizeDailyIngestionState(
448-
await privateFileStore(workspaceDir).readJsonIfExists(path.relative(workspaceDir, statePath)),
443+
await privateFileStore(workspaceDir).readJsonIfExists(DAILY_INGESTION_STATE_RELATIVE_PATH),
449444
);
450445
} catch (err) {
451446
if (err instanceof SyntaxError) {
@@ -459,8 +454,7 @@ async function writeDailyIngestionState(
459454
workspaceDir: string,
460455
state: DailyIngestionState,
461456
): Promise<void> {
462-
const statePath = resolveDailyIngestionStatePath(workspaceDir);
463-
await privateFileStore(workspaceDir).writeJson(path.relative(workspaceDir, statePath), state, {
457+
await privateFileStore(workspaceDir).writeJson(DAILY_INGESTION_STATE_RELATIVE_PATH, state, {
464458
trailingNewline: true,
465459
});
466460
}
@@ -496,10 +490,6 @@ function normalizeWorkspaceKey(workspaceDir: string): string {
496490
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
497491
}
498492

499-
function resolveSessionIngestionStatePath(workspaceDir: string): string {
500-
return path.join(workspaceDir, SESSION_INGESTION_STATE_RELATIVE_PATH);
501-
}
502-
503493
function normalizeSessionIngestionState(raw: unknown): SessionIngestionState {
504494
const record = asRecord(raw);
505495
const filesRaw = asRecord(record?.files);
@@ -554,10 +544,9 @@ function normalizeSessionIngestionState(raw: unknown): SessionIngestionState {
554544
}
555545

556546
async function readSessionIngestionState(workspaceDir: string): Promise<SessionIngestionState> {
557-
const statePath = resolveSessionIngestionStatePath(workspaceDir);
558547
try {
559548
return normalizeSessionIngestionState(
560-
await privateFileStore(workspaceDir).readJsonIfExists(path.relative(workspaceDir, statePath)),
549+
await privateFileStore(workspaceDir).readJsonIfExists(SESSION_INGESTION_STATE_RELATIVE_PATH),
561550
);
562551
} catch (err) {
563552
if (err instanceof SyntaxError) {
@@ -571,8 +560,7 @@ async function writeSessionIngestionState(
571560
workspaceDir: string,
572561
state: SessionIngestionState,
573562
): Promise<void> {
574-
const statePath = resolveSessionIngestionStatePath(workspaceDir);
575-
await privateFileStore(workspaceDir).writeJson(path.relative(workspaceDir, statePath), state, {
563+
await privateFileStore(workspaceDir).writeJson(SESSION_INGESTION_STATE_RELATIVE_PATH, state, {
576564
trailingNewline: true,
577565
});
578566
}

0 commit comments

Comments
 (0)