diff --git a/extensions/matrix/auth-presence.ts b/extensions/matrix/auth-presence.ts new file mode 100644 index 00000000000..d4fce07fa44 --- /dev/null +++ b/extensions/matrix/auth-presence.ts @@ -0,0 +1,59 @@ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; +import { resolveStateDir } from "openclaw/plugin-sdk/state-paths"; +import { + resolveMatrixCredentialsDir, + resolveMatrixCredentialsFilename, +} from "./src/storage-paths.js"; + +type MatrixAuthPresenceParams = + | { + cfg: OpenClawConfig; + env?: NodeJS.ProcessEnv; + } + | OpenClawConfig; + +function listMatrixCredentialPaths( + _cfg: OpenClawConfig, + env: NodeJS.ProcessEnv = process.env, +): readonly string[] { + const credentialsDir = resolveMatrixCredentialsDir(resolveStateDir(env, os.homedir)); + const paths = new Set([ + resolveMatrixCredentialsFilename(), + resolveMatrixCredentialsFilename("default"), + ]); + + try { + const entries = fs.readdirSync(credentialsDir, { withFileTypes: true }); + for (const entry of entries) { + if (entry.isFile() && /^credentials(?:-[a-z0-9._-]+)?\.json$/i.test(entry.name)) { + paths.add(entry.name); + } + } + } catch { + // Missing credentials directories mean no persisted Matrix auth state. + } + + return [...paths].map((filename) => path.join(credentialsDir, filename)); +} + +export function hasAnyMatrixAuth( + params: MatrixAuthPresenceParams, + env: NodeJS.ProcessEnv = process.env, +): boolean { + const cfg = + params && typeof params === "object" && "cfg" in params + ? params.cfg + : (params as OpenClawConfig); + const resolvedEnv = + params && typeof params === "object" && "cfg" in params ? (params.env ?? env) : env; + return listMatrixCredentialPaths(cfg, resolvedEnv).some((filePath) => { + try { + return fs.existsSync(filePath); + } catch { + return false; + } + }); +} diff --git a/extensions/matrix/channel-plugin-api.ts b/extensions/matrix/channel-plugin-api.ts new file mode 100644 index 00000000000..20c485c90b8 --- /dev/null +++ b/extensions/matrix/channel-plugin-api.ts @@ -0,0 +1,3 @@ +// Keep bundled channel entry imports narrow so bootstrap/discovery paths do +// not drag Matrix setup and onboarding helpers into lightweight plugin loads. +export { matrixPlugin } from "./src/channel.js"; diff --git a/extensions/matrix/index.ts b/extensions/matrix/index.ts index f4be1a80105..86a4e52f1b4 100644 --- a/extensions/matrix/index.ts +++ b/extensions/matrix/index.ts @@ -8,7 +8,7 @@ export default defineBundledChannelEntry({ description: "Matrix channel plugin (matrix-js-sdk)", importMetaUrl: import.meta.url, plugin: { - specifier: "./api.js", + specifier: "./channel-plugin-api.js", exportName: "matrixPlugin", }, runtime: { diff --git a/extensions/matrix/package.json b/extensions/matrix/package.json index 1a0014e5711..a5c26a36b64 100644 --- a/extensions/matrix/package.json +++ b/extensions/matrix/package.json @@ -35,7 +35,11 @@ "docsLabel": "matrix", "blurb": "open protocol; install the plugin to enable.", "order": 70, - "quickstartAllowFrom": true + "quickstartAllowFrom": true, + "persistedAuthState": { + "specifier": "./auth-presence", + "exportName": "hasAnyMatrixAuth" + } }, "install": { "npmSpec": "@openclaw/matrix", diff --git a/extensions/matrix/setup-entry.ts b/extensions/matrix/setup-entry.ts index d23aabec32c..fd5747b3556 100644 --- a/extensions/matrix/setup-entry.ts +++ b/extensions/matrix/setup-entry.ts @@ -3,7 +3,7 @@ import { defineBundledChannelSetupEntry } from "openclaw/plugin-sdk/channel-entr export default defineBundledChannelSetupEntry({ importMetaUrl: import.meta.url, plugin: { - specifier: "./api.js", + specifier: "./channel-plugin-api.js", exportName: "matrixPlugin", }, }); diff --git a/src/channels/config-presence.test.ts b/src/channels/config-presence.test.ts index 76fb098fdbc..06018f4639b 100644 --- a/src/channels/config-presence.test.ts +++ b/src/channels/config-presence.test.ts @@ -71,4 +71,26 @@ describe("config presence", () => { expectedConfigured: true, }); }); + + it("detects persisted Matrix credentials without config or env", () => { + const stateDir = makeTempStateDir(); + fs.mkdirSync(path.join(stateDir, "credentials", "matrix"), { recursive: true }); + fs.writeFileSync( + path.join(stateDir, "credentials", "matrix", "credentials.json"), + JSON.stringify({ + homeserver: "https://matrix.example.org", + userId: "@bot:example.org", + accessToken: "token", + }), + "utf8", + ); + const env = { OPENCLAW_STATE_DIR: stateDir } as NodeJS.ProcessEnv; + + expectPotentialConfiguredChannelCase({ + cfg: {}, + env, + expectedIds: ["matrix"], + expectedConfigured: true, + }); + }); }); diff --git a/src/config/channel-configured.test.ts b/src/config/channel-configured.test.ts index f97e871e437..360282103b6 100644 --- a/src/config/channel-configured.test.ts +++ b/src/config/channel-configured.test.ts @@ -1,6 +1,26 @@ -import { describe, expect, it } from "vitest"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; import { isChannelConfigured } from "./channel-configured.js"; +const tempDirs: string[] = []; + +function makeTempStateDir() { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-channel-configured-")); + tempDirs.push(dir); + return dir; +} + +afterEach(() => { + while (tempDirs.length > 0) { + const dir = tempDirs.pop(); + if (dir) { + fs.rmSync(dir, { recursive: true, force: true }); + } + } +}); + describe("isChannelConfigured", () => { it("detects Telegram env configuration through the package metadata seam", () => { expect(isChannelConfigured({}, "telegram", { TELEGRAM_BOT_TOKEN: "token" })).toBe(true); @@ -39,4 +59,20 @@ describe("isChannelConfigured", () => { ), ).toBe(true); }); + + it("detects persisted Matrix credentials through package metadata", () => { + const stateDir = makeTempStateDir(); + fs.mkdirSync(path.join(stateDir, "credentials", "matrix"), { recursive: true }); + fs.writeFileSync( + path.join(stateDir, "credentials", "matrix", "credentials-ops.json"), + JSON.stringify({ + homeserver: "https://matrix.example.org", + userId: "@ops:example.org", + accessToken: "token", + }), + "utf8", + ); + + expect(isChannelConfigured({}, "matrix", { OPENCLAW_STATE_DIR: stateDir })).toBe(true); + }); }); diff --git a/src/plugins/bundled-plugin-metadata.test.ts b/src/plugins/bundled-plugin-metadata.test.ts index fae6f3065ae..e19746f040b 100644 --- a/src/plugins/bundled-plugin-metadata.test.ts +++ b/src/plugins/bundled-plugin-metadata.test.ts @@ -105,6 +105,12 @@ describe("bundled plugin metadata", () => { specifier: "./auth-presence", exportName: "hasAnyWhatsAppAuth", }); + + const matrix = listBundledPluginMetadata().find((entry) => entry.dirName === "matrix"); + expect(matrix?.packageManifest?.channel?.persistedAuthState).toEqual({ + specifier: "./auth-presence", + exportName: "hasAnyMatrixAuth", + }); }); it("keeps bundled configured-state metadata on channel package manifests", () => {