Files
openclaw/src/logging/diagnostic-support-bundle.ts
Peter Steinberger 538605ff44 [codex] Extract filesystem safety primitives (#77918)
* refactor: extract filesystem safety primitives

* refactor: use fs-safe for file access helpers

* refactor: reuse fs-safe for media reads

* refactor: use fs-safe for image reads

* refactor: reuse fs-safe in qqbot media opener

* refactor: reuse fs-safe for local media checks

* refactor: consume cleaner fs-safe api

* refactor: align fs-safe json option names

* fix: preserve fs-safe migration contracts

* refactor: use fs-safe primitive subpaths

* refactor: use grouped fs-safe subpaths

* refactor: align fs-safe api usage

* refactor: adapt private state store api

* chore: refresh proof gate

* refactor: follow fs-safe json api split

* refactor: follow reduced fs-safe surface

* build: default fs-safe python helper off

* fix: preserve fs-safe plugin sdk aliases

* refactor: consolidate fs-safe usage

* refactor: unify fs-safe store usage

* refactor: trim fs-safe temp workspace usage

* refactor: hide low-level fs-safe primitives

* build: use published fs-safe package

* fix: preserve outbound recovery durability after rebase

* chore: refresh pr checks
2026-05-06 02:15:17 +01:00

134 lines
3.9 KiB
TypeScript

import fsp from "node:fs/promises";
import path from "node:path";
import { isPathInside } from "../infra/path-guards.js";
export type DiagnosticSupportBundleFile = {
path: string;
mediaType: string;
content: string;
};
export type DiagnosticSupportBundleContent = {
path: string;
mediaType: string;
bytes: number;
};
function supportBundleByteLength(content: string): number {
return Buffer.byteLength(content, "utf8");
}
export function jsonSupportBundleFile(
pathName: string,
value: unknown,
): DiagnosticSupportBundleFile {
return {
path: assertSafeBundleRelativePath(pathName),
mediaType: "application/json",
content: `${JSON.stringify(value, null, 2)}\n`,
};
}
export function jsonlSupportBundleFile(
pathName: string,
lines: readonly string[],
): DiagnosticSupportBundleFile {
return {
path: assertSafeBundleRelativePath(pathName),
mediaType: "application/x-ndjson",
content: `${lines.join("\n")}\n`,
};
}
export function textSupportBundleFile(
pathName: string,
content: string,
): DiagnosticSupportBundleFile {
return {
path: assertSafeBundleRelativePath(pathName),
mediaType: "text/plain; charset=utf-8",
content: content.endsWith("\n") ? content : `${content}\n`,
};
}
export function supportBundleContents(
files: readonly DiagnosticSupportBundleFile[],
): DiagnosticSupportBundleContent[] {
return files.map((file) => ({
path: file.path,
mediaType: file.mediaType,
bytes: supportBundleByteLength(file.content),
}));
}
function assertSafeBundleRelativePath(pathName: string): string {
const normalized = pathName.replaceAll("\\", "/");
if (
!normalized ||
normalized.startsWith("/") ||
normalized.split("/").some((part) => part === "" || part === "." || part === "..")
) {
throw new Error(`Invalid bundle file path: ${pathName}`);
}
return normalized;
}
async function prepareSupportBundleDirectory(outputDir: string): Promise<void> {
await fsp.mkdir(path.dirname(outputDir), { recursive: true, mode: 0o700 });
await fsp.mkdir(outputDir, { mode: 0o700 });
}
function resolveSupportBundleFilePath(outputDir: string, pathName: string): string {
const safePath = assertSafeBundleRelativePath(pathName);
const resolvedBase = path.resolve(outputDir);
const resolvedFile = path.resolve(resolvedBase, safePath);
if (resolvedFile === resolvedBase || !isPathInside(resolvedBase, resolvedFile)) {
throw new Error(`Bundle file path escaped output directory: ${pathName}`);
}
return resolvedFile;
}
async function writeSupportBundleFile(
outputDir: string,
file: DiagnosticSupportBundleFile,
): Promise<void> {
const filePath = resolveSupportBundleFilePath(outputDir, file.path);
await fsp.mkdir(path.dirname(filePath), { recursive: true, mode: 0o700 });
await fsp.writeFile(filePath, file.content, {
encoding: "utf8",
flag: "wx",
mode: 0o600,
});
}
export async function writeSupportBundleDirectory(params: {
outputDir: string;
files: readonly DiagnosticSupportBundleFile[];
}): Promise<DiagnosticSupportBundleContent[]> {
await prepareSupportBundleDirectory(params.outputDir);
for (const file of params.files) {
await writeSupportBundleFile(params.outputDir, file);
}
return supportBundleContents(params.files);
}
export async function writeSupportBundleZip(params: {
outputPath: string;
files: readonly DiagnosticSupportBundleFile[];
compressionLevel?: number;
}): Promise<number> {
const { default: JSZip } = await import("jszip");
const zip = new JSZip();
for (const file of params.files) {
zip.file(assertSafeBundleRelativePath(file.path), file.content);
}
const buffer = await zip.generateAsync({
type: "nodebuffer",
compression: "DEFLATE",
compressionOptions: { level: params.compressionLevel ?? 6 },
});
await fsp.mkdir(path.dirname(params.outputPath), { recursive: true, mode: 0o700 });
await fsp.writeFile(params.outputPath, buffer, { mode: 0o600 });
return buffer.length;
}