openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
121 lines (120 loc) • 4.37 kB
JavaScript
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 };