From 877069783b1c84e3d91bd3738ebeacd568fda762 Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Mon, 23 Feb 2026 00:44:44 -0500 Subject: [PATCH] Matrix-js: wire account-aware client and config plumbing --- extensions/matrix-js/src/actions.ts | 7 +- .../matrix-js/src/channel.directory.test.ts | 6 +- extensions/matrix-js/src/channel.ts | 38 ++--- .../matrix-js/src/directory-live.test.ts | 2 +- .../matrix-js/src/matrix/accounts.test.ts | 6 +- extensions/matrix-js/src/matrix/accounts.ts | 8 +- .../matrix-js/src/matrix/actions/client.ts | 5 +- .../matrix-js/src/matrix/active-client.ts | 25 ++- .../matrix-js/src/matrix/client.test.ts | 34 ++-- .../matrix-js/src/matrix/client/config.ts | 148 +++++++++++++++--- .../src/matrix/client/create-client.ts | 2 +- .../src/matrix/client/register-mode.test.ts | 10 +- .../src/matrix/client/register-mode.ts | 8 +- .../matrix-js/src/matrix/client/shared.ts | 20 ++- .../matrix-js/src/matrix/client/storage.ts | 6 +- .../matrix-js/src/matrix/credentials.ts | 2 +- extensions/matrix-js/src/matrix/send.ts | 6 +- .../matrix-js/src/matrix/send/client.ts | 11 +- extensions/matrix-js/src/onboarding.ts | 51 +++--- extensions/matrix-js/src/outbound.ts | 6 +- extensions/matrix-js/src/types.ts | 2 +- 21 files changed, 277 insertions(+), 126 deletions(-) diff --git a/extensions/matrix-js/src/actions.ts b/extensions/matrix-js/src/actions.ts index 9b4f3ac89ca..6e60921c169 100644 --- a/extensions/matrix-js/src/actions.ts +++ b/extensions/matrix-js/src/actions.ts @@ -17,7 +17,7 @@ export const matrixMessageActions: ChannelMessageActionAdapter = { if (!account.enabled || !account.configured) { return []; } - const gate = createActionGate((cfg as CoreConfig).channels?.matrix?.actions); + const gate = createActionGate((cfg as CoreConfig).channels?.["matrix-js"]?.actions); const actions = new Set(["send", "poll"]); if (gate("reactions")) { actions.add("react"); @@ -203,6 +203,9 @@ export const matrixMessageActions: ChannelMessageActionAdapter = { .toLowerCase(); const operationToAction: Record = { "encryption-status": "encryptionStatus", + "verification-status": "verificationStatus", + "verification-bootstrap": "verificationBootstrap", + "verification-recovery-key": "verificationRecoveryKey", "verification-list": "verificationList", "verification-request": "verificationRequest", "verification-accept": "verificationAccept", @@ -232,6 +235,6 @@ export const matrixMessageActions: ChannelMessageActionAdapter = { ); } - throw new Error(`Action ${action} is not supported for provider matrix.`); + throw new Error(`Action ${action} is not supported for provider matrix-js.`); }, }; diff --git a/extensions/matrix-js/src/channel.directory.test.ts b/extensions/matrix-js/src/channel.directory.test.ts index bfc1c0c5c35..16d9b1b12d1 100644 --- a/extensions/matrix-js/src/channel.directory.test.ts +++ b/extensions/matrix-js/src/channel.directory.test.ts @@ -24,7 +24,7 @@ describe("matrix directory", () => { it("lists peers and groups from config", async () => { const cfg = { channels: { - matrix: { + "matrix-js": { dm: { allowFrom: ["matrix:@alice:example.org", "bob"] }, groupAllowFrom: ["@dana:example.org"], groups: { @@ -75,7 +75,7 @@ describe("matrix directory", () => { it("resolves replyToMode from account config", () => { const cfg = { channels: { - matrix: { + "matrix-js": { replyToMode: "off", accounts: { Assistant: { @@ -106,7 +106,7 @@ describe("matrix directory", () => { it("resolves group mention policy from account config", () => { const cfg = { channels: { - matrix: { + "matrix-js": { groups: { "!room:example.org": { requireMention: true }, }, diff --git a/extensions/matrix-js/src/channel.ts b/extensions/matrix-js/src/channel.ts index 30c691b8d9d..61cee32565d 100644 --- a/extensions/matrix-js/src/channel.ts +++ b/extensions/matrix-js/src/channel.ts @@ -38,9 +38,9 @@ import type { CoreConfig } from "./types.js"; let matrixStartupLock: Promise = Promise.resolve(); const meta = { - id: "matrix", - label: "Matrix", - selectionLabel: "Matrix (plugin)", + id: "matrix-js", + label: "Matrix-js", + selectionLabel: "Matrix-js (plugin)", docsPath: "/channels/matrix", docsLabel: "matrix", blurb: "open protocol; configure a homeserver + access token.", @@ -73,12 +73,12 @@ function buildMatrixConfigUpdate( initialSyncLimit?: number; }, ): CoreConfig { - const existing = cfg.channels?.matrix ?? {}; + const existing = cfg.channels?.["matrix-js"] ?? {}; return { ...cfg, channels: { ...cfg.channels, - matrix: { + "matrix-js": { ...existing, enabled: true, ...(input.homeserver ? { homeserver: input.homeserver } : {}), @@ -96,7 +96,7 @@ function buildMatrixConfigUpdate( } export const matrixPlugin: ChannelPlugin = { - id: "matrix", + id: "matrix-js", meta, onboarding: matrixOnboardingAdapter, pairing: { @@ -113,7 +113,7 @@ export const matrixPlugin: ChannelPlugin = { threads: true, media: true, }, - reload: { configPrefixes: ["channels.matrix"] }, + reload: { configPrefixes: ["channels.matrix-js"] }, configSchema: buildChannelConfigSchema(MatrixConfigSchema), config: { listAccountIds: (cfg) => listMatrixAccountIds(cfg as CoreConfig), @@ -122,7 +122,7 @@ export const matrixPlugin: ChannelPlugin = { setAccountEnabled: ({ cfg, accountId, enabled }) => setAccountEnabledInConfigSection({ cfg: cfg as CoreConfig, - sectionKey: "matrix", + sectionKey: "matrix-js", accountId, enabled, allowTopLevel: true, @@ -130,7 +130,7 @@ export const matrixPlugin: ChannelPlugin = { deleteAccount: ({ cfg, accountId }) => deleteAccountFromConfigSection({ cfg: cfg as CoreConfig, - sectionKey: "matrix", + sectionKey: "matrix-js", accountId, clearBaseFields: [ "name", @@ -162,21 +162,21 @@ export const matrixPlugin: ChannelPlugin = { const accountId = account.accountId; const prefix = accountId && accountId !== "default" - ? `channels.matrix.accounts.${accountId}.dm` - : "channels.matrix.dm"; + ? `channels.matrix-js.accounts.${accountId}.dm` + : "channels.matrix-js.dm"; return { policy: account.config.dm?.policy ?? "pairing", allowFrom: account.config.dm?.allowFrom ?? [], policyPath: `${prefix}.policy`, allowFromPath: `${prefix}.allowFrom`, - approveHint: formatPairingApproveHint("matrix"), + approveHint: formatPairingApproveHint("matrix-js"), normalizeEntry: (raw) => normalizeMatrixUserId(raw), }; }, collectWarnings: ({ account, cfg }) => { const defaultGroupPolicy = resolveDefaultGroupPolicy(cfg as CoreConfig); const { groupPolicy } = resolveAllowlistProviderRuntimeGroupPolicy({ - providerConfigPresent: (cfg as CoreConfig).channels?.matrix !== undefined, + providerConfigPresent: (cfg as CoreConfig).channels?.["matrix-js"] !== undefined, groupPolicy: account.config.groupPolicy, defaultGroupPolicy, }); @@ -184,7 +184,7 @@ export const matrixPlugin: ChannelPlugin = { return []; } return [ - '- Matrix rooms: groupPolicy="open" allows any room to trigger (mention-gated). Set channels.matrix.groupPolicy="allowlist" + channels.matrix.groups (and optionally channels.matrix.groupAllowFrom) to restrict rooms.', + '- Matrix rooms: groupPolicy="open" allows any room to trigger (mention-gated). Set channels.matrix-js.groupPolicy="allowlist" + channels.matrix-js.groups (and optionally channels.matrix-js.groupAllowFrom) to restrict rooms.', ]; }, }, @@ -316,7 +316,7 @@ export const matrixPlugin: ChannelPlugin = { applyAccountName: ({ cfg, accountId, name }) => applyAccountNameToChannelSection({ cfg: cfg as CoreConfig, - channelKey: "matrix", + channelKey: "matrix-js", accountId, name, }), @@ -346,7 +346,7 @@ export const matrixPlugin: ChannelPlugin = { applyAccountConfig: ({ cfg, input }) => { const namedConfig = applyAccountNameToChannelSection({ cfg: cfg as CoreConfig, - channelKey: "matrix", + channelKey: "matrix-js", accountId: DEFAULT_ACCOUNT_ID, name: input.name, }); @@ -355,8 +355,8 @@ export const matrixPlugin: ChannelPlugin = { ...namedConfig, channels: { ...namedConfig.channels, - matrix: { - ...namedConfig.channels?.matrix, + "matrix-js": { + ...namedConfig.channels?.["matrix-js"], enabled: true, }, }, @@ -389,7 +389,7 @@ export const matrixPlugin: ChannelPlugin = { } return [ { - channel: "matrix", + channel: "matrix-js", accountId: account.accountId, kind: "runtime", message: `Channel error: ${lastError}`, diff --git a/extensions/matrix-js/src/directory-live.test.ts b/extensions/matrix-js/src/directory-live.test.ts index d499574bc8d..405624ccaa9 100644 --- a/extensions/matrix-js/src/directory-live.test.ts +++ b/extensions/matrix-js/src/directory-live.test.ts @@ -7,7 +7,7 @@ vi.mock("./matrix/client.js", () => ({ })); describe("matrix directory live", () => { - const cfg = { channels: { matrix: {} } }; + const cfg = { channels: { "matrix-js": {} } }; beforeEach(() => { vi.mocked(resolveMatrixAuth).mockReset(); diff --git a/extensions/matrix-js/src/matrix/accounts.test.ts b/extensions/matrix-js/src/matrix/accounts.test.ts index d453684756c..f61b123fc32 100644 --- a/extensions/matrix-js/src/matrix/accounts.test.ts +++ b/extensions/matrix-js/src/matrix/accounts.test.ts @@ -40,7 +40,7 @@ describe("resolveMatrixAccount", () => { it("treats access-token-only config as configured", () => { const cfg: CoreConfig = { channels: { - matrix: { + "matrix-js": { homeserver: "https://matrix.example.org", accessToken: "tok-access", }, @@ -54,7 +54,7 @@ describe("resolveMatrixAccount", () => { it("requires userId + password when no access token is set", () => { const cfg: CoreConfig = { channels: { - matrix: { + "matrix-js": { homeserver: "https://matrix.example.org", userId: "@bot:example.org", }, @@ -68,7 +68,7 @@ describe("resolveMatrixAccount", () => { it("marks password auth as configured when userId is present", () => { const cfg: CoreConfig = { channels: { - matrix: { + "matrix-js": { homeserver: "https://matrix.example.org", userId: "@bot:example.org", password: "secret", diff --git a/extensions/matrix-js/src/matrix/accounts.ts b/extensions/matrix-js/src/matrix/accounts.ts index ca0716ce505..ef2eb6d6b08 100644 --- a/extensions/matrix-js/src/matrix/accounts.ts +++ b/extensions/matrix-js/src/matrix/accounts.ts @@ -30,7 +30,7 @@ export type ResolvedMatrixAccount = { }; function listConfiguredAccountIds(cfg: CoreConfig): string[] { - const accounts = cfg.channels?.matrix?.accounts; + const accounts = cfg.channels?.["matrix-js"]?.accounts; if (!accounts || typeof accounts !== "object") { return []; } @@ -62,7 +62,7 @@ export function resolveDefaultMatrixAccountId(cfg: CoreConfig): string { } function resolveAccountConfig(cfg: CoreConfig, accountId: string): MatrixConfig | undefined { - const accounts = cfg.channels?.matrix?.accounts; + const accounts = cfg.channels?.["matrix-js"]?.accounts; if (!accounts || typeof accounts !== "object") { return undefined; } @@ -85,7 +85,7 @@ export function resolveMatrixAccount(params: { accountId?: string | null; }): ResolvedMatrixAccount { const accountId = normalizeAccountId(params.accountId); - const matrixBase = params.cfg.channels?.matrix ?? {}; + const matrixBase = params.cfg.channels?.["matrix-js"] ?? {}; const base = resolveMatrixAccountConfig({ cfg: params.cfg, accountId }); const enabled = base.enabled !== false && matrixBase.enabled !== false; @@ -120,7 +120,7 @@ export function resolveMatrixAccountConfig(params: { accountId?: string | null; }): MatrixConfig { const accountId = normalizeAccountId(params.accountId); - const matrixBase = params.cfg.channels?.matrix ?? {}; + const matrixBase = params.cfg.channels?.["matrix-js"] ?? {}; const accountConfig = resolveAccountConfig(params.cfg, accountId); if (!accountConfig) { return matrixBase; diff --git a/extensions/matrix-js/src/matrix/actions/client.ts b/extensions/matrix-js/src/matrix/actions/client.ts index 131eec67485..108be18aafd 100644 --- a/extensions/matrix-js/src/matrix/actions/client.ts +++ b/extensions/matrix-js/src/matrix/actions/client.ts @@ -22,7 +22,7 @@ export async function resolveActionClient( if (opts.client) { return { client: opts.client, stopOnDone: false }; } - const active = getActiveMatrixClient(); + const active = getActiveMatrixClient(opts.accountId); if (active) { return { client: active, stopOnDone: false }; } @@ -31,11 +31,13 @@ export async function resolveActionClient( const client = await resolveSharedMatrixClient({ cfg: getMatrixRuntime().config.loadConfig() as CoreConfig, timeoutMs: opts.timeoutMs, + accountId: opts.accountId, }); return { client, stopOnDone: false }; } const auth = await resolveMatrixAuth({ cfg: getMatrixRuntime().config.loadConfig() as CoreConfig, + accountId: opts.accountId, }); const client = await createMatrixClient({ homeserver: auth.homeserver, @@ -45,6 +47,7 @@ export async function resolveActionClient( deviceId: auth.deviceId, encryption: auth.encryption, localTimeoutMs: opts.timeoutMs, + accountId: opts.accountId, }); if (auth.encryption && client.crypto) { try { diff --git a/extensions/matrix-js/src/matrix/active-client.ts b/extensions/matrix-js/src/matrix/active-client.ts index dbb04ea347b..990acb6f116 100644 --- a/extensions/matrix-js/src/matrix/active-client.ts +++ b/extensions/matrix-js/src/matrix/active-client.ts @@ -1,11 +1,26 @@ +import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id"; import type { MatrixClient } from "./sdk.js"; -let activeClient: MatrixClient | null = null; +const activeClients = new Map(); -export function setActiveMatrixClient(client: MatrixClient | null): void { - activeClient = client; +function resolveAccountKey(accountId?: string | null): string { + const normalized = normalizeAccountId(accountId); + return normalized || DEFAULT_ACCOUNT_ID; } -export function getActiveMatrixClient(): MatrixClient | null { - return activeClient; +export function setActiveMatrixClient( + client: MatrixClient | null, + accountId?: string | null, +): void { + const key = resolveAccountKey(accountId); + if (!client) { + activeClients.delete(key); + return; + } + activeClients.set(key, client); +} + +export function getActiveMatrixClient(accountId?: string | null): MatrixClient | null { + const key = resolveAccountKey(accountId); + return activeClients.get(key) ?? null; } diff --git a/extensions/matrix-js/src/matrix/client.test.ts b/extensions/matrix-js/src/matrix/client.test.ts index 82fc5ce6ac8..27989345feb 100644 --- a/extensions/matrix-js/src/matrix/client.test.ts +++ b/extensions/matrix-js/src/matrix/client.test.ts @@ -26,7 +26,7 @@ describe("resolveMatrixConfig", () => { it("prefers config over env", () => { const cfg = { channels: { - matrix: { + "matrix-js": { homeserver: "https://cfg.example.org", userId: "@cfg:example.org", accessToken: "cfg-token", @@ -82,7 +82,7 @@ describe("resolveMatrixConfig", () => { it("reads register flag from config and env", () => { const cfg = { channels: { - matrix: { + "matrix-js": { register: true, }, }, @@ -118,7 +118,7 @@ describe("resolveMatrixAuth", () => { const cfg = { channels: { - matrix: { + "matrix-js": { homeserver: "https://matrix.example.org", userId: "@bot:example.org", password: "secret", @@ -154,6 +154,8 @@ describe("resolveMatrixAuth", () => { accessToken: "tok-123", deviceId: "DEVICE123", }), + expect.any(Object), + undefined, ); }); @@ -169,7 +171,7 @@ describe("resolveMatrixAuth", () => { const cfg = { channels: { - matrix: { + "matrix-js": { homeserver: "https://matrix.example.org", userId: "@newbot:example.org", password: "secret", @@ -224,7 +226,7 @@ describe("resolveMatrixAuth", () => { }); }); - it("ignores cached credentials when matrix.register=true", async () => { + it("ignores cached credentials when matrix-js.register=true", async () => { vi.mocked(credentialsModule.loadMatrixCredentials).mockReturnValue({ homeserver: "https://matrix.example.org", userId: "@bot:example.org", @@ -242,7 +244,7 @@ describe("resolveMatrixAuth", () => { const cfg = { channels: { - matrix: { + "matrix-js": { homeserver: "https://matrix.example.org", userId: "@bot:example.org", password: "secret", @@ -265,10 +267,10 @@ describe("resolveMatrixAuth", () => { expect(prepareMatrixRegisterModeMock).toHaveBeenCalledTimes(1); }); - it("requires matrix.password when matrix.register=true", async () => { + it("requires matrix-js.password when matrix-js.register=true", async () => { const cfg = { channels: { - matrix: { + "matrix-js": { homeserver: "https://matrix.example.org", userId: "@bot:example.org", register: true, @@ -277,16 +279,16 @@ describe("resolveMatrixAuth", () => { } as CoreConfig; await expect(resolveMatrixAuth({ cfg, env: {} as NodeJS.ProcessEnv })).rejects.toThrow( - "Matrix password is required when matrix.register=true", + "Matrix password is required when matrix-js.register=true", ); expect(prepareMatrixRegisterModeMock).not.toHaveBeenCalled(); expect(finalizeMatrixRegisterConfigAfterSuccessMock).not.toHaveBeenCalled(); }); - it("requires matrix.userId when matrix.register=true", async () => { + it("requires matrix-js.userId when matrix-js.register=true", async () => { const cfg = { channels: { - matrix: { + "matrix-js": { homeserver: "https://matrix.example.org", password: "secret", register: true, @@ -295,7 +297,7 @@ describe("resolveMatrixAuth", () => { } as CoreConfig; await expect(resolveMatrixAuth({ cfg, env: {} as NodeJS.ProcessEnv })).rejects.toThrow( - "Matrix userId is required when matrix.register=true", + "Matrix userId is required when matrix-js.register=true", ); expect(prepareMatrixRegisterModeMock).not.toHaveBeenCalled(); expect(finalizeMatrixRegisterConfigAfterSuccessMock).not.toHaveBeenCalled(); @@ -312,7 +314,7 @@ describe("resolveMatrixAuth", () => { const cfg = { channels: { - matrix: { + "matrix-js": { homeserver: "https://matrix.example.org", userId: "@bot:example.org", accessToken: "tok-123", @@ -332,6 +334,8 @@ describe("resolveMatrixAuth", () => { accessToken: "tok-123", deviceId: "DEVICE123", }), + expect.any(Object), + undefined, ); }); @@ -343,7 +347,7 @@ describe("resolveMatrixAuth", () => { const cfg = { channels: { - matrix: { + "matrix-js": { homeserver: "https://matrix.example.org", accessToken: "tok-123", encryption: true, @@ -377,7 +381,7 @@ describe("resolveMatrixAuth", () => { const cfg = { channels: { - matrix: { + "matrix-js": { homeserver: "https://matrix.example.org", userId: "@bot:example.org", deviceId: "DEVICE123", diff --git a/extensions/matrix-js/src/matrix/client/config.ts b/extensions/matrix-js/src/matrix/client/config.ts index 90bd5ff85fa..444211f9359 100644 --- a/extensions/matrix-js/src/matrix/client/config.ts +++ b/extensions/matrix-js/src/matrix/client/config.ts @@ -1,3 +1,4 @@ +import { normalizeAccountId } from "openclaw/plugin-sdk/account-id"; import { getMatrixRuntime } from "../../runtime.js"; import { MatrixClient } from "../sdk.js"; import type { CoreConfig } from "../types.js"; @@ -32,6 +33,27 @@ function parseOptionalBoolean(value: unknown): boolean | undefined { return undefined; } +function findAccountConfig(cfg: CoreConfig, accountId: string): Record { + const accounts = cfg.channels?.["matrix-js"]?.accounts; + if (!accounts || typeof accounts !== "object") { + return {}; + } + if (accounts[accountId] && typeof accounts[accountId] === "object") { + return accounts[accountId] as Record; + } + const normalized = normalizeAccountId(accountId); + for (const key of Object.keys(accounts)) { + if (normalizeAccountId(key) === normalized) { + const candidate = accounts[key]; + if (candidate && typeof candidate === "object") { + return candidate as Record; + } + return {}; + } + } + return {}; +} + function resolveMatrixLocalpart(userId: string): string { const trimmed = userId.trim(); const noPrefix = trimmed.startsWith("@") ? trimmed.slice(1) : trimmed; @@ -100,7 +122,7 @@ export function resolveMatrixConfig( cfg: CoreConfig = getMatrixRuntime().config.loadConfig() as CoreConfig, env: NodeJS.ProcessEnv = process.env, ): MatrixResolvedConfig { - const matrix = cfg.channels?.matrix ?? {}; + const matrix = cfg.channels?.["matrix-js"] ?? {}; const homeserver = clean(matrix.homeserver) || clean(env.MATRIX_HOMESERVER); const userId = clean(matrix.userId) || clean(env.MATRIX_USER_ID); const accessToken = clean(matrix.accessToken) || clean(env.MATRIX_ACCESS_TOKEN) || undefined; @@ -127,16 +149,86 @@ export function resolveMatrixConfig( }; } +export function resolveMatrixConfigForAccount( + cfg: CoreConfig, + accountId: string, + env: NodeJS.ProcessEnv = process.env, +): MatrixResolvedConfig { + const matrix = cfg.channels?.["matrix-js"] ?? {}; + const account = findAccountConfig(cfg, accountId); + + const accountHomeserver = clean( + typeof account.homeserver === "string" ? account.homeserver : undefined, + ); + const accountUserId = clean(typeof account.userId === "string" ? account.userId : undefined); + const accountAccessToken = clean( + typeof account.accessToken === "string" ? account.accessToken : undefined, + ); + const accountPassword = clean( + typeof account.password === "string" ? account.password : undefined, + ); + const accountDeviceId = clean( + typeof account.deviceId === "string" ? account.deviceId : undefined, + ); + const accountDeviceName = clean( + typeof account.deviceName === "string" ? account.deviceName : undefined, + ); + + const homeserver = accountHomeserver || clean(matrix.homeserver) || clean(env.MATRIX_HOMESERVER); + const userId = accountUserId || clean(matrix.userId) || clean(env.MATRIX_USER_ID); + const accessToken = + accountAccessToken || clean(matrix.accessToken) || clean(env.MATRIX_ACCESS_TOKEN) || undefined; + const password = + accountPassword || clean(matrix.password) || clean(env.MATRIX_PASSWORD) || undefined; + const register = + parseOptionalBoolean(account.register) ?? + parseOptionalBoolean(matrix.register) ?? + parseOptionalBoolean(env.MATRIX_REGISTER) ?? + false; + const deviceId = + accountDeviceId || clean(matrix.deviceId) || clean(env.MATRIX_DEVICE_ID) || undefined; + const deviceName = + accountDeviceName || clean(matrix.deviceName) || clean(env.MATRIX_DEVICE_NAME) || undefined; + + const accountInitialSyncLimit = + typeof account.initialSyncLimit === "number" + ? Math.max(0, Math.floor(account.initialSyncLimit)) + : undefined; + const initialSyncLimit = + accountInitialSyncLimit ?? + (typeof matrix.initialSyncLimit === "number" + ? Math.max(0, Math.floor(matrix.initialSyncLimit)) + : undefined); + const encryption = + typeof account.encryption === "boolean" ? account.encryption : (matrix.encryption ?? false); + + return { + homeserver, + userId, + accessToken, + password, + register, + deviceId, + deviceName, + initialSyncLimit, + encryption, + }; +} + export async function resolveMatrixAuth(params?: { cfg?: CoreConfig; env?: NodeJS.ProcessEnv; + accountId?: string | null; }): Promise { const cfg = params?.cfg ?? (getMatrixRuntime().config.loadConfig() as CoreConfig); const env = params?.env ?? process.env; - const resolved = resolveMatrixConfig(cfg, env); - const registerFromConfig = cfg.channels?.matrix?.register === true; + const accountId = params?.accountId; + const resolved = accountId + ? resolveMatrixConfigForAccount(cfg, accountId, env) + : resolveMatrixConfig(cfg, env); + const registerFromConfig = resolved.register === true; if (!resolved.homeserver) { - throw new Error("Matrix homeserver is required (matrix.homeserver)"); + throw new Error("Matrix homeserver is required (matrix-js.homeserver)"); } const { @@ -146,7 +238,7 @@ export async function resolveMatrixAuth(params?: { touchMatrixCredentials, } = await import("../credentials.js"); - const cached = loadMatrixCredentials(env); + const cached = loadMatrixCredentials(env, accountId); const cachedCredentials = cached && credentialsMatchConfig(cached, { @@ -158,10 +250,10 @@ export async function resolveMatrixAuth(params?: { if (registerFromConfig) { if (!resolved.userId) { - throw new Error("Matrix userId is required when matrix.register=true"); + throw new Error("Matrix userId is required when matrix-js.register=true"); } if (!resolved.password) { - throw new Error("Matrix password is required when matrix.register=true"); + throw new Error("Matrix password is required when matrix-js.register=true"); } await prepareMatrixRegisterMode({ cfg, @@ -205,14 +297,18 @@ export async function resolveMatrixAuth(params?: { cachedCredentials.userId !== userId || (cachedCredentials.deviceId || undefined) !== knownDeviceId; if (shouldRefreshCachedCredentials) { - saveMatrixCredentials({ - homeserver: resolved.homeserver, - userId, - accessToken: resolved.accessToken, - deviceId: knownDeviceId, - }); + saveMatrixCredentials( + { + homeserver: resolved.homeserver, + userId, + accessToken: resolved.accessToken, + deviceId: knownDeviceId, + }, + env, + accountId, + ); } else if (hasMatchingCachedToken) { - touchMatrixCredentials(env); + touchMatrixCredentials(env, accountId); } return { homeserver: resolved.homeserver, @@ -227,7 +323,7 @@ export async function resolveMatrixAuth(params?: { } if (cachedCredentials && !registerFromConfig) { - touchMatrixCredentials(env); + touchMatrixCredentials(env, accountId); return { homeserver: cachedCredentials.homeserver, userId: cachedCredentials.userId, @@ -241,12 +337,14 @@ export async function resolveMatrixAuth(params?: { } if (!resolved.userId) { - throw new Error("Matrix userId is required when no access token is configured (matrix.userId)"); + throw new Error( + "Matrix userId is required when no access token is configured (matrix-js.userId)", + ); } if (!resolved.password) { throw new Error( - "Matrix password is required when no access token is configured (matrix.password)", + "Matrix password is required when no access token is configured (matrix-js.password)", ); } @@ -308,12 +406,16 @@ export async function resolveMatrixAuth(params?: { encryption: resolved.encryption, }; - saveMatrixCredentials({ - homeserver: auth.homeserver, - userId: auth.userId, - accessToken: auth.accessToken, - deviceId: auth.deviceId, - }); + saveMatrixCredentials( + { + homeserver: auth.homeserver, + userId: auth.userId, + accessToken: auth.accessToken, + deviceId: auth.deviceId, + }, + env, + accountId, + ); if (registerFromConfig) { await finalizeMatrixRegisterConfigAfterSuccess({ diff --git a/extensions/matrix-js/src/matrix/client/create-client.ts b/extensions/matrix-js/src/matrix/client/create-client.ts index 7626a6c8477..f349a0565a9 100644 --- a/extensions/matrix-js/src/matrix/client/create-client.ts +++ b/extensions/matrix-js/src/matrix/client/create-client.ts @@ -40,7 +40,7 @@ export async function createMatrixClient(params: { accountId: params.accountId, }); - const cryptoDatabasePrefix = `openclaw-matrix-${storagePaths.accountKey}-${storagePaths.tokenHash}`; + const cryptoDatabasePrefix = `openclaw-matrix-js-${storagePaths.accountKey}-${storagePaths.tokenHash}`; return new MatrixClient(params.homeserver, params.accessToken, undefined, undefined, { userId: matrixClientUserId, diff --git a/extensions/matrix-js/src/matrix/client/register-mode.test.ts b/extensions/matrix-js/src/matrix/client/register-mode.test.ts index 7fb20f76387..bc82877556d 100644 --- a/extensions/matrix-js/src/matrix/client/register-mode.test.ts +++ b/extensions/matrix-js/src/matrix/client/register-mode.test.ts @@ -24,7 +24,7 @@ describe("matrix register mode helpers", () => { it("moves existing matrix state into a .bak snapshot before fresh registration", async () => { const stateDir = fs.mkdtempSync(path.join(os.tmpdir(), "matrix-register-mode-")); tempDirs.push(stateDir); - const credentialsDir = path.join(stateDir, "credentials", "matrix"); + const credentialsDir = path.join(stateDir, "credentials", "matrix-js"); const accountsDir = path.join(credentialsDir, "accounts"); fs.mkdirSync(accountsDir, { recursive: true }); fs.writeFileSync(path.join(credentialsDir, "credentials.json"), '{"accessToken":"old"}\n'); @@ -32,7 +32,7 @@ describe("matrix register mode helpers", () => { const cfg = { channels: { - matrix: { + "matrix-js": { userId: "@pinguini:matrix.gumadeiras.com", register: true, encryption: true, @@ -62,7 +62,7 @@ describe("matrix register mode helpers", () => { loadConfig: () => ({ channels: { - matrix: { + "matrix-js": { register: true, accessToken: "stale-token", userId: "@pinguini:matrix.gumadeiras.com", @@ -82,7 +82,7 @@ describe("matrix register mode helpers", () => { expect(writeConfigFile).toHaveBeenCalledWith( expect.objectContaining({ channels: expect.objectContaining({ - matrix: expect.objectContaining({ + "matrix-js": expect.objectContaining({ register: false, homeserver: "https://matrix.gumadeiras.com", userId: "@pinguini:matrix.gumadeiras.com", @@ -92,6 +92,6 @@ describe("matrix register mode helpers", () => { }), ); const written = writeConfigFile.mock.calls[0]?.[0] as CoreConfig; - expect(written.channels?.matrix?.accessToken).toBeUndefined(); + expect(written.channels?.["matrix-js"]?.accessToken).toBeUndefined(); }); }); diff --git a/extensions/matrix-js/src/matrix/client/register-mode.ts b/extensions/matrix-js/src/matrix/client/register-mode.ts index 4ffed2c9bf3..10672f7fbe0 100644 --- a/extensions/matrix-js/src/matrix/client/register-mode.ts +++ b/extensions/matrix-js/src/matrix/client/register-mode.ts @@ -65,7 +65,7 @@ export async function prepareMatrixRegisterMode(params: { const backupDir = path.join(backupRoot, buildBackupDirName()); fs.mkdirSync(backupDir, { recursive: true }); - const matrixConfig = params.cfg.channels?.matrix ?? {}; + const matrixConfig = params.cfg.channels?.["matrix-js"] ?? {}; fs.writeFileSync( path.join(backupDir, "matrix-config.json"), JSON.stringify(matrixConfig, null, 2).trimEnd().concat("\n"), @@ -93,11 +93,11 @@ export async function finalizeMatrixRegisterConfigAfterSuccess(params: { } const cfg = runtime.config.loadConfig() as CoreConfig; - if (cfg.channels?.matrix?.register !== true) { + if (cfg.channels?.["matrix-js"]?.register !== true) { return false; } - const matrixCfg = cfg.channels?.matrix ?? {}; + const matrixCfg = cfg.channels?.["matrix-js"] ?? {}; const nextMatrix: Record = { ...matrixCfg, register: false, @@ -112,7 +112,7 @@ export async function finalizeMatrixRegisterConfigAfterSuccess(params: { ...cfg, channels: { ...(cfg.channels ?? {}), - matrix: nextMatrix as CoreConfig["channels"]["matrix"], + "matrix-js": nextMatrix as CoreConfig["channels"]["matrix-js"], }, }; diff --git a/extensions/matrix-js/src/matrix/client/shared.ts b/extensions/matrix-js/src/matrix/client/shared.ts index 4f04ad71321..ecf121b3dbe 100644 --- a/extensions/matrix-js/src/matrix/client/shared.ts +++ b/extensions/matrix-js/src/matrix/client/shared.ts @@ -100,7 +100,13 @@ export async function resolveSharedMatrixClient( accountId?: string | null; } = {}, ): Promise { - const auth = params.auth ?? (await resolveMatrixAuth({ cfg: params.cfg, env: params.env })); + const auth = + params.auth ?? + (await resolveMatrixAuth({ + cfg: params.cfg, + env: params.env, + accountId: params.accountId, + })); const key = buildSharedClientKey(auth, params.accountId); const shouldStart = params.startClient !== false; @@ -171,3 +177,15 @@ export function stopSharedClient(): void { sharedClientState = null; } } + +export function stopSharedClientForAccount(auth: MatrixAuth, accountId?: string | null): void { + if (!sharedClientState) { + return; + } + const key = buildSharedClientKey(auth, accountId); + if (sharedClientState.key !== key) { + return; + } + sharedClientState.client.stop(); + sharedClientState = null; +} diff --git a/extensions/matrix-js/src/matrix/client/storage.ts b/extensions/matrix-js/src/matrix/client/storage.ts index 2575eb2c8f2..b09b6be2695 100644 --- a/extensions/matrix-js/src/matrix/client/storage.ts +++ b/extensions/matrix-js/src/matrix/client/storage.ts @@ -39,8 +39,8 @@ function resolveLegacyStoragePaths(env: NodeJS.ProcessEnv = process.env): { } { const stateDir = getMatrixRuntime().state.resolveStateDir(env, os.homedir); return { - storagePath: path.join(stateDir, "credentials", "matrix", "bot-storage.json"), - cryptoPath: path.join(stateDir, "credentials", "matrix", "crypto"), + storagePath: path.join(stateDir, "credentials", "matrix-js", "bot-storage.json"), + cryptoPath: path.join(stateDir, "credentials", "matrix-js", "crypto"), }; } @@ -60,7 +60,7 @@ export function resolveMatrixStoragePaths(params: { const rootDir = path.join( stateDir, "credentials", - "matrix", + "matrix-js", "accounts", accountKey, `${serverKey}__${userKey}`, diff --git a/extensions/matrix-js/src/matrix/credentials.ts b/extensions/matrix-js/src/matrix/credentials.ts index 7da620324d7..71e3d6e5113 100644 --- a/extensions/matrix-js/src/matrix/credentials.ts +++ b/extensions/matrix-js/src/matrix/credentials.ts @@ -28,7 +28,7 @@ export function resolveMatrixCredentialsDir( stateDir?: string, ): string { const resolvedStateDir = stateDir ?? getMatrixRuntime().state.resolveStateDir(env, os.homedir); - return path.join(resolvedStateDir, "credentials", "matrix"); + return path.join(resolvedStateDir, "credentials", "matrix-js"); } export function resolveMatrixCredentialsPath( diff --git a/extensions/matrix-js/src/matrix/send.ts b/extensions/matrix-js/src/matrix/send.ts index 977c868369c..7b85cb65ef4 100644 --- a/extensions/matrix-js/src/matrix/send.ts +++ b/extensions/matrix-js/src/matrix/send.ts @@ -52,16 +52,16 @@ export async function sendMessageMatrix( const cfg = getCore().config.loadConfig(); const tableMode = getCore().channel.text.resolveMarkdownTableMode({ cfg, - channel: "matrix", + channel: "matrix-js", accountId: opts.accountId, }); const convertedMessage = getCore().channel.text.convertMarkdownTables( trimmedMessage, tableMode, ); - const textLimit = getCore().channel.text.resolveTextChunkLimit(cfg, "matrix"); + const textLimit = getCore().channel.text.resolveTextChunkLimit(cfg, "matrix-js"); const chunkLimit = Math.min(textLimit, MATRIX_TEXT_LIMIT); - const chunkMode = getCore().channel.text.resolveChunkMode(cfg, "matrix", opts.accountId); + const chunkMode = getCore().channel.text.resolveChunkMode(cfg, "matrix-js", opts.accountId); const chunks = getCore().channel.text.chunkMarkdownTextWithMode( convertedMessage, chunkLimit, diff --git a/extensions/matrix-js/src/matrix/send/client.ts b/extensions/matrix-js/src/matrix/send/client.ts index 7dd15dc1f1b..d68d35e4edd 100644 --- a/extensions/matrix-js/src/matrix/send/client.ts +++ b/extensions/matrix-js/src/matrix/send/client.ts @@ -19,8 +19,8 @@ export function ensureNodeRuntime() { export function resolveMediaMaxBytes(): number | undefined { const cfg = getCore().config.loadConfig() as CoreConfig; - if (typeof cfg.channels?.matrix?.mediaMaxMb === "number") { - return cfg.channels.matrix.mediaMaxMb * 1024 * 1024; + if (typeof cfg.channels?.["matrix-js"]?.mediaMaxMb === "number") { + return cfg.channels["matrix-js"].mediaMaxMb * 1024 * 1024; } return undefined; } @@ -28,12 +28,13 @@ export function resolveMediaMaxBytes(): number | undefined { export async function resolveMatrixClient(opts: { client?: MatrixClient; timeoutMs?: number; + accountId?: string | null; }): Promise<{ client: MatrixClient; stopOnDone: boolean }> { ensureNodeRuntime(); if (opts.client) { return { client: opts.client, stopOnDone: false }; } - const active = getActiveMatrixClient(); + const active = getActiveMatrixClient(opts.accountId); if (active) { return { client: active, stopOnDone: false }; } @@ -41,10 +42,11 @@ export async function resolveMatrixClient(opts: { if (shouldShareClient) { const client = await resolveSharedMatrixClient({ timeoutMs: opts.timeoutMs, + accountId: opts.accountId, }); return { client, stopOnDone: false }; } - const auth = await resolveMatrixAuth(); + const auth = await resolveMatrixAuth({ accountId: opts.accountId }); const client = await createMatrixClient({ homeserver: auth.homeserver, userId: auth.userId, @@ -53,6 +55,7 @@ export async function resolveMatrixClient(opts: { deviceId: auth.deviceId, encryption: auth.encryption, localTimeoutMs: opts.timeoutMs, + accountId: opts.accountId, }); if (auth.encryption && client.crypto) { try { diff --git a/extensions/matrix-js/src/onboarding.ts b/extensions/matrix-js/src/onboarding.ts index 117e3e14f09..e4f4121d841 100644 --- a/extensions/matrix-js/src/onboarding.ts +++ b/extensions/matrix-js/src/onboarding.ts @@ -14,19 +14,21 @@ import { ensureMatrixSdkInstalled, isMatrixSdkAvailable } from "./matrix/deps.js import { resolveMatrixTargets } from "./resolve-targets.js"; import type { CoreConfig } from "./types.js"; -const channel = "matrix" as const; +const channel = "matrix-js" as const; function setMatrixDmPolicy(cfg: CoreConfig, policy: DmPolicy) { const allowFrom = - policy === "open" ? addWildcardAllowFrom(cfg.channels?.matrix?.dm?.allowFrom) : undefined; + policy === "open" + ? addWildcardAllowFrom(cfg.channels?.["matrix-js"]?.dm?.allowFrom) + : undefined; return { ...cfg, channels: { ...cfg.channels, - matrix: { - ...cfg.channels?.matrix, + "matrix-js": { + ...cfg.channels?.["matrix-js"], dm: { - ...cfg.channels?.matrix?.dm, + ...cfg.channels?.["matrix-js"]?.dm, policy, ...(allowFrom ? { allowFrom } : {}), }, @@ -54,7 +56,7 @@ async function promptMatrixAllowFrom(params: { prompter: WizardPrompter; }): Promise { const { cfg, prompter } = params; - const existingAllowFrom = cfg.channels?.matrix?.dm?.allowFrom ?? []; + const existingAllowFrom = cfg.channels?.["matrix-js"]?.dm?.allowFrom ?? []; const account = resolveMatrixAccount({ cfg }); const canResolve = Boolean(account.configured); @@ -125,11 +127,11 @@ async function promptMatrixAllowFrom(params: { ...cfg, channels: { ...cfg.channels, - matrix: { - ...cfg.channels?.matrix, + "matrix-js": { + ...cfg.channels?.["matrix-js"], enabled: true, dm: { - ...cfg.channels?.matrix?.dm, + ...cfg.channels?.["matrix-js"]?.dm, policy: "allowlist", allowFrom: unique, }, @@ -144,8 +146,8 @@ function setMatrixGroupPolicy(cfg: CoreConfig, groupPolicy: "open" | "allowlist" ...cfg, channels: { ...cfg.channels, - matrix: { - ...cfg.channels?.matrix, + "matrix-js": { + ...cfg.channels?.["matrix-js"], enabled: true, groupPolicy, }, @@ -159,8 +161,8 @@ function setMatrixGroupRooms(cfg: CoreConfig, roomKeys: string[]) { ...cfg, channels: { ...cfg.channels, - matrix: { - ...cfg.channels?.matrix, + "matrix-js": { + ...cfg.channels?.["matrix-js"], enabled: true, groups, }, @@ -171,9 +173,9 @@ function setMatrixGroupRooms(cfg: CoreConfig, roomKeys: string[]) { const dmPolicy: ChannelOnboardingDmPolicy = { label: "Matrix", channel, - policyKey: "channels.matrix.dm.policy", - allowFromKey: "channels.matrix.dm.allowFrom", - getCurrent: (cfg) => (cfg as CoreConfig).channels?.matrix?.dm?.policy ?? "pairing", + policyKey: "channels.matrix-js.dm.policy", + allowFromKey: "channels.matrix-js.dm.allowFrom", + getCurrent: (cfg) => (cfg as CoreConfig).channels?.["matrix-js"]?.dm?.policy ?? "pairing", setPolicy: (cfg, policy) => setMatrixDmPolicy(cfg as CoreConfig, policy), promptAllowFrom: promptMatrixAllowFrom, }; @@ -203,7 +205,7 @@ export const matrixOnboardingAdapter: ChannelOnboardingAdapter = { initialValue: true, }), }); - const existing = next.channels?.matrix ?? {}; + const existing = next.channels?.["matrix-js"] ?? {}; const account = resolveMatrixAccount({ cfg: next }); if (!account.configured) { await noteMatrixAuthHelp(prompter); @@ -231,8 +233,8 @@ export const matrixOnboardingAdapter: ChannelOnboardingAdapter = { ...next, channels: { ...next.channels, - matrix: { - ...next.channels?.matrix, + "matrix-js": { + ...next.channels?.["matrix-js"], enabled: true, }, }, @@ -352,8 +354,8 @@ export const matrixOnboardingAdapter: ChannelOnboardingAdapter = { ...next, channels: { ...next.channels, - matrix: { - ...next.channels?.matrix, + "matrix-js": { + ...next.channels?.["matrix-js"], enabled: true, homeserver, userId: userId || undefined, @@ -370,11 +372,12 @@ export const matrixOnboardingAdapter: ChannelOnboardingAdapter = { next = await promptMatrixAllowFrom({ cfg: next, prompter }); } - const existingGroups = next.channels?.matrix?.groups ?? next.channels?.matrix?.rooms; + const existingGroups = + next.channels?.["matrix-js"]?.groups ?? next.channels?.["matrix-js"]?.rooms; const accessConfig = await promptChannelAccessConfig({ prompter, label: "Matrix rooms", - currentPolicy: next.channels?.matrix?.groupPolicy ?? "allowlist", + currentPolicy: next.channels?.["matrix-js"]?.groupPolicy ?? "allowlist", currentEntries: Object.keys(existingGroups ?? {}), placeholder: "!roomId:server, #alias:server, Project Room", updatePrompt: Boolean(existingGroups), @@ -446,7 +449,7 @@ export const matrixOnboardingAdapter: ChannelOnboardingAdapter = { ...(cfg as CoreConfig), channels: { ...(cfg as CoreConfig).channels, - matrix: { ...(cfg as CoreConfig).channels?.matrix, enabled: false }, + "matrix-js": { ...(cfg as CoreConfig).channels?.["matrix-js"], enabled: false }, }, }), }; diff --git a/extensions/matrix-js/src/outbound.ts b/extensions/matrix-js/src/outbound.ts index 5ad3afbaf03..f100126f325 100644 --- a/extensions/matrix-js/src/outbound.ts +++ b/extensions/matrix-js/src/outbound.ts @@ -17,7 +17,7 @@ export const matrixOutbound: ChannelOutboundAdapter = { accountId: accountId ?? undefined, }); return { - channel: "matrix", + channel: "matrix-js", messageId: result.messageId, roomId: result.roomId, }; @@ -33,7 +33,7 @@ export const matrixOutbound: ChannelOutboundAdapter = { accountId: accountId ?? undefined, }); return { - channel: "matrix", + channel: "matrix-js", messageId: result.messageId, roomId: result.roomId, }; @@ -46,7 +46,7 @@ export const matrixOutbound: ChannelOutboundAdapter = { accountId: accountId ?? undefined, }); return { - channel: "matrix", + channel: "matrix-js", messageId: result.eventId, roomId: result.roomId, pollId: result.eventId, diff --git a/extensions/matrix-js/src/types.ts b/extensions/matrix-js/src/types.ts index fc373d6acdb..8c2db558be8 100644 --- a/extensions/matrix-js/src/types.ts +++ b/extensions/matrix-js/src/types.ts @@ -102,7 +102,7 @@ export type MatrixConfig = { export type CoreConfig = { channels?: { - matrix?: MatrixConfig; + "matrix-js"?: MatrixConfig; defaults?: { groupPolicy?: "open" | "allowlist" | "disabled"; };