openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
903 lines (902 loc) • 40.5 kB
JavaScript
import { c as isRecord } from "../../record-coerce-DItp3I4t.js";
import "../../string-coerce-runtime-GQa0ehRA.js";
import { i as defineStrayPluginEntryConfigMigration } from "../../runtime-doctor-migrations-BYo-q8_8.js";
import { t as archiveLegacyStateSource } from "../../doctor-state-migration-fs-CfVap4xL.js";
import { f as parseVerdict, i as parseReefRelayUrl, r as normalizeReefTarget, t as ReefChannelConfigSchema } from "../../config-schema-DGoCUvrP.js";
import { A as reefReplayStoreKey, B as parseReefSetupSession, C as REEF_REVIEWS_MAX_ENTRIES, G as REEF_AUDIT_HEAD_NAMESPACE, J as REEF_AUDIT_MIGRATION_NAMESPACE, K as REEF_AUDIT_MAX_ENTRIES, M as REEF_REGISTRATION_NAMESPACE, N as REEF_REGISTRATION_SESSION_KEY, Q as reefAuditEntryKey, S as REEF_REPLAY_TTL_MS, W as REEF_AUDIT_HEAD_KEY, X as REEF_AUDIT_STORE_MAX_ENTRIES, Y as REEF_AUDIT_NAMESPACE, Z as parseReefAuditHead, _ as REEF_KEYS_MIGRATION_KEY, b as REEF_REPLAY_MAX_ENTRIES, d as REEF_DELIVERED_MAX_ENTRIES, f as REEF_DELIVERED_NAMESPACE, ft as verifyChain, g as REEF_KEYS_KEY, h as REEF_DURABLE_MIGRATION_NAMESPACE, j as REEF_REGISTRATION_IDENTITY_KEY, k as parseReefKeys, m as REEF_DURABLE_MIGRATION_KEY, n as legacyReefFileExists, o as REEF_TRUST_STORE_MAX_ENTRIES, p as REEF_DELIVERED_TTL_MS, pt as verifyChainSegment, q as REEF_AUDIT_MIGRATION_KEY, r as resolveLegacyReefStateDir, s as REEF_TRUST_STORE_NAMESPACE, t as REEF_DURABLE_LEGACY_FILENAMES, u as resolveReefTrustStoreKey, v as REEF_KEYS_MIGRATION_NAMESPACE, w as REEF_REVIEWS_NAMESPACE, x as REEF_REPLAY_NAMESPACE, y as REEF_KEYS_NAMESPACE, z as parseReefIdentityBinding } from "../../doctor-state-paths-Bf7anfKJ.js";
import { r as ReefPeerTrustSchema } from "../../friend-types-DBQVddbp.js";
import path from "node:path";
import fs from "node:fs/promises";
//#region extensions/reef/src/doctor-durable-state.ts
const REEF_RUNTIME_LEGACY_FILENAMES = [
"replay.jsonl",
"reviews.json",
"delivered.json"
];
async function readLegacyReefAudit(filePath) {
const entries = (await fs.readFile(filePath, "utf8")).split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
if (!verifyChain(entries)) throw new Error("invalid Reef audit chain");
return entries;
}
async function readStoredReefAudit(store, headStore) {
const headValue = await headStore.lookup(REEF_AUDIT_HEAD_KEY);
if (!headValue) return [];
const head = parseReefAuditHead(headValue);
const reversed = [];
let hash = head.hash;
for (let seq = head.seq; seq > 0 && reversed.length < 3e4; seq--) {
const record = await store.lookup(reefAuditEntryKey(hash));
if (!record) break;
if (record.entry.entryHash !== hash || record.entry.event.seq !== seq) throw new Error("invalid Reef audit chain state");
reversed.push(record.entry);
hash = record.entry.prevHash;
}
const expectedEntries = Math.min(head.seq, REEF_AUDIT_MAX_ENTRIES);
if (reversed.length !== expectedEntries) throw new Error("Reef audit chain is shorter than its committed retention window");
const entries = reversed.toReversed();
const first = entries[0];
if (!first || !verifyChainSegment(entries, {
previousHash: first.prevHash,
previousSeq: first.event.seq - 1,
head: head.hash
})) throw new Error("invalid Reef audit chain state");
return entries;
}
function requireLegacyReplayString(record, field) {
const value = record[field];
if (typeof value !== "string" || value.length === 0) throw new Error(`invalid Reef replay ${field}`);
return value;
}
function parseLegacySignedReceipt(value) {
const id = requireLegacyReplayString(value, "id");
const bodyHash = requireLegacyReplayString(value, "bodyHash");
const auditHead = requireLegacyReplayString(value, "auditHead");
const signature = requireLegacyReplayString(value, "signature");
if (value.status !== "accepted" && value.status !== "rejected") throw new Error("invalid Reef replay receipt status");
if (value.category !== void 0 && typeof value.category !== "string") throw new Error("invalid Reef replay receipt category");
return {
id,
bodyHash,
auditHead,
status: value.status,
signature,
...value.category !== void 0 ? { category: value.category } : {}
};
}
function parseLegacyReviewRequest(value) {
const direction = value.direction;
if (direction !== "inbound" && direction !== "outbound") throw new Error("invalid Reef review direction");
return {
id: requireLegacyReplayString(value, "id"),
from: requireLegacyReplayString(value, "from"),
to: requireLegacyReplayString(value, "to"),
direction,
bodyHash: requireLegacyReplayString(value, "bodyHash"),
approvalDigest: requireLegacyReplayString(value, "approvalDigest"),
verdict: parseVerdict(value.verdict)
};
}
function parseLegacyReefReplayLine(value) {
if (!isRecord(value)) throw new Error("invalid Reef replay record");
const peer = requireLegacyReplayString(value, "peer");
const id = requireLegacyReplayString(value, "id");
if (value.op === "claim") return {
op: "claim",
peer,
id,
envelopeHash: requireLegacyReplayString(value, "envelopeHash")
};
if (value.op === "consume" || value.op === "release") return {
op: value.op,
peer,
id
};
if (value.op !== "complete" || !isRecord(value.receipt)) throw new Error("invalid Reef replay operation");
const receipt = parseLegacySignedReceipt(value.receipt);
if (receipt.id !== id || !["accepted", "rejected"].includes(receipt.status)) throw new Error("invalid Reef replay receipt");
const body = value.body;
if (receipt.status === "accepted" && (!isRecord(body) || typeof body.enc !== "string") || receipt.status === "rejected" && body !== void 0) throw new Error("invalid Reef replay completion");
return {
op: "complete",
peer,
id,
receipt,
...isRecord(body) && typeof body.enc === "string" ? { body: { enc: body.enc } } : {}
};
}
async function readLegacyReefReplay(filePath) {
const raw = await fs.readFile(filePath, "utf8");
const lines = raw.split("\n").filter((line) => line.length > 0);
const records = /* @__PURE__ */ new Map();
for (const [index, line] of lines.entries()) {
let log;
try {
log = parseLegacyReefReplayLine(JSON.parse(line));
} catch (error) {
if (index === lines.length - 1 && !raw.endsWith("\n")) break;
throw error;
}
const key = reefReplayStoreKey(log.peer, log.id);
const existing = records.get(key);
let next;
if (log.op === "claim") {
if (existing && existing.envelopeHash !== log.envelopeHash) throw new Error("conflicting Reef replay binding");
next = {
peer: log.peer,
id: log.id,
envelopeHash: log.envelopeHash,
state: "available"
};
} else {
if (!existing) throw new Error(`Reef replay ${log.op} lacks claim`);
if (log.op === "complete") next = {
...existing,
state: "completed",
receipt: log.receipt,
...log.body ? { body: log.body } : {}
};
else if (log.op === "consume") next = {
peer: existing.peer,
id: existing.id,
envelopeHash: existing.envelopeHash,
state: "consumed"
};
else next = {
...existing,
state: "available"
};
}
records.delete(key);
records.set(key, next);
}
return [...records.values()];
}
async function readLegacyReefReviews(filePath) {
const value = JSON.parse(await fs.readFile(filePath, "utf8"));
if (!isRecord(value)) throw new Error("invalid Reef reviews file");
const records = /* @__PURE__ */ new Map();
for (const [digest, raw] of Object.entries(value)) {
if (!isRecord(raw) || !isRecord(raw.review)) throw new Error(`invalid Reef review ${digest}`);
const review = parseLegacyReviewRequest(raw.review);
if (review.approvalDigest !== digest || raw.approved !== void 0 && typeof raw.approved !== "boolean") throw new Error(`invalid Reef review ${digest}`);
records.set(digest, {
review,
...typeof raw.approved === "boolean" ? { approved: raw.approved } : {}
});
}
return records;
}
async function readLegacyReefDelivered(filePath) {
const value = JSON.parse(await fs.readFile(filePath, "utf8"));
if (!Array.isArray(value) || value.some((id) => typeof id !== "string" || id.length === 0)) throw new Error("invalid Reef delivered file");
return [...new Set(value)];
}
const reefAuditStateMigration = {
id: "reef-audit-jsonl-to-plugin-state",
label: "Reef audit trail",
async detectLegacyState(params) {
const filePath = path.join(resolveLegacyReefStateDir(params), "audit.jsonl");
const migrationStore = params.context.openPluginStateKeyedStore({
namespace: REEF_AUDIT_MIGRATION_NAMESPACE,
maxEntries: 1,
overflowPolicy: "reject-new"
});
const sourceExists = await legacyReefFileExists(filePath);
const pending = await migrationStore.lookup(REEF_AUDIT_MIGRATION_KEY);
return sourceExists || pending ? { preview: [sourceExists ? "- Reef audit trail -> plugin state (audit)" : "- Verify Reef audit migration marker"] } : null;
},
async migrateLegacyState(params) {
const changes = [];
const warnings = [];
const filePath = path.join(resolveLegacyReefStateDir(params), "audit.jsonl");
const migrationStore = params.context.openPluginStateKeyedStore({
namespace: REEF_AUDIT_MIGRATION_NAMESPACE,
maxEntries: 1,
overflowPolicy: "reject-new"
});
const durableMigrationStore = params.context.openPluginStateKeyedStore({
namespace: REEF_DURABLE_MIGRATION_NAMESPACE,
maxEntries: 1,
overflowPolicy: "reject-new"
});
const store = params.context.openPluginStateKeyedStore({
namespace: REEF_AUDIT_NAMESPACE,
maxEntries: REEF_AUDIT_STORE_MAX_ENTRIES,
overflowPolicy: "reject-new"
});
const headStore = params.context.openPluginStateKeyedStore({
namespace: REEF_AUDIT_HEAD_NAMESPACE,
maxEntries: 1,
overflowPolicy: "reject-new"
});
if (await legacyReefFileExists(filePath) || await migrationStore.lookup("audit-jsonl") || await durableMigrationStore.lookup("legacy-files")) await durableMigrationStore.register(REEF_DURABLE_MIGRATION_KEY, { pending: true });
if (!await legacyReefFileExists(filePath)) {
const pending = await migrationStore.lookup(REEF_AUDIT_MIGRATION_KEY);
if (!pending) return {
changes,
warnings
};
try {
const canonical = await readStoredReefAudit(store, headStore);
if (pending.expectedEntries === void 0 ? canonical.length === 0 : canonical.length !== pending.expectedEntries) throw new Error("canonical audit trail does not match the verified import");
await migrationStore.delete(REEF_AUDIT_MIGRATION_KEY);
changes.push("Verified Reef audit trail; cleared completed migration marker");
} catch (error) {
warnings.push(`Reef audit migration is incomplete and audit.jsonl is missing: ${String(error)}; left migration blocker in place`);
}
return {
changes,
warnings
};
}
await migrationStore.register(REEF_AUDIT_MIGRATION_KEY, { pending: true });
let legacy;
try {
legacy = await readLegacyReefAudit(filePath);
} catch (error) {
if (error.code === "ENOENT") return {
changes,
warnings
};
warnings.push(`Failed importing Reef audit trail: ${String(error)}; left source in place`);
return {
changes,
warnings
};
}
let canonical;
try {
canonical = await readStoredReefAudit(store, headStore);
} catch (error) {
warnings.push(`Failed reading canonical Reef audit trail: ${String(error)}; left legacy source in place`);
return {
changes,
warnings
};
}
if (canonical.length > 0 && JSON.stringify(canonical) !== JSON.stringify(legacy.slice(-canonical.length))) {
warnings.push("Kept existing Reef audit trail; left differing legacy source in place");
return {
changes,
warnings
};
}
const retained = legacy.slice(-REEF_AUDIT_MAX_ENTRIES);
if (canonical.length === 0 && retained.length > 0) try {
for (const [index, entry] of retained.entries()) {
const key = reefAuditEntryKey(entry.entryHash);
const nextHash = retained[index + 1]?.entryHash;
const record = {
kind: "entry",
entry,
...nextHash ? { nextHash } : {}
};
const existing = await store.lookup(key);
if (existing && JSON.stringify(existing) !== JSON.stringify(record)) throw new Error(`conflicting audit entry ${entry.entryHash}`);
await store.registerIfAbsent(key, record);
}
const last = retained.at(-1);
const first = retained[0];
if (!await headStore.registerIfAbsent("head", {
kind: "head",
hash: last.entryHash,
seq: last.event.seq,
oldestHash: first.entryHash
})) throw new Error("audit head appeared during import");
} catch (error) {
warnings.push(`Failed importing Reef audit trail: ${String(error)}; left source in place`);
return {
changes,
warnings
};
}
const persisted = await readStoredReefAudit(store, headStore);
if (JSON.stringify(persisted) !== JSON.stringify(retained)) {
warnings.push("Failed verifying Reef audit trail after import; left source in place");
return {
changes,
warnings
};
}
changes.push(`Migrated ${legacy.length} Reef audit ${legacy.length === 1 ? "entry" : "entries"} -> plugin state`);
await migrationStore.register(REEF_AUDIT_MIGRATION_KEY, {
pending: true,
expectedEntries: persisted.length
});
const warningCount = warnings.length;
await archiveLegacyStateSource({
filePath,
label: "Reef audit trail",
changes,
warnings
});
if (persisted.length < legacy.length && warnings.length === warningCount) changes.push(`Retained the newest ${persisted.length} Reef audit entries in SQLite; preserved the complete ${legacy.length}-entry chain in the archived legacy source`);
if (warnings.length === warningCount) await migrationStore.delete(REEF_AUDIT_MIGRATION_KEY);
return {
changes,
warnings
};
}
};
const reefRuntimeStateMigration = {
id: "reef-runtime-files-to-plugin-state",
label: "Reef durable runtime state",
async detectLegacyState(params) {
const stateDir = resolveLegacyReefStateDir(params);
const files = (await Promise.all(REEF_RUNTIME_LEGACY_FILENAMES.map(async (filename) => ({
filename,
exists: await legacyReefFileExists(path.join(stateDir, filename))
})))).filter((entry) => entry.exists);
const durableSourceExists = (await Promise.all(REEF_DURABLE_LEGACY_FILENAMES.map((filename) => legacyReefFileExists(path.join(stateDir, filename))))).some(Boolean);
const durablePending = await params.context.openPluginStateKeyedStore({
namespace: REEF_DURABLE_MIGRATION_NAMESPACE,
maxEntries: 1,
overflowPolicy: "reject-new"
}).lookup(REEF_DURABLE_MIGRATION_KEY);
return files.length > 0 || durableSourceExists || durablePending ? { preview: [files.length > 0 ? `- Reef runtime state -> plugin state (${files.map((entry) => entry.filename).join(", ")})` : durableSourceExists ? "- Finalize Reef durable state migration barrier" : "- Verify Reef durable state migration barrier"] } : null;
},
async migrateLegacyState(params) {
const changes = [];
const warnings = [];
const stateDir = resolveLegacyReefStateDir(params);
const durableMigrationStore = params.context.openPluginStateKeyedStore({
namespace: REEF_DURABLE_MIGRATION_NAMESPACE,
maxEntries: 1,
overflowPolicy: "reject-new"
});
const durablePending = await durableMigrationStore.lookup(REEF_DURABLE_MIGRATION_KEY);
if ((await Promise.all(REEF_RUNTIME_LEGACY_FILENAMES.map((filename) => legacyReefFileExists(path.join(stateDir, filename))))).some(Boolean) || durablePending) await durableMigrationStore.register(REEF_DURABLE_MIGRATION_KEY, { pending: true });
const replayPath = path.join(stateDir, "replay.jsonl");
if (await legacyReefFileExists(replayPath)) try {
const legacy = await readLegacyReefReplay(replayPath);
const store = params.context.openPluginStateKeyedStore({
namespace: REEF_REPLAY_NAMESPACE,
maxEntries: REEF_REPLAY_MAX_ENTRIES,
overflowPolicy: "reject-new",
defaultTtlMs: REEF_REPLAY_TTL_MS
});
const canonicalEntries = await store.entries();
const canonical = new Map(canonicalEntries.map((entry) => [entry.key, entry.value]));
for (const record of legacy) {
const key = reefReplayStoreKey(record.peer, record.id);
const existing = canonical.get(key);
if (existing && JSON.stringify(existing) !== JSON.stringify(record)) throw new Error(`canonical replay state ${key} differs`);
}
const missing = legacy.filter((record) => !canonical.has(reefReplayStoreKey(record.peer, record.id)));
if (canonical.size + missing.length > 3e3) throw new Error(`${canonical.size + missing.length} replay bindings exceed plugin-state capacity`);
for (const record of missing) await store.registerIfAbsent(reefReplayStoreKey(record.peer, record.id), record);
for (const entry of canonicalEntries) if (JSON.stringify(await store.lookup(entry.key)) !== JSON.stringify(entry.value)) throw new Error(`canonical replay state ${entry.key} changed during import`);
for (const record of missing) if (JSON.stringify(await store.lookup(reefReplayStoreKey(record.peer, record.id))) !== JSON.stringify(record)) throw new Error("persisted replay state differs");
changes.push(`Migrated ${legacy.length} Reef replay bindings -> plugin state`);
await archiveLegacyStateSource({
filePath: replayPath,
label: "Reef replay state",
changes,
warnings
});
} catch (error) {
warnings.push(`Failed importing Reef replay state: ${String(error)}; left source in place`);
}
const reviewsPath = path.join(stateDir, "reviews.json");
if (await legacyReefFileExists(reviewsPath)) try {
const legacy = await readLegacyReefReviews(reviewsPath);
const pending = [...legacy].filter(([, record]) => record.approved === void 0);
if (pending.length > 2e3) throw new Error(`${pending.length} pending reviews exceed plugin-state capacity`);
const completed = [...legacy].filter(([, record]) => record.approved !== void 0);
const completedCapacity = REEF_REVIEWS_MAX_ENTRIES - pending.length;
const retainedCompleted = completedCapacity > 0 ? completed.slice(-completedCapacity) : [];
const retainedKeys = new Set([...pending, ...retainedCompleted].map(([digest]) => digest));
const retained = new Map([...legacy].filter(([digest]) => retainedKeys.has(digest)));
const store = params.context.openPluginStateKeyedStore({
namespace: REEF_REVIEWS_NAMESPACE,
maxEntries: REEF_REVIEWS_MAX_ENTRIES,
overflowPolicy: "reject-new"
});
for (const [digest, record] of retained) {
const existing = await store.lookup(digest);
if (existing && JSON.stringify(existing) !== JSON.stringify(record)) throw new Error(`canonical review ${digest} differs`);
if (!existing) await store.registerIfAbsent(digest, record);
}
for (const [digest, record] of retained) if (JSON.stringify(await store.lookup(digest)) !== JSON.stringify(record)) throw new Error(`persisted review ${digest} differs`);
changes.push(`Migrated ${retained.size} of ${legacy.size} Reef reviews -> plugin state`);
await archiveLegacyStateSource({
filePath: reviewsPath,
label: "Reef reviews",
changes,
warnings
});
} catch (error) {
warnings.push(`Failed importing Reef reviews: ${String(error)}; left source in place`);
}
const deliveredPath = path.join(stateDir, "delivered.json");
if (await legacyReefFileExists(deliveredPath)) try {
const legacy = await readLegacyReefDelivered(deliveredPath);
const store = params.context.openPluginStateKeyedStore({
namespace: REEF_DELIVERED_NAMESPACE,
maxEntries: REEF_DELIVERED_MAX_ENTRIES,
overflowPolicy: "reject-new",
defaultTtlMs: REEF_DELIVERED_TTL_MS
});
const canonicalEntries = await store.entries();
const canonical = new Map(canonicalEntries.map((entry) => [entry.key, entry.value]));
for (const id of legacy) {
const existing = canonical.get(id);
if (existing && existing.id !== id) throw new Error(`canonical delivered marker ${id} differs`);
}
const missing = legacy.filter((id) => !canonical.has(id));
if (canonical.size + missing.length > 5e3) throw new Error(`${canonical.size + missing.length} delivered markers exceed plugin-state capacity`);
for (const id of missing) await store.registerIfAbsent(id, { id });
for (const entry of canonicalEntries) if (JSON.stringify(await store.lookup(entry.key)) !== JSON.stringify(entry.value)) throw new Error(`canonical delivered marker ${entry.key} changed during import`);
for (const id of missing) if ((await store.lookup(id))?.id !== id) throw new Error(`persisted delivered marker ${id} differs`);
changes.push(`Migrated ${legacy.length} Reef delivered markers -> plugin state`);
await archiveLegacyStateSource({
filePath: deliveredPath,
label: "Reef delivered markers",
changes,
warnings
});
} catch (error) {
warnings.push(`Failed importing Reef delivered markers: ${String(error)}; left source in place`);
}
const remainingSources = (await Promise.all(REEF_DURABLE_LEGACY_FILENAMES.map(async (filename) => ({
filename,
exists: await legacyReefFileExists(path.join(stateDir, filename))
})))).filter((entry) => entry.exists);
const identityMigrationStore = params.context.openPluginStateKeyedStore({
namespace: REEF_KEYS_MIGRATION_NAMESPACE,
maxEntries: 1,
overflowPolicy: "reject-new"
});
const auditMigrationStore = params.context.openPluginStateKeyedStore({
namespace: REEF_AUDIT_MIGRATION_NAMESPACE,
maxEntries: 1,
overflowPolicy: "reject-new"
});
if (remainingSources.length === 0 && !await identityMigrationStore.lookup("keys-json") && !await auditMigrationStore.lookup("audit-jsonl")) {
if (await durableMigrationStore.delete("legacy-files")) changes.push("Verified all Reef durable state; cleared migration barrier");
} else if (await durableMigrationStore.lookup("legacy-files")) warnings.push(`Reef durable state migration is incomplete; left migration blocker in place${remainingSources.length > 0 ? ` (${remainingSources.map((entry) => entry.filename).join(", ")})` : ""}`);
return {
changes,
warnings
};
}
};
//#endregion
//#region extensions/reef/config-doctor-api.ts
function hasRetiredReefPolicyConfig(value) {
return isRecord(value) && ["dmPolicy", "allowFrom"].some((key) => Object.hasOwn(value, key));
}
const reefStrayEntryConfigMigration = defineStrayPluginEntryConfigMigration({
pluginId: "reef",
channelId: "reef",
validateMergedChannelConfig: (merged) => ReefChannelConfigSchema.safeParse(merged).success
});
const legacyConfigRules = [{
path: ["channels", "reef"],
message: "channels.reef dmPolicy/allowFrom are legacy; run \"openclaw doctor --fix\" to remove them. Peer trust is SQLite-backed.",
match: hasRetiredReefPolicyConfig
}, reefStrayEntryConfigMigration.legacyConfigRule];
function normalizeCompatibilityConfig({ cfg }) {
const reef = cfg.channels?.reef;
let config = cfg;
const changes = [];
if (isRecord(reef) && hasRetiredReefPolicyConfig(reef)) {
const next = structuredClone(cfg);
const nextReef = next.channels?.reef;
if (isRecord(nextReef)) {
for (const key of ["dmPolicy", "allowFrom"]) if (Object.hasOwn(nextReef, key)) {
delete nextReef[key];
changes.push(`Removed retired Reef ${key} field.`);
}
config = next;
}
}
const stray = reefStrayEntryConfigMigration.normalizeConfig({ cfg: config });
return {
config: stray.config,
changes: [...changes, ...stray.changes]
};
}
//#endregion
//#region extensions/reef/doctor-contract-api.ts
const RETIRED_REEF_CONFIG_KEYS = [
"friends",
"dmPolicy",
"allowFrom"
];
const REEF_CONFIG_IMPORT_NAMESPACE = "peer-state-config-imports";
const LegacyReefFriendSchema = ReefPeerTrustSchema.omit({ approvedAt: true });
const ReefIdentityConfigSchema = ReefChannelConfigSchema.pick({
handle: true,
relayUrl: true
});
const REEF_LEGACY_REGISTRATION_SOURCES = [{
filename: "identity.json",
key: REEF_REGISTRATION_IDENTITY_KEY,
parse: parseReefIdentityBinding,
label: "Reef identity binding"
}, {
filename: "setup-session.json",
key: REEF_REGISTRATION_SESSION_KEY,
parse: parseReefSetupSession,
label: "Reef setup session"
}];
function configuredReefIdentityBinding(cfg) {
const reef = cfg.channels?.reef;
if (!isRecord(reef) || !Object.hasOwn(reef, "handle") || reef.handle === void 0) return { status: "absent" };
const parsed = ReefIdentityConfigSchema.safeParse({
handle: reef.handle,
relayUrl: reef.relayUrl
});
if (!parsed.success || !parsed.data.handle) return { status: "invalid" };
return {
status: "valid",
binding: {
handle: parsed.data.handle,
relayUrl: parseReefRelayUrl(parsed.data.relayUrl)
}
};
}
function inspectLegacyReefFriends(cfg) {
const reef = cfg.channels?.reef;
if (!isRecord(reef) || !Object.hasOwn(reef, "friends")) return null;
const rawFriends = isRecord(reef.friends) ? reef.friends : null;
const canonicalCandidate = { ...reef };
for (const key of RETIRED_REEF_CONFIG_KEYS) delete canonicalCandidate[key];
const parsedConfig = ReefChannelConfigSchema.safeParse(canonicalCandidate);
const config = parsedConfig.success && parsedConfig.data.handle ? parsedConfig.data : null;
const friends = /* @__PURE__ */ new Map();
let rejected = rawFriends ? 0 : 1;
for (const [peer, value] of Object.entries(rawFriends ?? {})) {
const parsedFriend = LegacyReefFriendSchema.safeParse(value);
if (normalizeReefTarget(peer) !== peer || !parsedFriend.success) {
rejected++;
continue;
}
friends.set(peer, parsedFriend.data);
}
return {
config,
friends,
rejected,
total: rawFriends ? Object.keys(rawFriends).length : 0
};
}
const stateMigrations = [
{
id: "reef-keys-json-to-plugin-state",
label: "Reef identity keys",
async detectLegacyState(params) {
const stateDir = resolveLegacyReefStateDir(params);
const filePath = path.join(stateDir, "keys.json");
const migrationStore = params.context.openPluginStateKeyedStore({
namespace: REEF_KEYS_MIGRATION_NAMESPACE,
maxEntries: 1,
overflowPolicy: "reject-new"
});
const durableMigrationStore = params.context.openPluginStateKeyedStore({
namespace: REEF_DURABLE_MIGRATION_NAMESPACE,
maxEntries: 1,
overflowPolicy: "reject-new"
});
const sourceExists = await legacyReefFileExists(filePath);
const pending = await migrationStore.lookup(REEF_KEYS_MIGRATION_KEY);
const durableSourceExists = (await Promise.all(REEF_DURABLE_LEGACY_FILENAMES.map((filename) => legacyReefFileExists(path.join(stateDir, filename))))).some(Boolean);
const durablePending = await durableMigrationStore.lookup(REEF_DURABLE_MIGRATION_KEY);
return sourceExists || pending || durableSourceExists || durablePending ? { preview: [sourceExists ? "- Reef identity keys -> plugin state (identity)" : pending ? "- Verify Reef identity-key migration marker" : "- Prepare Reef durable state migration barrier"] } : null;
},
async migrateLegacyState(params) {
const changes = [];
const warnings = [];
const stateDir = resolveLegacyReefStateDir(params);
const filePath = path.join(stateDir, "keys.json");
const migrationStore = params.context.openPluginStateKeyedStore({
namespace: REEF_KEYS_MIGRATION_NAMESPACE,
maxEntries: 1,
overflowPolicy: "reject-new"
});
const store = params.context.openPluginStateKeyedStore({
namespace: REEF_KEYS_NAMESPACE,
maxEntries: 1,
overflowPolicy: "reject-new"
});
const durableMigrationStore = params.context.openPluginStateKeyedStore({
namespace: REEF_DURABLE_MIGRATION_NAMESPACE,
maxEntries: 1,
overflowPolicy: "reject-new"
});
const durableSourceExists = (await Promise.all(REEF_DURABLE_LEGACY_FILENAMES.map((filename) => legacyReefFileExists(path.join(stateDir, filename))))).some(Boolean);
const durablePending = await durableMigrationStore.lookup(REEF_DURABLE_MIGRATION_KEY);
if (durableSourceExists || durablePending) await durableMigrationStore.register(REEF_DURABLE_MIGRATION_KEY, { pending: true });
if (!await legacyReefFileExists(filePath)) {
const pending = await migrationStore.lookup(REEF_KEYS_MIGRATION_KEY);
if (!pending) return {
changes,
warnings
};
try {
parseReefKeys(await store.lookup(REEF_KEYS_KEY));
if (!pending?.identityBindingRequired) {
await migrationStore.delete(REEF_KEYS_MIGRATION_KEY);
changes.push("Verified Reef identity keys; cleared completed migration marker");
}
} catch {
warnings.push("Reef identity key migration is incomplete and keys.json is missing; left migration blocker in place");
}
return {
changes,
warnings
};
}
const existingMarker = await migrationStore.lookup(REEF_KEYS_MIGRATION_KEY);
const configuredBinding = configuredReefIdentityBinding(params.config);
const identityBindingRequired = existingMarker?.identityBindingRequired || await legacyReefFileExists(path.join(resolveLegacyReefStateDir(params), "identity.json")) || configuredBinding.status !== "absent";
await migrationStore.register(REEF_KEYS_MIGRATION_KEY, {
pending: true,
identityBindingRequired
});
let keys;
try {
keys = parseReefKeys(JSON.parse(await fs.readFile(filePath, "utf8")));
} catch (error) {
if (error.code === "ENOENT") return {
changes,
warnings
};
warnings.push(`Failed importing Reef identity keys: ${String(error)}; left source in place`);
return {
changes,
warnings
};
}
const existing = await store.lookup(REEF_KEYS_KEY);
if (existing && JSON.stringify(existing) !== JSON.stringify(keys)) {
warnings.push("Kept existing Reef identity keys; left differing legacy source in place");
return {
changes,
warnings
};
}
if (!existing) try {
await store.registerIfAbsent(REEF_KEYS_KEY, keys);
} catch (error) {
warnings.push(`Failed importing Reef identity keys: ${String(error)}; left source in place`);
return {
changes,
warnings
};
}
const persisted = await store.lookup(REEF_KEYS_KEY);
try {
if (JSON.stringify(parseReefKeys(persisted)) !== JSON.stringify(keys)) throw new Error("persisted value differs");
} catch (error) {
warnings.push(`Failed verifying Reef identity keys after import: ${String(error)}; left source in place`);
return {
changes,
warnings
};
}
changes.push("Migrated Reef identity keys -> plugin state");
const warningCount = warnings.length;
await archiveLegacyStateSource({
filePath,
label: "Reef identity keys",
changes,
warnings
});
if (warnings.length === warningCount && !identityBindingRequired) await migrationStore.delete(REEF_KEYS_MIGRATION_KEY);
return {
changes,
warnings
};
}
},
{
id: "reef-registration-json-to-plugin-state",
label: "Reef registration state",
async detectLegacyState(params) {
const stateDir = resolveLegacyReefStateDir(params);
const migrationStore = params.context.openPluginStateKeyedStore({
namespace: REEF_KEYS_MIGRATION_NAMESPACE,
maxEntries: 1,
overflowPolicy: "reject-new"
});
const files = (await Promise.all(REEF_LEGACY_REGISTRATION_SOURCES.map(async (source) => ({
source,
exists: await legacyReefFileExists(path.join(stateDir, source.filename))
})))).filter((entry) => entry.exists);
const pending = await migrationStore.lookup(REEF_KEYS_MIGRATION_KEY);
const configuredBindingNeedsImport = configuredReefIdentityBinding(params.config).status !== "absent" && await legacyReefFileExists(path.join(stateDir, "keys.json"));
return files.length > 0 || pending?.identityBindingRequired || configuredBindingNeedsImport ? { preview: [files.length > 0 ? `- Reef registration state -> plugin state (${files.map((entry) => entry.source.filename).join(", ")})` : configuredBindingNeedsImport ? "- Reef configured identity binding -> plugin state" : "- Verify Reef identity binding migration marker"] } : null;
},
async migrateLegacyState(params) {
const changes = [];
const warnings = [];
const stateDir = resolveLegacyReefStateDir(params);
const store = params.context.openPluginStateKeyedStore({
namespace: REEF_REGISTRATION_NAMESPACE,
maxEntries: 2,
overflowPolicy: "reject-new"
});
const migrationStore = params.context.openPluginStateKeyedStore({
namespace: REEF_KEYS_MIGRATION_NAMESPACE,
maxEntries: 1,
overflowPolicy: "reject-new"
});
const durableMigrationStore = params.context.openPluginStateKeyedStore({
namespace: REEF_DURABLE_MIGRATION_NAMESPACE,
maxEntries: 1,
overflowPolicy: "reject-new"
});
if ((await Promise.all(REEF_LEGACY_REGISTRATION_SOURCES.map((source) => legacyReefFileExists(path.join(stateDir, source.filename))))).some(Boolean) || await migrationStore.lookup("keys-json") || await durableMigrationStore.lookup("legacy-files")) await durableMigrationStore.register(REEF_DURABLE_MIGRATION_KEY, { pending: true });
for (const source of REEF_LEGACY_REGISTRATION_SOURCES) {
const filePath = path.join(stateDir, source.filename);
if (!await legacyReefFileExists(filePath)) continue;
let legacy;
try {
legacy = source.parse(JSON.parse(await fs.readFile(filePath, "utf8")));
} catch {}
if (!legacy) {
warnings.push(`Failed importing ${source.label}: invalid JSON; left source in place`);
continue;
}
const existing = await store.lookup(source.key);
const normalizedExisting = source.parse(existing);
if (normalizedExisting && JSON.stringify(normalizedExisting) !== JSON.stringify(legacy)) {
warnings.push(`Kept existing ${source.label}; left differing legacy source in place`);
continue;
}
if (!normalizedExisting) try {
await store.registerIfAbsent(source.key, legacy);
} catch (error) {
warnings.push(`Failed importing ${source.label}: ${String(error)}; left source in place`);
continue;
}
const persisted = source.parse(await store.lookup(source.key));
if (!persisted || JSON.stringify(persisted) !== JSON.stringify(legacy)) {
warnings.push(`Failed verifying ${source.label}; left source in place`);
continue;
}
changes.push(`Migrated ${source.label} -> plugin state`);
await archiveLegacyStateSource({
filePath,
label: source.label,
changes,
warnings
});
}
const configuredBindingResult = configuredReefIdentityBinding(params.config);
const configuredBinding = configuredBindingResult.status === "valid" ? configuredBindingResult.binding : void 0;
if (configuredBinding) {
const existing = parseReefIdentityBinding(await store.lookup(REEF_REGISTRATION_IDENTITY_KEY));
if (existing && JSON.stringify(existing) !== JSON.stringify(configuredBinding)) warnings.push("Kept existing Reef identity binding; configured handle or relay differs");
else if (!existing) try {
await store.registerIfAbsent(REEF_REGISTRATION_IDENTITY_KEY, configuredBinding);
const persisted = parseReefIdentityBinding(await store.lookup(REEF_REGISTRATION_IDENTITY_KEY));
if (JSON.stringify(persisted) !== JSON.stringify(configuredBinding)) throw new Error("persisted value differs");
changes.push("Migrated Reef identity binding from config -> plugin state");
} catch (error) {
warnings.push(`Failed importing Reef identity binding from config: ${String(error)}`);
}
}
if ((await migrationStore.lookup("keys-json"))?.identityBindingRequired) {
const keysPath = path.join(stateDir, "keys.json");
const identityPath = path.join(stateDir, "identity.json");
try {
parseReefKeys(await params.context.openPluginStateKeyedStore({
namespace: REEF_KEYS_NAMESPACE,
maxEntries: 1,
overflowPolicy: "reject-new"
}).lookup(REEF_KEYS_KEY));
const binding = parseReefIdentityBinding(await store.lookup(REEF_REGISTRATION_IDENTITY_KEY));
if (!binding) throw new Error("canonical identity binding is missing");
if (configuredBindingResult.status === "invalid") throw new Error("configured handle or relay is invalid");
if (configuredBinding && JSON.stringify(binding) !== JSON.stringify(configuredBinding)) throw new Error("configured handle or relay differs from canonical identity binding");
if (await legacyReefFileExists(keysPath) || await legacyReefFileExists(identityPath)) throw new Error("legacy identity sources remain");
await migrationStore.delete(REEF_KEYS_MIGRATION_KEY);
changes.push("Verified Reef identity keys and binding; cleared migration marker");
} catch (error) {
warnings.push(`Reef identity migration is incomplete: ${String(error)}; left migration blocker in place`);
}
}
return {
changes,
warnings
};
}
},
reefAuditStateMigration,
reefRuntimeStateMigration,
{
id: "reef-config-trust-to-plugin-state",
label: "Reef peer trust",
async detectLegacyState({ config, context }) {
const legacy = inspectLegacyReefFriends(config);
const markerStore = context.openPluginStateKeyedStore({
namespace: REEF_CONFIG_IMPORT_NAMESPACE,
maxEntries: REEF_TRUST_STORE_MAX_ENTRIES,
overflowPolicy: "reject-new"
});
const markedKeys = new Set((await markerStore.entries()).map((entry) => entry.key));
const legacyConfig = legacy?.config;
const count = legacyConfig ? [...legacy.friends.keys()].filter((peer) => !markedKeys.has(resolveReefTrustStoreKey(legacyConfig, peer))).length : legacy?.friends.size ?? 0;
const rejected = legacy?.rejected ?? 0;
return count > 0 || rejected > 0 ? { preview: [`- Reef peer trust: config -> plugin state (${count} peer(s), ${rejected} invalid)`] } : null;
},
async migrateLegacyState({ config, context }) {
const legacy = inspectLegacyReefFriends(config);
if (!legacy) return {
changes: [],
warnings: []
};
const warnings = [];
if (legacy.rejected > 0) warnings.push(`Skipped ${legacy.rejected} invalid Reef peer trust row(s); left legacy friends config in place`);
if (!legacy.config) {
if (legacy.total > 0) warnings.push("Skipped Reef peer trust migration because channels.reef needs a valid handle and canonical config; left legacy friends config in place");
return {
changes: [],
warnings
};
}
const reefConfig = legacy.config;
if (legacy.friends.size === 0) return {
changes: [],
warnings
};
const store = context.openPluginStateKeyedStore({
namespace: REEF_TRUST_STORE_NAMESPACE,
maxEntries: REEF_TRUST_STORE_MAX_ENTRIES,
overflowPolicy: "reject-new"
});
const markerStore = context.openPluginStateKeyedStore({
namespace: REEF_CONFIG_IMPORT_NAMESPACE,
maxEntries: REEF_TRUST_STORE_MAX_ENTRIES,
overflowPolicy: "reject-new"
});
const existingEntries = await store.entries();
const existingKeys = new Set(existingEntries.map((entry) => entry.key));
const markerEntries = await markerStore.entries();
const markedKeys = new Set(markerEntries.map((entry) => entry.key));
const pendingKeys = [...legacy.friends.keys()].map((peer) => resolveReefTrustStoreKey(reefConfig, peer)).filter((key) => !markedKeys.has(key));
const missingTrust = pendingKeys.filter((key) => !existingKeys.has(key));
const availableTrust = Math.max(0, REEF_TRUST_STORE_MAX_ENTRIES - existingEntries.length);
const availableMarkers = Math.max(0, REEF_TRUST_STORE_MAX_ENTRIES - markerEntries.length);
if (missingTrust.length > availableTrust || pendingKeys.length > availableMarkers) {
warnings.push(`Skipped Reef peer trust migration because plugin state has room for ${availableTrust} of ${missingTrust.length} trust row(s) and ${availableMarkers} of ${pendingKeys.length} import marker(s); left legacy friends config in place`);
return {
changes: [],
warnings
};
}
let imported = 0;
let alreadyPresent = 0;
for (const [peer, trust] of legacy.friends) {
const key = resolveReefTrustStoreKey(reefConfig, peer);
if (markedKeys.has(key)) continue;
if (await store.registerIfAbsent(key, {
revision: 1,
trust: {
...trust,
approvedAt: 0
}
})) imported++;
else alreadyPresent++;
await markerStore.registerIfAbsent(key, {
version: 1,
importedAt: Date.now()
});
markedKeys.add(key);
}
if (imported === 0 && alreadyPresent === 0) return {
changes: [],
warnings
};
return {
changes: [`Migrated Reef peer trust -> plugin state (${imported} imported, ${alreadyPresent} already present)`],
warnings
};
}
}
];
//#endregion
export { legacyConfigRules, normalizeCompatibilityConfig, stateMigrations };