UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

121 lines (120 loc) 4.37 kB
import { c as isRecord } from "./record-coerce-DItp3I4t.js"; import { c as readJsonIfExists } from "./json-files-Bq1lIlQB.js"; import { b as resolvePairingPaths, v as coercePairingStateRecord } from "./device-bootstrap-DRFMRCcr.js"; import { b as withPairedDeviceRecords } from "./device-pairing-DFIQp3ZY.js"; import path from "node:path"; import fs from "node:fs/promises"; //#region src/infra/device-pairing-migration.ts const SQLITE_TEXT_FIELDS = [ "displayName", "operatorLabel", "platform", "deviceFamily", "clientId", "clientMode", "browserOrigin", "role", "remoteIp", "approvedVia", "lastSeenReason" ]; function normalizeLegacyPairedDevice(record) { if (!isRecord(record)) return null; if (typeof record.publicKey !== "string" || !record.publicKey.trim() || !Number.isSafeInteger(record.createdAtMs) || !Number.isSafeInteger(record.approvedAtMs)) return null; const device = { ...record }; let omittedFields = 0; if (device.lastSeenAtMs !== void 0 && !Number.isSafeInteger(device.lastSeenAtMs)) { delete device.lastSeenAtMs; omittedFields += 1; } for (const field of SQLITE_TEXT_FIELDS) if (device[field] !== void 0 && typeof device[field] !== "string") { delete device[field]; omittedFields += 1; } return { device, omittedFields }; } async function archiveLegacyFile(filePath) { try { await fs.rename(filePath, `${filePath}.migrated`); } catch {} } async function fileExists(filePath) { return await fs.access(filePath).then(() => true, () => false); } /** List legacy devices/*.json files the startup import has not archived yet. */ async function listLegacyDevicePairingStoreFiles(baseDir) { const { dir, pendingPath, pairedPath } = resolvePairingPaths(baseDir, "devices"); const candidates = [ pairedPath, pendingPath, path.join(dir, "bootstrap.json") ]; const present = await Promise.all(candidates.map(fileExists)); return candidates.filter((_, index) => present[index]); } /** * Import legacy devices/paired.json records into the SQLite pairing store, * then archive the legacy files. Existing SQLite records win over legacy rows * for the same device id. Idempotent: after the first run the files carry a * `.migrated` suffix and the function returns null immediately. Throws on an * unreadable paired.json so a failed import leaves the files for a retry * instead of silently dropping approved pairings. */ async function migrateLegacyDevicePairingStore(params) { const { dir, pendingPath, pairedPath } = resolvePairingPaths(params?.baseDir, "devices"); const bootstrapPath = path.join(dir, "bootstrap.json"); const pairedRaw = await readJsonIfExists(pairedPath); const hasTransientFiles = await fileExists(pendingPath) || await fileExists(bootstrapPath); if (pairedRaw == null && !hasTransientFiles) return null; const legacyPaired = coercePairingStateRecord(pairedRaw); let imported = 0; let skippedExisting = 0; let skippedInvalid = 0; let omittedInvalidFields = 0; if (Object.keys(legacyPaired).length > 0) await withPairedDeviceRecords(params?.baseDir, (pairedByDeviceId) => { for (const [rawDeviceId, record] of Object.entries(legacyPaired)) { const deviceId = rawDeviceId.trim(); if (!deviceId) { skippedInvalid += 1; continue; } if (pairedByDeviceId[deviceId]) { skippedExisting += 1; continue; } const normalized = normalizeLegacyPairedDevice(record); if (!normalized) { skippedInvalid += 1; continue; } omittedInvalidFields += normalized.omittedFields; pairedByDeviceId[deviceId] = { ...normalized.device, deviceId }; imported += 1; } return { value: void 0, persist: imported > 0 }; }); if (skippedInvalid > 0) params?.log?.warn(`device pairing store migration skipped ${skippedInvalid} invalid paired record(s)`); if (omittedInvalidFields > 0) params?.log?.warn(`device pairing store migration omitted ${omittedInvalidFields} invalid optional field(s)`); await Promise.all([ archiveLegacyFile(pairedPath), archiveLegacyFile(pendingPath), archiveLegacyFile(bootstrapPath) ]); const result = { imported, skippedExisting }; params?.log?.info(`device pairing store migrated to SQLite: imported ${imported} paired device(s), kept ${skippedExisting} existing record(s)`); return result; } //#endregion export { listLegacyDevicePairingStoreFiles, migrateLegacyDevicePairingStore };