UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

231 lines (230 loc) 9.07 kB
import { o as isRecord } from "./record-coerce-DHZ4bFlT.js"; //#region src/plugin-sdk/migration.ts /** Shared migration failure reason when an item lacks required paths. */ const MIGRATION_REASON_MISSING_SOURCE_OR_TARGET = "missing source or target"; /** Shared migration conflict reason when a target already exists. */ const MIGRATION_REASON_TARGET_EXISTS = "target exists"; /** Creates a migration item, defaulting new provider output to the planned state. */ function createMigrationItem(params) { return { ...params, status: params.status ?? "planned" }; } /** Marks a planned item as blocked by an existing target value. */ function markMigrationItemConflict(item, reason) { return { ...item, status: "conflict", reason }; } /** Marks an item as failed during detection or apply. */ function markMigrationItemError(item, reason) { return { ...item, status: "error", reason }; } /** Marks an item as intentionally skipped, usually for manual follow-up. */ function markMigrationItemSkipped(item, reason) { return { ...item, status: "skipped", reason }; } /** Counts migration item statuses for provider plans, apply results, and CLI reports. */ function summarizeMigrationItems(items) { return { total: items.length, planned: items.filter((item) => item.status === "planned").length, migrated: items.filter((item) => item.status === "migrated").length, skipped: items.filter((item) => item.status === "skipped").length, conflicts: items.filter((item) => item.status === "conflict").length, errors: items.filter((item) => item.status === "error").length, sensitive: items.filter((item) => item.sensitive).length }; } const REDACTED_MIGRATION_VALUE = "[redacted]"; const SECRET_KEY_MARKERS = [ "accesstoken", "apikey", "authorization", "bearertoken", "clientsecret", "cookie", "credential", "password", "privatekey", "refreshtoken", "secret" ]; const SECRET_VALUE_PATTERNS = [ /\bBearer\s+[A-Za-z0-9._~+/=-]+/gu, /\bsk-[A-Za-z0-9_-]{8,}\b/gu, /\bgh[pousr]_[A-Za-z0-9_]{16,}\b/gu, /\bxox[abprs]-[A-Za-z0-9-]{8,}\b/gu, /\bAIza[0-9A-Za-z_-]{12,}\b/gu ]; function normalizeSecretKey(key) { return key.toLowerCase().replaceAll(/[^a-z0-9]/gu, ""); } function isSecretKey(key) { const normalized = normalizeSecretKey(key); if (normalized === "token" || normalized.endsWith("token")) return true; if (normalized === "auth" || normalized === "authorization") return true; return SECRET_KEY_MARKERS.some((marker) => normalized.includes(marker)); } var MigrationConfigPatchConflictError = class extends Error { constructor(reason) { super(reason); this.reason = reason; this.name = "MigrationConfigPatchConflictError"; } }; /** Reads a nested config value, returning undefined when a parent is not an object. */ function readMigrationConfigPath(root, path) { let current = root; for (const segment of path) { if (!isRecord(current)) return; current = current[segment]; } return current; } /** Deep-merges object patches and replaces scalar/array values with a cloned target value. */ function mergeMigrationConfigValue(left, right) { if (!isRecord(left) || !isRecord(right)) return structuredClone(right); const next = { ...left }; for (const [key, value] of Object.entries(right)) next[key] = mergeMigrationConfigValue(next[key], value); return next; } /** Writes a config patch path in-place, creating missing object parents as needed. */ function writeMigrationConfigPath(root, path, value) { let current = root; for (const segment of path.slice(0, -1)) { const existing = current[segment]; if (!isRecord(existing)) current[segment] = {}; current = current[segment]; } const leaf = path.at(-1); if (!leaf) return; current[leaf] = mergeMigrationConfigValue(current[leaf], value); } /** Checks whether a config patch would overwrite existing leaf keys without `--overwrite`. */ function hasMigrationConfigPatchConflict(config, path, value) { if (!isRecord(value)) return readMigrationConfigPath(config, path) !== void 0; const existing = readMigrationConfigPath(config, path); if (!isRecord(existing)) return false; return Object.keys(value).some((key) => existing[key] !== void 0); } /** Builds a planned or conflicting config-merge migration item. */ function createMigrationConfigPatchItem(params) { return createMigrationItem({ id: params.id, kind: "config", action: "merge", source: params.source, target: params.target, status: params.conflict ? "conflict" : "planned", reason: params.conflict ? params.reason ?? "target exists" : void 0, message: params.message, details: { ...params.details, path: params.path, value: params.value } }); } /** Builds a skipped item that records user-facing manual migration guidance. */ function createMigrationManualItem(params) { return createMigrationItem({ id: params.id, kind: "manual", action: "manual", source: params.source, status: "skipped", message: params.message, reason: params.recommendation }); } /** Reads config patch metadata from an item produced by `createMigrationConfigPatchItem`. */ function readMigrationConfigPatchDetails(item) { const path = item.details?.path; if (!Array.isArray(path) || !path.every((segment) => typeof segment === "string")) return; return { path, value: item.details?.value }; } /** Applies one planned config patch through the runtime config writer and returns its final status. */ async function applyMigrationConfigPatchItem(ctx, item) { if (item.status !== "planned") return item; const details = readMigrationConfigPatchDetails(item); if (!details) return markMigrationItemError(item, "missing config patch"); const configApi = ctx.runtime?.config; if (!configApi?.current || !configApi.mutateConfigFile) return markMigrationItemError(item, "config runtime unavailable"); try { const currentConfig = configApi.current(); if (!ctx.overwrite && hasMigrationConfigPatchConflict(currentConfig, details.path, details.value)) return markMigrationItemConflict(item, MIGRATION_REASON_TARGET_EXISTS); await configApi.mutateConfigFile({ base: "runtime", afterWrite: { mode: "auto" }, mutate(draft) { if (!ctx.overwrite && hasMigrationConfigPatchConflict(draft, details.path, details.value)) throw new MigrationConfigPatchConflictError(MIGRATION_REASON_TARGET_EXISTS); writeMigrationConfigPath(draft, details.path, details.value); } }); return { ...item, status: "migrated" }; } catch (err) { if (err instanceof MigrationConfigPatchConflictError) return markMigrationItemConflict(item, err.reason); return markMigrationItemError(item, err instanceof Error ? err.message : String(err)); } } /** Manual items never mutate state; applying one preserves the skipped/manual status. */ function applyMigrationManualItem(item) { return markMigrationItemSkipped(item, item.reason ?? "manual follow-up required"); } function isSecretReferenceLike(value) { if (!isRecord(value)) return false; return value.source === "env" && typeof value.id === "string" && (value.provider === void 0 || typeof value.provider === "string"); } function redactString(value) { let next = value; for (const pattern of SECRET_VALUE_PATTERNS) next = next.replace(pattern, REDACTED_MIGRATION_VALUE); return next; } function redactMigrationValueInternal(value, seen) { if (typeof value === "string") return redactString(value); if (Array.isArray(value)) return value.map((entry) => redactMigrationValueInternal(entry, seen)); if (!value || typeof value !== "object") return value; if (seen.has(value)) return REDACTED_MIGRATION_VALUE; seen.add(value); const next = {}; for (const [key, entry] of Object.entries(value)) { if (isSecretKey(key) && !isSecretReferenceLike(entry)) { next[key] = REDACTED_MIGRATION_VALUE; continue; } next[key] = redactMigrationValueInternal(entry, seen); } return next; } /** Redacts likely secret values while preserving SecretRef-like objects for operator context. */ function redactMigrationValue(value) { return redactMigrationValueInternal(value, /* @__PURE__ */ new WeakSet()); } /** Redacts sensitive fields from one migration item before report/output serialization. */ function redactMigrationItem(item) { return redactMigrationValue(item); } /** Redacts sensitive fields from a full migration plan before report/output serialization. */ function redactMigrationPlan(plan) { return redactMigrationValue(plan); } //#endregion export { redactMigrationValue as _, createMigrationConfigPatchItem as a, hasMigrationConfigPatchConflict as c, markMigrationItemSkipped as d, mergeMigrationConfigValue as f, redactMigrationPlan as g, redactMigrationItem as h, applyMigrationManualItem as i, markMigrationItemConflict as l, readMigrationConfigPath as m, MIGRATION_REASON_TARGET_EXISTS as n, createMigrationItem as o, readMigrationConfigPatchDetails as p, applyMigrationConfigPatchItem as r, createMigrationManualItem as s, MIGRATION_REASON_MISSING_SOURCE_OR_TARGET as t, markMigrationItemError as u, summarizeMigrationItems as v, writeMigrationConfigPath as y };