UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

263 lines (262 loc) • 14.8 kB
import { c as isRecord } from "./record-coerce-TIsquoVR.mjs"; import { a as normalizeWindowsPathForComparison } from "./path-guards-CH7noY0Z.mjs"; import { Jn as string, Lt as boolean, Nt as array, Pn as number, Yt as discriminatedUnion, or as union, qn as strictObject, xn as literal } from "./schemas-BOYIvvln.mjs"; import { a as UpdateRunDriverSchema } from "./update-run-schema-DOichlUj.mjs"; import { n as normalizeAgentId } from "./agent-id-D1-Q6vR4.mjs"; import { h as normalizeArchiveRoot, m as normalizeArchivePath, p as isArchivePathWithin, r as buildBackupArchivePath } from "./backup-shared-D8dux81d.mjs"; import path from "node:path"; //#region src/commands/backup-verify-manifest.ts const recoveryPath = string().min(1).refine((value) => !value.includes("\0") && path.resolve(value) === value); const recoveryDigest = string().regex(/^[a-f0-9]{64}$/u); const recoveryEntry = discriminatedUnion("kind", [ strictObject({ kind: literal("file"), sourcePath: recoveryPath, archivePath: string().regex(/^payload\/\d+$/u), size: number().int().nonnegative(), sha256: recoveryDigest, sqlite: boolean(), mode: number().int().nonnegative() }), strictObject({ kind: literal("directory"), sourcePath: recoveryPath, mode: number().int().nonnegative() }), strictObject({ kind: literal("symlink"), sourcePath: recoveryPath, target: string().refine((value) => !value.includes("\0")), contentPath: recoveryPath.optional() }), strictObject({ kind: literal("missing"), sourcePath: recoveryPath, sqlite: boolean(), directory: boolean() }) ]); const updateRecoveryManifestSchema = strictObject({ schemaVersion: union([literal(1), literal(2)]), kind: literal("update-recovery"), generation: discriminatedUnion("kind", [ strictObject({ kind: literal("baseline") }), strictObject({ kind: literal("candidate"), baselineSha256: recoveryDigest }), strictObject({ kind: literal("prepared"), baselineSha256: recoveryDigest, candidateSha256: recoveryDigest }) ]).optional(), databases: array(discriminatedUnion("role", [strictObject({ path: recoveryPath, role: literal("global") }), strictObject({ path: recoveryPath, role: literal("agent"), agentId: string().min(1) })])).optional(), runId: string().regex(/^[a-zA-Z0-9_-]{1,128}$/u), installRoot: recoveryPath, stateDir: recoveryPath, configPath: recoveryPath, configPaths: array(recoveryPath).min(1).max(512), creator: UpdateRunDriverSchema, drivers: array(UpdateRunDriverSchema).max(32), createdAt: string().datetime(), roots: array(recoveryPath).min(1), excludedRoots: array(recoveryPath), protectedPaths: array(recoveryPath), entries: array(recoveryEntry).max(1e6), warnings: array(strictObject({ kind: literal("undeclared-migration-resources"), pluginId: string().min(1), message: string().min(1) })).optional() }); /** Update recovery binds every payload; ordinary archive manifests retain their existing contract. */ function parseUpdateRecoveryBackupManifest(raw) { const manifest = updateRecoveryManifestSchema.parse(JSON.parse(raw)); if (manifest.schemaVersion === 1 && (manifest.generation || manifest.databases) || manifest.schemaVersion === 2 && (!manifest.generation || !manifest.databases)) throw new Error("Update recovery generation metadata does not match its format version."); const databasePaths = /* @__PURE__ */ new Set(); let globalDatabasePath; for (const database of manifest.databases ?? []) { if (databasePaths.has(database.path) || database.role === "global" && globalDatabasePath !== void 0 || database.role === "agent" && normalizeAgentId(database.agentId) !== database.agentId) throw new Error(`Invalid update recovery database identity: ${database.path}`); databasePaths.add(database.path); if (database.role === "global") globalDatabasePath = database.path; } const sources = /* @__PURE__ */ new Map(); const payloads = /* @__PURE__ */ new Set(); const excluded = new Set(manifest.excludedRoots); if (excluded.size !== manifest.excludedRoots.length || [ ...manifest.roots, ...manifest.protectedPaths, ...manifest.configPaths ].some((pathname) => excluded.has(pathname))) throw new Error("Update recovery exclusions conflict with retained resource inventory."); for (const entry of manifest.entries) { if (excluded.has(entry.sourcePath)) throw new Error(`Excluded update recovery source has a retained entry: ${entry.sourcePath}`); if (entry.kind === "missing" && entry.sqlite && entry.directory) throw new Error("Missing SQLite inventory cannot describe a directory."); if (sources.has(entry.sourcePath) || !manifest.roots.some((root) => { const relative = path.relative(root, entry.sourcePath); return relative === "" || !path.isAbsolute(relative) && relative !== ".." && !relative.startsWith(`..${path.sep}`); })) throw new Error(`Invalid update recovery source: ${entry.sourcePath}`); sources.set(entry.sourcePath, entry); if (entry.kind === "file") { if (payloads.has(entry.archivePath)) throw new Error(`Duplicate update recovery payload: ${entry.archivePath}`); payloads.add(entry.archivePath); } } for (const database of manifest.databases ?? []) { const entry = sources.get(database.path); if (!entry || !((entry.kind === "file" || entry.kind === "missing" && !entry.directory) && entry.sqlite)) throw new Error(`Update recovery database is missing its SQLite inventory: ${database.path}`); } if (manifest.roots.some((root) => !sources.has(root))) throw new Error("Update recovery manifest is missing a root entry."); if (!manifest.configPaths.includes(manifest.configPath) || new Set(manifest.configPaths).size !== manifest.configPaths.length) throw new Error("Update recovery manifest is missing its configuration inventory."); for (const pathname of manifest.configPaths) { const config = sources.get(pathname); const content = config?.kind === "symlink" && config.contentPath ? sources.get(config.contentPath) : void 0; if (!config || config.kind === "directory" || config.kind === "file" && config.sqlite || config.kind === "missing" && (config.directory || config.sqlite) || config.kind === "symlink" && (!config.contentPath || !manifest.configPaths.includes(config.contentPath) || !content || !((content.kind === "file" || content.kind === "missing" && !content.directory) && !content.sqlite))) throw new Error("Update recovery manifest is missing its configuration inventory."); } return manifest; } function backupManifestSizeError(bytes) { const maxBytes = 1048576; return bytes > maxBytes ? /* @__PURE__ */ new Error(`Backup manifest exceeds ${maxBytes} byte limit.`) : void 0; } function parseBackupManifestSourcePath(value, label) { if (typeof value !== "string" || value.includes("\0")) throw new Error(`Backup manifest ${label} has an invalid sourcePath.`); const windowsPath = /^(?:[A-Za-z]:[\\/]|\\\\(?![?.]\\))/u.test(value); const normalized = windowsPath ? path.win32.normalize(value) : path.posix.normalize(value); if (!windowsPath && !value.startsWith("/") || normalized !== value) throw new Error(`Backup manifest ${label} sourcePath must be absolute and normalized.`); return value; } function parseBackupManifestAgentRoots(value) { if (value === void 0) return; if (!Array.isArray(value)) throw new Error("Backup manifest agentRoots must be an array."); const agentRoots = []; const seenAgentIds = /* @__PURE__ */ new Set(); const seenSourcePaths = /* @__PURE__ */ new Set(); for (const agentRoot of value) { if (!isRecord(agentRoot) || Object.keys(agentRoot).length !== 2 || !Object.hasOwn(agentRoot, "agentId") || !Object.hasOwn(agentRoot, "sourcePath")) throw new Error("Backup manifest agent root must contain only agentId and sourcePath."); const { agentId, sourcePath } = agentRoot; if (typeof agentId !== "string" || !agentId || normalizeAgentId(agentId) !== agentId) throw new Error("Backup manifest agent root has an invalid or noncanonical agentId."); const normalizedSourcePath = parseBackupManifestSourcePath(sourcePath, "agent root"); const sourcePathKey = /^(?:[A-Za-z]:[\\/]|\\\\(?![?.]\\))/u.test(normalizedSourcePath) ? normalizeWindowsPathForComparison(normalizedSourcePath) : normalizedSourcePath; if (seenAgentIds.has(agentId) || seenSourcePaths.has(sourcePathKey)) throw new Error("Backup manifest contains duplicate agent root ownership."); seenAgentIds.add(agentId); seenSourcePaths.add(sourcePathKey); agentRoots.push({ agentId, sourcePath: normalizedSourcePath }); } return agentRoots; } function parseBackupManifestSqliteSnapshots(value) { if (value === void 0) return; if (!Array.isArray(value)) throw new Error("Backup manifest sqliteSnapshots must be an array."); let hasGlobal = false; const paths = /* @__PURE__ */ new Set(); return value.map((snapshot) => { if (!isRecord(snapshot) || snapshot.role !== "global" && snapshot.role !== "agent" || Object.keys(snapshot).some((key) => key !== "sourcePath" && key !== "role" && !(snapshot.role === "agent" && key === "agentId"))) throw new Error("Backup manifest contains an invalid SQLite snapshot owner."); const sourcePath = parseBackupManifestSourcePath(snapshot.sourcePath, "SQLite snapshot"); let identity; if (snapshot.role === "global") identity = { role: "global" }; else { const agentId = snapshot.agentId; if (typeof agentId !== "string" || !agentId || normalizeAgentId(agentId) !== agentId) throw new Error("Backup manifest SQLite snapshot has an invalid agentId."); identity = { role: "agent", agentId }; } const sourceKey = sourcePath.replaceAll("\\", "/").normalize("NFC").toLowerCase(); if (identity.role === "global" && hasGlobal || paths.has(sourceKey)) throw new Error("Backup manifest contains duplicate SQLite snapshot ownership."); hasGlobal ||= identity.role === "global"; paths.add(sourceKey); return { sourcePath, ...identity }; }); } function parseBackupManifest(raw) { let parsed; try { parsed = JSON.parse(raw); } catch (err) { throw new Error("Backup manifest is not valid JSON.", { cause: err }); } if (!isRecord(parsed)) throw new Error("Backup manifest must be an object."); if (parsed.schemaVersion !== 1) throw new Error(`Unsupported backup manifest schemaVersion: ${String(parsed.schemaVersion)}`); if (typeof parsed.archiveRoot !== "string" || !parsed.archiveRoot.trim()) throw new Error("Backup manifest is missing archiveRoot."); if (typeof parsed.createdAt !== "string" || !parsed.createdAt.trim()) throw new Error("Backup manifest is missing createdAt."); if (!Array.isArray(parsed.assets)) throw new Error("Backup manifest is missing assets."); const assets = []; for (const asset of parsed.assets) { if (!isRecord(asset)) throw new Error("Backup manifest contains a non-object asset."); if (typeof asset.kind !== "string" || !asset.kind.trim()) throw new Error("Backup manifest asset is missing kind."); if (typeof asset.sourcePath !== "string" || !asset.sourcePath.trim()) throw new Error("Backup manifest asset is missing sourcePath."); if (typeof asset.archivePath !== "string" || !asset.archivePath.trim()) throw new Error("Backup manifest asset is missing archivePath."); assets.push({ kind: asset.kind, sourcePath: asset.sourcePath, archivePath: asset.archivePath }); } const externalSymbolicLinks = []; if (parsed.externalSymbolicLinks !== void 0) { if (!Array.isArray(parsed.externalSymbolicLinks)) throw new Error("Backup manifest externalSymbolicLinks must be an array."); for (const link of parsed.externalSymbolicLinks) { if (!isRecord(link) || typeof link.entryPath !== "string" || typeof link.linkpath !== "string") throw new Error("Backup manifest contains an invalid external symbolic link."); externalSymbolicLinks.push({ entryPath: link.entryPath, linkpath: link.linkpath }); } } return { schemaVersion: 1, archiveRoot: parsed.archiveRoot, createdAt: parsed.createdAt, runtimeVersion: typeof parsed.runtimeVersion === "string" && parsed.runtimeVersion.trim() ? parsed.runtimeVersion : "unknown", platform: typeof parsed.platform === "string" ? parsed.platform : "unknown", nodeVersion: typeof parsed.nodeVersion === "string" ? parsed.nodeVersion : "unknown", paths: isRecord(parsed.paths) ? { ...parsed.paths.stateDir === void 0 ? {} : { stateDir: parseBackupManifestSourcePath(parsed.paths.stateDir, "state directory") }, agentRoots: parseBackupManifestAgentRoots(parsed.paths.agentRoots) } : void 0, assets, sqliteSnapshots: parseBackupManifestSqliteSnapshots(parsed.sqliteSnapshots), ...parsed.externalSymbolicLinks === void 0 ? {} : { externalSymbolicLinks } }; } function isRootBackupManifestEntry(entryPath) { const parts = entryPath.split("/"); return parts.length === 2 && parts[0] !== "" && parts[1] === "manifest.json"; } function verifyBackupManifestEntries(manifest, entries) { const archiveRoot = normalizeArchiveRoot(manifest.archiveRoot); const manifestEntryPath = path.posix.join(archiveRoot, "manifest.json"); const normalizedEntries = [...entries]; if (!entries.has(manifestEntryPath)) throw new Error(`Archive is missing manifest entry: ${manifestEntryPath}`); for (const entry of normalizedEntries) if (!isArchivePathWithin(entry, archiveRoot)) throw new Error(`Archive entry is outside the declared archive root: ${entry}`); const payloadRoot = path.posix.join(archiveRoot, "payload"); for (const asset of manifest.assets) { const assetArchivePath = normalizeArchivePath(asset.archivePath, "Backup manifest asset path"); if (!isArchivePathWithin(assetArchivePath, payloadRoot)) throw new Error(`Manifest asset path is outside payload root: ${asset.archivePath}`); if (!entries.has(assetArchivePath) && !normalizedEntries.some((entry) => isArchivePathWithin(entry, assetArchivePath))) throw new Error(`Archive is missing payload for manifest asset: ${assetArchivePath}`); } } /** The capture-time inventory, never today's filesystem, defines required database coverage. */ function verifyBackupSqliteCoverage(manifest, requiredSnapshots, verifiedSnapshots) { for (const required of [...manifest.sqliteSnapshots ?? [], ...requiredSnapshots]) { const expectedPath = buildBackupArchivePath(manifest.archiveRoot, required.sourcePath); if (!verifiedSnapshots.some((verified) => verified.archivePath === expectedPath && verified.role === required.role && (required.role === "global" || verified.role === "agent" && verified.agentId === required.agentId))) throw new Error(`Backup lacks verified canonical SQLite coverage for ${required.sourcePath}.`); } } //#endregion export { verifyBackupManifestEntries as a, parseUpdateRecoveryBackupManifest as i, isRootBackupManifestEntry as n, verifyBackupSqliteCoverage as o, parseBackupManifest as r, backupManifestSizeError as t };