From ae02f4014456cbf7ea5cfaac658b8e1d6b8c5890 Mon Sep 17 00:00:00 2001 From: Josh Lehman Date: Thu, 19 Mar 2026 14:21:42 -0700 Subject: [PATCH] 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 --- src/infra/matrix-plugin-helper.test.ts | 72 ++++++++++++++++++++++++++ src/infra/matrix-plugin-helper.ts | 37 ++++++++++--- 2 files changed, 103 insertions(+), 6 deletions(-) diff --git a/src/infra/matrix-plugin-helper.test.ts b/src/infra/matrix-plugin-helper.test.ts index ae71aca0bc8..602d151c853 100644 --- a/src/infra/matrix-plugin-helper.test.ts +++ b/src/infra/matrix-plugin-helper.test.ts @@ -25,6 +25,22 @@ function writeMatrixPluginFixture(rootDir: string, helperBody: string): void { fs.writeFileSync(path.join(rootDir, "legacy-crypto-inspector.js"), helperBody, "utf8"); } +function writeMatrixPluginManifest(rootDir: string): void { + fs.mkdirSync(rootDir, { recursive: true }); + fs.writeFileSync( + path.join(rootDir, "openclaw.plugin.json"), + JSON.stringify({ + id: "matrix", + configSchema: { + type: "object", + additionalProperties: false, + }, + }), + "utf8", + ); + fs.writeFileSync(path.join(rootDir, "index.js"), "export default {};\n", "utf8"); +} + describe("matrix plugin helper resolution", () => { it("loads the legacy crypto inspector from the bundled matrix plugin", async () => { await withTempHome( @@ -125,6 +141,62 @@ describe("matrix plugin helper resolution", () => { ); }); + it("keeps source-style root helper shims on the Jiti fallback path", async () => { + await withTempHome( + async (home) => { + const customRoot = path.join(home, "plugins", "matrix-local"); + writeMatrixPluginManifest(customRoot); + fs.mkdirSync(path.join(customRoot, "src", "matrix"), { recursive: true }); + fs.writeFileSync( + path.join(customRoot, "legacy-crypto-inspector.js"), + 'export { inspectLegacyMatrixCryptoStore } from "./src/matrix/legacy-crypto-inspector.js";\n', + "utf8", + ); + fs.writeFileSync( + path.join(customRoot, "src", "matrix", "legacy-crypto-inspector.ts"), + [ + "export async function inspectLegacyMatrixCryptoStore() {", + ' return { deviceId: "SRCJS", roomKeyCounts: null, backupVersion: null, decryptionKeyBase64: null };', + "}", + ].join("\n"), + "utf8", + ); + + const cfg: OpenClawConfig = { + plugins: { + load: { + paths: [customRoot], + }, + }, + }; + + expect(isMatrixLegacyCryptoInspectorAvailable({ cfg, env: process.env })).toBe(true); + const inspectLegacyStore = await loadMatrixLegacyCryptoInspector({ + cfg, + env: process.env, + }); + + await expect( + inspectLegacyStore({ + cryptoRootDir: "/tmp/legacy", + userId: "@bot:example.org", + deviceId: "DEVICE123", + }), + ).resolves.toEqual({ + deviceId: "SRCJS", + roomKeyCounts: null, + backupVersion: null, + decryptionKeyBase64: null, + }); + }, + { + env: { + OPENCLAW_BUNDLED_PLUGINS_DIR: (home) => path.join(home, "empty-bundled"), + }, + }, + ); + }); + it("rejects helper files that escape the plugin root", async () => { await withTempHome( async (home) => { diff --git a/src/infra/matrix-plugin-helper.ts b/src/infra/matrix-plugin-helper.ts index ab40287029f..a0a78eb4d72 100644 --- a/src/infra/matrix-plugin-helper.ts +++ b/src/infra/matrix-plugin-helper.ts @@ -1,11 +1,13 @@ 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"; @@ -98,15 +100,26 @@ let jitiLoader: ReturnType | null = null; const inspectorCache = new Map>(); function getJiti() { - if (!jitiLoader) { - jitiLoader = createJiti(import.meta.url, { - interopDefault: false, - extensions: [".ts", ".tsx", ".mts", ".cts", ".js", ".mjs", ".cjs", ".json"], - }); + 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 { return typeof value === "object" && value !== null; } @@ -154,7 +167,19 @@ export async function loadMatrixLegacyCryptoInspector(params: { } const pending = (async () => { - const loaded: unknown = await getJiti().import(helperPath); + 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(