openclaw/src/infra/matrix-plugin-helper.ts
Josh Lehman ae02f40144
fix: load matrix legacy helper through native ESM when possible (#50623)
* fix(matrix): load legacy helper natively when possible

* fix(matrix): narrow jiti fallback to source helpers

* fix(matrix): fall back to jiti for source-style helper wrappers
2026-03-19 14:21:42 -07:00

199 lines
6.1 KiB
TypeScript

import fs from "node:fs";
import path from "node:path";
import { pathToFileURL } from "node:url";
import { createJiti } from "jiti";
import type { OpenClawConfig } from "../config/config.js";
import {
loadPluginManifestRegistry,
type PluginManifestRecord,
} from "../plugins/manifest-registry.js";
import { shouldPreferNativeJiti } from "../plugins/sdk-alias.js";
import { openBoundaryFileSync } from "./boundary-file-read.js";
const MATRIX_PLUGIN_ID = "matrix";
const MATRIX_HELPER_CANDIDATES = [
"legacy-crypto-inspector.ts",
"legacy-crypto-inspector.js",
path.join("dist", "legacy-crypto-inspector.js"),
] as const;
export const MATRIX_LEGACY_CRYPTO_INSPECTOR_UNAVAILABLE_MESSAGE =
"Legacy Matrix encrypted state was detected, but the Matrix plugin helper is unavailable. Install or repair @openclaw/matrix so OpenClaw can inspect the old rust crypto store before upgrading.";
type MatrixLegacyCryptoInspectorParams = {
cryptoRootDir: string;
userId: string;
deviceId: string;
log?: (message: string) => void;
};
type MatrixLegacyCryptoInspectorResult = {
deviceId: string | null;
roomKeyCounts: {
total: number;
backedUp: number;
} | null;
backupVersion: string | null;
decryptionKeyBase64: string | null;
};
export type MatrixLegacyCryptoInspector = (
params: MatrixLegacyCryptoInspectorParams,
) => Promise<MatrixLegacyCryptoInspectorResult>;
function resolveMatrixPluginRecord(params: {
cfg: OpenClawConfig;
env?: NodeJS.ProcessEnv;
workspaceDir?: string;
}): PluginManifestRecord | null {
const registry = loadPluginManifestRegistry({
config: params.cfg,
workspaceDir: params.workspaceDir,
env: params.env,
});
return registry.plugins.find((plugin) => plugin.id === MATRIX_PLUGIN_ID) ?? null;
}
type MatrixLegacyCryptoInspectorPathResolution =
| { status: "ok"; helperPath: string }
| { status: "missing" }
| { status: "unsafe"; candidatePath: string };
function resolveMatrixLegacyCryptoInspectorPath(params: {
cfg: OpenClawConfig;
env?: NodeJS.ProcessEnv;
workspaceDir?: string;
}): MatrixLegacyCryptoInspectorPathResolution {
const plugin = resolveMatrixPluginRecord(params);
if (!plugin) {
return { status: "missing" };
}
for (const relativePath of MATRIX_HELPER_CANDIDATES) {
const candidatePath = path.join(plugin.rootDir, relativePath);
const opened = openBoundaryFileSync({
absolutePath: candidatePath,
rootPath: plugin.rootDir,
boundaryLabel: "plugin root",
rejectHardlinks: plugin.origin !== "bundled",
allowedType: "file",
});
if (opened.ok) {
fs.closeSync(opened.fd);
return { status: "ok", helperPath: opened.path };
}
if (opened.reason !== "path") {
return { status: "unsafe", candidatePath };
}
}
return { status: "missing" };
}
export function isMatrixLegacyCryptoInspectorAvailable(params: {
cfg: OpenClawConfig;
env?: NodeJS.ProcessEnv;
workspaceDir?: string;
}): boolean {
return resolveMatrixLegacyCryptoInspectorPath(params).status === "ok";
}
let jitiLoader: ReturnType<typeof createJiti> | null = null;
const inspectorCache = new Map<string, Promise<MatrixLegacyCryptoInspector>>();
function getJiti() {
if (jitiLoader) {
return jitiLoader;
}
jitiLoader = createJiti(import.meta.url, {
interopDefault: false,
tryNative: false,
extensions: [".ts", ".tsx", ".mts", ".cts", ".mtsx", ".ctsx", ".js", ".mjs", ".cjs", ".json"],
});
return jitiLoader;
}
function canRetryWithJiti(error: unknown): boolean {
if (!error || typeof error !== "object") {
return false;
}
const code = "code" in error ? (error as { code?: unknown }).code : undefined;
return code === "ERR_MODULE_NOT_FOUND" || code === "ERR_UNKNOWN_FILE_EXTENSION";
}
function isObjectRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null;
}
function resolveInspectorExport(loaded: unknown): MatrixLegacyCryptoInspector | null {
if (!isObjectRecord(loaded)) {
return null;
}
const directInspector = loaded.inspectLegacyMatrixCryptoStore;
if (typeof directInspector === "function") {
return directInspector as MatrixLegacyCryptoInspector;
}
const directDefault = loaded.default;
if (typeof directDefault === "function") {
return directDefault as MatrixLegacyCryptoInspector;
}
if (!isObjectRecord(directDefault)) {
return null;
}
const nestedInspector = directDefault.inspectLegacyMatrixCryptoStore;
return typeof nestedInspector === "function"
? (nestedInspector as MatrixLegacyCryptoInspector)
: null;
}
export async function loadMatrixLegacyCryptoInspector(params: {
cfg: OpenClawConfig;
env?: NodeJS.ProcessEnv;
workspaceDir?: string;
}): Promise<MatrixLegacyCryptoInspector> {
const resolution = resolveMatrixLegacyCryptoInspectorPath(params);
if (resolution.status === "missing") {
throw new Error(MATRIX_LEGACY_CRYPTO_INSPECTOR_UNAVAILABLE_MESSAGE);
}
if (resolution.status === "unsafe") {
throw new Error(
`Matrix plugin helper path is unsafe: ${resolution.candidatePath}. Reinstall @openclaw/matrix and try again.`,
);
}
const helperPath = resolution.helperPath;
const cached = inspectorCache.get(helperPath);
if (cached) {
return await cached;
}
const pending = (async () => {
let loaded: unknown;
if (shouldPreferNativeJiti(helperPath)) {
try {
loaded = await import(pathToFileURL(helperPath).href);
} catch (error) {
if (!canRetryWithJiti(error)) {
throw error;
}
loaded = getJiti()(helperPath);
}
} else {
loaded = getJiti()(helperPath);
}
const inspectLegacyMatrixCryptoStore = resolveInspectorExport(loaded);
if (!inspectLegacyMatrixCryptoStore) {
throw new Error(
`Matrix plugin helper at ${helperPath} does not export inspectLegacyMatrixCryptoStore(). Reinstall @openclaw/matrix and try again.`,
);
}
return inspectLegacyMatrixCryptoStore;
})();
inspectorCache.set(helperPath, pending);
try {
return await pending;
} catch (err) {
inspectorCache.delete(helperPath);
throw err;
}
}