@openclaw/matrix 2026.3.1 → 2026.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 2026.3.2
4
+
5
+ ### Changes
6
+
7
+ - Version alignment with core OpenClaw release numbers.
8
+
3
9
  ## 2026.3.1
4
10
 
5
11
  ### Changes
package/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
2
2
  import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
3
3
  import { matrixPlugin } from "./src/channel.js";
4
+ import { ensureMatrixCryptoRuntime } from "./src/matrix/deps.js";
4
5
  import { setMatrixRuntime } from "./src/runtime.js";
5
6
 
6
7
  const plugin = {
@@ -10,6 +11,10 @@ const plugin = {
10
11
  configSchema: emptyPluginConfigSchema(),
11
12
  register(api: OpenClawPluginApi) {
12
13
  setMatrixRuntime(api.runtime);
14
+ void ensureMatrixCryptoRuntime({ log: api.logger.info }).catch((err) => {
15
+ const message = err instanceof Error ? err.message : String(err);
16
+ api.logger.warn?.(`matrix: crypto runtime bootstrap failed: ${message}`);
17
+ });
13
18
  api.registerChannel({ plugin: matrixPlugin });
14
19
  },
15
20
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/matrix",
3
- "version": "2026.3.1",
3
+ "version": "2026.3.2",
4
4
  "description": "OpenClaw Matrix channel plugin",
5
5
  "type": "module",
6
6
  "dependencies": {
package/src/channel.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  applyAccountNameToChannelSection,
3
3
  buildChannelConfigSchema,
4
+ buildProbeChannelStatusSummary,
4
5
  DEFAULT_ACCOUNT_ID,
5
6
  deleteAccountFromConfigSection,
6
7
  formatPairingApproveHint,
@@ -32,6 +33,7 @@ import { sendMessageMatrix } from "./matrix/send.js";
32
33
  import { matrixOnboardingAdapter } from "./onboarding.js";
33
34
  import { matrixOutbound } from "./outbound.js";
34
35
  import { resolveMatrixTargets } from "./resolve-targets.js";
36
+ import { normalizeSecretInputString } from "./secret-input.js";
35
37
  import type { CoreConfig } from "./types.js";
36
38
 
37
39
  // Mutex for serializing account startup (workaround for concurrent dynamic import race condition)
@@ -325,7 +327,7 @@ export const matrixPlugin: ChannelPlugin<ResolvedMatrixAccount> = {
325
327
  return "Matrix requires --homeserver";
326
328
  }
327
329
  const accessToken = input.accessToken?.trim();
328
- const password = input.password?.trim();
330
+ const password = normalizeSecretInputString(input.password);
329
331
  const userId = input.userId?.trim();
330
332
  if (!accessToken && !password) {
331
333
  return "Matrix requires --access-token or --password";
@@ -363,7 +365,7 @@ export const matrixPlugin: ChannelPlugin<ResolvedMatrixAccount> = {
363
365
  homeserver: input.homeserver?.trim(),
364
366
  userId: input.userId?.trim(),
365
367
  accessToken: input.accessToken?.trim(),
366
- password: input.password?.trim(),
368
+ password: normalizeSecretInputString(input.password),
367
369
  deviceName: input.deviceName?.trim(),
368
370
  initialSyncLimit: input.initialSyncLimit,
369
371
  });
@@ -393,16 +395,8 @@ export const matrixPlugin: ChannelPlugin<ResolvedMatrixAccount> = {
393
395
  },
394
396
  ];
395
397
  }),
396
- buildChannelSummary: ({ snapshot }) => ({
397
- configured: snapshot.configured ?? false,
398
- baseUrl: snapshot.baseUrl ?? null,
399
- running: snapshot.running ?? false,
400
- lastStartAt: snapshot.lastStartAt ?? null,
401
- lastStopAt: snapshot.lastStopAt ?? null,
402
- lastError: snapshot.lastError ?? null,
403
- probe: snapshot.probe,
404
- lastProbeAt: snapshot.lastProbeAt ?? null,
405
- }),
398
+ buildChannelSummary: ({ snapshot }) =>
399
+ buildProbeChannelStatusSummary(snapshot, { baseUrl: snapshot.baseUrl ?? null }),
406
400
  probeAccount: async ({ account, timeoutMs, cfg }) => {
407
401
  try {
408
402
  const auth = await resolveMatrixAuth({
@@ -0,0 +1,26 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { MatrixConfigSchema } from "./config-schema.js";
3
+
4
+ describe("MatrixConfigSchema SecretInput", () => {
5
+ it("accepts SecretRef password at top-level", () => {
6
+ const result = MatrixConfigSchema.safeParse({
7
+ homeserver: "https://matrix.example.org",
8
+ userId: "@bot:example.org",
9
+ password: { source: "env", provider: "default", id: "MATRIX_PASSWORD" },
10
+ });
11
+ expect(result.success).toBe(true);
12
+ });
13
+
14
+ it("accepts SecretRef password on account", () => {
15
+ const result = MatrixConfigSchema.safeParse({
16
+ accounts: {
17
+ work: {
18
+ homeserver: "https://matrix.example.org",
19
+ userId: "@bot:example.org",
20
+ password: { source: "env", provider: "default", id: "MATRIX_WORK_PASSWORD" },
21
+ },
22
+ },
23
+ });
24
+ expect(result.success).toBe(true);
25
+ });
26
+ });
@@ -1,5 +1,6 @@
1
1
  import { MarkdownConfigSchema, ToolPolicySchema } from "openclaw/plugin-sdk";
2
2
  import { z } from "zod";
3
+ import { buildSecretInputSchema } from "./secret-input.js";
3
4
 
4
5
  const allowFromEntry = z.union([z.string(), z.number()]);
5
6
 
@@ -43,7 +44,7 @@ export const MatrixConfigSchema = z.object({
43
44
  homeserver: z.string().optional(),
44
45
  userId: z.string().optional(),
45
46
  accessToken: z.string().optional(),
46
- password: z.string().optional(),
47
+ password: buildSecretInputSchema().optional(),
47
48
  deviceName: z.string().optional(),
48
49
  initialSyncLimit: z.number().optional(),
49
50
  encryption: z.boolean().optional(),
@@ -3,6 +3,7 @@ import {
3
3
  normalizeAccountId,
4
4
  normalizeOptionalAccountId,
5
5
  } from "openclaw/plugin-sdk/account-id";
6
+ import { hasConfiguredSecretInput } from "../secret-input.js";
6
7
  import type { CoreConfig, MatrixConfig } from "../types.js";
7
8
  import { resolveMatrixConfigForAccount } from "./client.js";
8
9
  import { credentialsMatchConfig, loadMatrixCredentials } from "./credentials.js";
@@ -106,7 +107,7 @@ export function resolveMatrixAccount(params: {
106
107
  const hasUserId = Boolean(resolved.userId);
107
108
  const hasAccessToken = Boolean(resolved.accessToken);
108
109
  const hasPassword = Boolean(resolved.password);
109
- const hasPasswordAuth = hasUserId && hasPassword;
110
+ const hasPasswordAuth = hasUserId && (hasPassword || hasConfiguredSecretInput(base.password));
110
111
  const stored = loadMatrixCredentials(process.env, accountId);
111
112
  const hasStored =
112
113
  stored && resolved.homeserver
@@ -1,12 +1,17 @@
1
- import { MatrixClient } from "@vector-im/matrix-bot-sdk";
1
+ import { fetchWithSsrFGuard } from "openclaw/plugin-sdk";
2
2
  import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
3
3
  import { getMatrixRuntime } from "../../runtime.js";
4
+ import {
5
+ normalizeResolvedSecretInputString,
6
+ normalizeSecretInputString,
7
+ } from "../../secret-input.js";
4
8
  import type { CoreConfig } from "../../types.js";
9
+ import { loadMatrixSdk } from "../sdk-runtime.js";
5
10
  import { ensureMatrixSdkLoggingConfigured } from "./logging.js";
6
11
  import type { MatrixAuth, MatrixResolvedConfig } from "./types.js";
7
12
 
8
- function clean(value?: string): string {
9
- return value?.trim() ?? "";
13
+ function clean(value: unknown, path: string): string {
14
+ return normalizeResolvedSecretInputString({ value, path }) ?? "";
10
15
  }
11
16
 
12
17
  /** Shallow-merge known nested config sub-objects so partial overrides inherit base values. */
@@ -52,11 +57,23 @@ export function resolveMatrixConfigForAccount(
52
57
  // nested object inheritance (dm, actions, groups) so partial overrides work.
53
58
  const matrix = accountConfig ? deepMergeConfig(matrixBase, accountConfig) : matrixBase;
54
59
 
55
- const homeserver = clean(matrix.homeserver) || clean(env.MATRIX_HOMESERVER);
56
- const userId = clean(matrix.userId) || clean(env.MATRIX_USER_ID);
57
- const accessToken = clean(matrix.accessToken) || clean(env.MATRIX_ACCESS_TOKEN) || undefined;
58
- const password = clean(matrix.password) || clean(env.MATRIX_PASSWORD) || undefined;
59
- const deviceName = clean(matrix.deviceName) || clean(env.MATRIX_DEVICE_NAME) || undefined;
60
+ const homeserver =
61
+ clean(matrix.homeserver, "channels.matrix.homeserver") ||
62
+ clean(env.MATRIX_HOMESERVER, "MATRIX_HOMESERVER");
63
+ const userId =
64
+ clean(matrix.userId, "channels.matrix.userId") || clean(env.MATRIX_USER_ID, "MATRIX_USER_ID");
65
+ const accessToken =
66
+ clean(matrix.accessToken, "channels.matrix.accessToken") ||
67
+ clean(env.MATRIX_ACCESS_TOKEN, "MATRIX_ACCESS_TOKEN") ||
68
+ undefined;
69
+ const password =
70
+ clean(matrix.password, "channels.matrix.password") ||
71
+ clean(env.MATRIX_PASSWORD, "MATRIX_PASSWORD") ||
72
+ undefined;
73
+ const deviceName =
74
+ clean(matrix.deviceName, "channels.matrix.deviceName") ||
75
+ clean(env.MATRIX_DEVICE_NAME, "MATRIX_DEVICE_NAME") ||
76
+ undefined;
60
77
  const initialSyncLimit =
61
78
  typeof matrix.initialSyncLimit === "number"
62
79
  ? Math.max(0, Math.floor(matrix.initialSyncLimit))
@@ -119,6 +136,7 @@ export async function resolveMatrixAuth(params?: {
119
136
  if (!userId) {
120
137
  // Fetch userId from access token via whoami
121
138
  ensureMatrixSdkLoggingConfigured();
139
+ const { MatrixClient } = loadMatrixSdk();
122
140
  const tempClient = new MatrixClient(resolved.homeserver, resolved.accessToken);
123
141
  const whoami = await tempClient.getUserId();
124
142
  userId = whoami;
@@ -167,28 +185,36 @@ export async function resolveMatrixAuth(params?: {
167
185
  );
168
186
  }
169
187
 
170
- // Login with password using HTTP API
171
- const loginResponse = await fetch(`${resolved.homeserver}/_matrix/client/v3/login`, {
172
- method: "POST",
173
- headers: { "Content-Type": "application/json" },
174
- body: JSON.stringify({
175
- type: "m.login.password",
176
- identifier: { type: "m.id.user", user: resolved.userId },
177
- password: resolved.password,
178
- initial_device_display_name: resolved.deviceName ?? "OpenClaw Gateway",
179
- }),
188
+ // Login with password using HTTP API.
189
+ const { response: loginResponse, release: releaseLoginResponse } = await fetchWithSsrFGuard({
190
+ url: `${resolved.homeserver}/_matrix/client/v3/login`,
191
+ init: {
192
+ method: "POST",
193
+ headers: { "Content-Type": "application/json" },
194
+ body: JSON.stringify({
195
+ type: "m.login.password",
196
+ identifier: { type: "m.id.user", user: resolved.userId },
197
+ password: resolved.password,
198
+ initial_device_display_name: resolved.deviceName ?? "OpenClaw Gateway",
199
+ }),
200
+ },
201
+ auditContext: "matrix.login",
180
202
  });
181
-
182
- if (!loginResponse.ok) {
183
- const errorText = await loginResponse.text();
184
- throw new Error(`Matrix login failed: ${errorText}`);
185
- }
186
-
187
- const login = (await loginResponse.json()) as {
188
- access_token?: string;
189
- user_id?: string;
190
- device_id?: string;
191
- };
203
+ const login = await (async () => {
204
+ try {
205
+ if (!loginResponse.ok) {
206
+ const errorText = await loginResponse.text();
207
+ throw new Error(`Matrix login failed: ${errorText}`);
208
+ }
209
+ return (await loginResponse.json()) as {
210
+ access_token?: string;
211
+ user_id?: string;
212
+ device_id?: string;
213
+ };
214
+ } finally {
215
+ await releaseLoginResponse();
216
+ }
217
+ })();
192
218
 
193
219
  const accessToken = login.access_token?.trim();
194
220
  if (!accessToken) {
@@ -1,11 +1,10 @@
1
1
  import fs from "node:fs";
2
- import type { IStorageProvider, ICryptoStorageProvider } from "@vector-im/matrix-bot-sdk";
3
- import {
4
- LogService,
2
+ import type {
3
+ IStorageProvider,
4
+ ICryptoStorageProvider,
5
5
  MatrixClient,
6
- SimpleFsStorageProvider,
7
- RustSdkCryptoStorageProvider,
8
6
  } from "@vector-im/matrix-bot-sdk";
7
+ import { loadMatrixSdk } from "../sdk-runtime.js";
9
8
  import { ensureMatrixSdkLoggingConfigured } from "./logging.js";
10
9
  import {
11
10
  maybeMigrateLegacyStorage,
@@ -14,6 +13,7 @@ import {
14
13
  } from "./storage.js";
15
14
 
16
15
  function sanitizeUserIdList(input: unknown, label: string): string[] {
16
+ const LogService = loadMatrixSdk().LogService;
17
17
  if (input == null) {
18
18
  return [];
19
19
  }
@@ -44,6 +44,8 @@ export async function createMatrixClient(params: {
44
44
  localTimeoutMs?: number;
45
45
  accountId?: string | null;
46
46
  }): Promise<MatrixClient> {
47
+ const { MatrixClient, SimpleFsStorageProvider, RustSdkCryptoStorageProvider, LogService } =
48
+ loadMatrixSdk();
47
49
  ensureMatrixSdkLoggingConfigured();
48
50
  const env = process.env;
49
51
 
@@ -1,7 +1,15 @@
1
- import { ConsoleLogger, LogService } from "@vector-im/matrix-bot-sdk";
1
+ import { loadMatrixSdk } from "../sdk-runtime.js";
2
2
 
3
3
  let matrixSdkLoggingConfigured = false;
4
- const matrixSdkBaseLogger = new ConsoleLogger();
4
+ let matrixSdkBaseLogger:
5
+ | {
6
+ trace: (module: string, ...messageOrObject: unknown[]) => void;
7
+ debug: (module: string, ...messageOrObject: unknown[]) => void;
8
+ info: (module: string, ...messageOrObject: unknown[]) => void;
9
+ warn: (module: string, ...messageOrObject: unknown[]) => void;
10
+ error: (module: string, ...messageOrObject: unknown[]) => void;
11
+ }
12
+ | undefined;
5
13
 
6
14
  function shouldSuppressMatrixHttpNotFound(module: string, messageOrObject: unknown[]): boolean {
7
15
  if (module !== "MatrixHttpClient") {
@@ -19,18 +27,20 @@ export function ensureMatrixSdkLoggingConfigured(): void {
19
27
  if (matrixSdkLoggingConfigured) {
20
28
  return;
21
29
  }
30
+ const { ConsoleLogger, LogService } = loadMatrixSdk();
31
+ matrixSdkBaseLogger = new ConsoleLogger();
22
32
  matrixSdkLoggingConfigured = true;
23
33
 
24
34
  LogService.setLogger({
25
- trace: (module, ...messageOrObject) => matrixSdkBaseLogger.trace(module, ...messageOrObject),
26
- debug: (module, ...messageOrObject) => matrixSdkBaseLogger.debug(module, ...messageOrObject),
27
- info: (module, ...messageOrObject) => matrixSdkBaseLogger.info(module, ...messageOrObject),
28
- warn: (module, ...messageOrObject) => matrixSdkBaseLogger.warn(module, ...messageOrObject),
35
+ trace: (module, ...messageOrObject) => matrixSdkBaseLogger?.trace(module, ...messageOrObject),
36
+ debug: (module, ...messageOrObject) => matrixSdkBaseLogger?.debug(module, ...messageOrObject),
37
+ info: (module, ...messageOrObject) => matrixSdkBaseLogger?.info(module, ...messageOrObject),
38
+ warn: (module, ...messageOrObject) => matrixSdkBaseLogger?.warn(module, ...messageOrObject),
29
39
  error: (module, ...messageOrObject) => {
30
40
  if (shouldSuppressMatrixHttpNotFound(module, messageOrObject)) {
31
41
  return;
32
42
  }
33
- matrixSdkBaseLogger.error(module, ...messageOrObject);
43
+ matrixSdkBaseLogger?.error(module, ...messageOrObject);
34
44
  },
35
45
  });
36
46
  }
@@ -1,7 +1,7 @@
1
1
  import type { MatrixClient } from "@vector-im/matrix-bot-sdk";
2
- import { LogService } from "@vector-im/matrix-bot-sdk";
3
2
  import { normalizeAccountId } from "openclaw/plugin-sdk/account-id";
4
3
  import type { CoreConfig } from "../../types.js";
4
+ import { getMatrixLogService } from "../sdk-runtime.js";
5
5
  import { resolveMatrixAuth } from "./config.js";
6
6
  import { createMatrixClient } from "./create-client.js";
7
7
  import { startMatrixClientWithGrace } from "./startup.js";
@@ -81,6 +81,7 @@ async function ensureSharedClientStarted(params: {
81
81
  params.state.cryptoReady = true;
82
82
  }
83
83
  } catch (err) {
84
+ const LogService = getMatrixLogService();
84
85
  LogService.warn("MatrixClientLite", "Failed to prepare crypto:", err);
85
86
  }
86
87
  }
@@ -89,6 +90,7 @@ async function ensureSharedClientStarted(params: {
89
90
  client,
90
91
  onError: (err: unknown) => {
91
92
  params.state.started = false;
93
+ const LogService = getMatrixLogService();
92
94
  LogService.error("MatrixClientLite", "client.start() error:", err);
93
95
  },
94
96
  });
@@ -1,6 +1,6 @@
1
- import { LogService } from "@vector-im/matrix-bot-sdk";
2
1
  import { createMatrixClient } from "./client/create-client.js";
3
2
  import { startMatrixClientWithGrace } from "./client/startup.js";
3
+ import { getMatrixLogService } from "./sdk-runtime.js";
4
4
 
5
5
  type MatrixClientBootstrapAuth = {
6
6
  homeserver: string;
@@ -39,6 +39,7 @@ export async function createPreparedMatrixClient(opts: {
39
39
  await startMatrixClientWithGrace({
40
40
  client,
41
41
  onError: (err: unknown) => {
42
+ const LogService = getMatrixLogService();
42
43
  LogService.error("MatrixClientBootstrap", "client.start() error:", err);
43
44
  },
44
45
  });
@@ -0,0 +1,74 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { ensureMatrixCryptoRuntime } from "./deps.js";
3
+
4
+ const logStub = vi.fn();
5
+
6
+ describe("ensureMatrixCryptoRuntime", () => {
7
+ it("returns immediately when matrix SDK loads", async () => {
8
+ const runCommand = vi.fn();
9
+ const requireFn = vi.fn(() => ({}));
10
+
11
+ await ensureMatrixCryptoRuntime({
12
+ log: logStub,
13
+ requireFn,
14
+ runCommand,
15
+ resolveFn: () => "/tmp/download-lib.js",
16
+ nodeExecutable: "/usr/bin/node",
17
+ });
18
+
19
+ expect(requireFn).toHaveBeenCalledTimes(1);
20
+ expect(runCommand).not.toHaveBeenCalled();
21
+ });
22
+
23
+ it("bootstraps missing crypto runtime and retries matrix SDK load", async () => {
24
+ let bootstrapped = false;
25
+ const requireFn = vi.fn(() => {
26
+ if (!bootstrapped) {
27
+ throw new Error(
28
+ "Cannot find module '@matrix-org/matrix-sdk-crypto-nodejs-linux-x64-gnu' (required by matrix sdk)",
29
+ );
30
+ }
31
+ return {};
32
+ });
33
+ const runCommand = vi.fn(async () => {
34
+ bootstrapped = true;
35
+ return { code: 0, stdout: "", stderr: "" };
36
+ });
37
+
38
+ await ensureMatrixCryptoRuntime({
39
+ log: logStub,
40
+ requireFn,
41
+ runCommand,
42
+ resolveFn: () => "/tmp/download-lib.js",
43
+ nodeExecutable: "/usr/bin/node",
44
+ });
45
+
46
+ expect(runCommand).toHaveBeenCalledWith({
47
+ argv: ["/usr/bin/node", "/tmp/download-lib.js"],
48
+ cwd: "/tmp",
49
+ timeoutMs: 300_000,
50
+ env: { COREPACK_ENABLE_DOWNLOAD_PROMPT: "0" },
51
+ });
52
+ expect(requireFn).toHaveBeenCalledTimes(2);
53
+ });
54
+
55
+ it("rethrows non-crypto module errors without bootstrapping", async () => {
56
+ const runCommand = vi.fn();
57
+ const requireFn = vi.fn(() => {
58
+ throw new Error("Cannot find module '@vector-im/matrix-bot-sdk'");
59
+ });
60
+
61
+ await expect(
62
+ ensureMatrixCryptoRuntime({
63
+ log: logStub,
64
+ requireFn,
65
+ runCommand,
66
+ resolveFn: () => "/tmp/download-lib.js",
67
+ nodeExecutable: "/usr/bin/node",
68
+ }),
69
+ ).rejects.toThrow("Cannot find module '@vector-im/matrix-bot-sdk'");
70
+
71
+ expect(runCommand).not.toHaveBeenCalled();
72
+ expect(requireFn).toHaveBeenCalledTimes(1);
73
+ });
74
+ });
@@ -5,6 +5,27 @@ import { fileURLToPath } from "node:url";
5
5
  import { runPluginCommandWithTimeout, type RuntimeEnv } from "openclaw/plugin-sdk";
6
6
 
7
7
  const MATRIX_SDK_PACKAGE = "@vector-im/matrix-bot-sdk";
8
+ const MATRIX_CRYPTO_DOWNLOAD_HELPER = "@matrix-org/matrix-sdk-crypto-nodejs/download-lib.js";
9
+
10
+ function formatCommandError(result: { stderr: string; stdout: string }): string {
11
+ const stderr = result.stderr.trim();
12
+ if (stderr) {
13
+ return stderr;
14
+ }
15
+ const stdout = result.stdout.trim();
16
+ if (stdout) {
17
+ return stdout;
18
+ }
19
+ return "unknown error";
20
+ }
21
+
22
+ function isMissingMatrixCryptoRuntimeError(err: unknown): boolean {
23
+ const message = err instanceof Error ? err.message : String(err ?? "");
24
+ return (
25
+ message.includes("Cannot find module") &&
26
+ message.includes("@matrix-org/matrix-sdk-crypto-nodejs-")
27
+ );
28
+ }
8
29
 
9
30
  export function isMatrixSdkAvailable(): boolean {
10
31
  try {
@@ -21,6 +42,51 @@ function resolvePluginRoot(): string {
21
42
  return path.resolve(currentDir, "..", "..");
22
43
  }
23
44
 
45
+ export async function ensureMatrixCryptoRuntime(
46
+ params: {
47
+ log?: (message: string) => void;
48
+ requireFn?: (id: string) => unknown;
49
+ resolveFn?: (id: string) => string;
50
+ runCommand?: typeof runPluginCommandWithTimeout;
51
+ nodeExecutable?: string;
52
+ } = {},
53
+ ): Promise<void> {
54
+ const req = createRequire(import.meta.url);
55
+ const requireFn = params.requireFn ?? ((id: string) => req(id));
56
+ const resolveFn = params.resolveFn ?? ((id: string) => req.resolve(id));
57
+ const runCommand = params.runCommand ?? runPluginCommandWithTimeout;
58
+ const nodeExecutable = params.nodeExecutable ?? process.execPath;
59
+
60
+ try {
61
+ requireFn(MATRIX_SDK_PACKAGE);
62
+ return;
63
+ } catch (err) {
64
+ if (!isMissingMatrixCryptoRuntimeError(err)) {
65
+ throw err;
66
+ }
67
+ }
68
+
69
+ const scriptPath = resolveFn(MATRIX_CRYPTO_DOWNLOAD_HELPER);
70
+ params.log?.("matrix: crypto runtime missing; downloading platform library…");
71
+ const result = await runCommand({
72
+ argv: [nodeExecutable, scriptPath],
73
+ cwd: path.dirname(scriptPath),
74
+ timeoutMs: 300_000,
75
+ env: { COREPACK_ENABLE_DOWNLOAD_PROMPT: "0" },
76
+ });
77
+ if (result.code !== 0) {
78
+ throw new Error(`Matrix crypto runtime bootstrap failed: ${formatCommandError(result)}`);
79
+ }
80
+
81
+ try {
82
+ requireFn(MATRIX_SDK_PACKAGE);
83
+ } catch (err) {
84
+ throw new Error(
85
+ `Matrix crypto runtime remains unavailable after bootstrap: ${err instanceof Error ? err.message : String(err)}`,
86
+ );
87
+ }
88
+ }
89
+
24
90
  export async function ensureMatrixSdkInstalled(params: {
25
91
  runtime: RuntimeEnv;
26
92
  confirm?: (message: string) => Promise<boolean>;
@@ -1,4 +1,4 @@
1
- import type { AllowlistMatch } from "openclaw/plugin-sdk";
1
+ import { resolveAllowlistMatchByCandidates, type AllowlistMatch } from "openclaw/plugin-sdk";
2
2
 
3
3
  function normalizeAllowList(list?: Array<string | number>) {
4
4
  return (list ?? []).map((entry) => String(entry).trim()).filter(Boolean);
@@ -65,6 +65,7 @@ export function normalizeMatrixAllowList(list?: Array<string | number>) {
65
65
  export type MatrixAllowListMatch = AllowlistMatch<
66
66
  "wildcard" | "id" | "prefixed-id" | "prefixed-user"
67
67
  >;
68
+ type MatrixAllowListSource = Exclude<MatrixAllowListMatch["matchSource"], undefined>;
68
69
 
69
70
  export function resolveMatrixAllowListMatch(params: {
70
71
  allowList: string[];
@@ -78,24 +79,12 @@ export function resolveMatrixAllowListMatch(params: {
78
79
  return { allowed: true, matchKey: "*", matchSource: "wildcard" };
79
80
  }
80
81
  const userId = normalizeMatrixUser(params.userId);
81
- const candidates: Array<{ value?: string; source: MatrixAllowListMatch["matchSource"] }> = [
82
+ const candidates: Array<{ value?: string; source: MatrixAllowListSource }> = [
82
83
  { value: userId, source: "id" },
83
84
  { value: userId ? `matrix:${userId}` : "", source: "prefixed-id" },
84
85
  { value: userId ? `user:${userId}` : "", source: "prefixed-user" },
85
86
  ];
86
- for (const candidate of candidates) {
87
- if (!candidate.value) {
88
- continue;
89
- }
90
- if (allowList.includes(candidate.value)) {
91
- return {
92
- allowed: true,
93
- matchKey: candidate.value,
94
- matchSource: candidate.source,
95
- };
96
- }
97
- }
98
- return { allowed: false };
87
+ return resolveAllowlistMatchByCandidates({ allowList, candidates });
99
88
  }
100
89
 
101
90
  export function resolveMatrixAllowListMatches(params: { allowList: string[]; userId?: string }) {
@@ -1,8 +1,8 @@
1
1
  import type { MatrixClient } from "@vector-im/matrix-bot-sdk";
2
- import { AutojoinRoomsMixin } from "@vector-im/matrix-bot-sdk";
3
2
  import type { RuntimeEnv } from "openclaw/plugin-sdk";
4
3
  import { getMatrixRuntime } from "../../runtime.js";
5
4
  import type { CoreConfig } from "../../types.js";
5
+ import { loadMatrixSdk } from "../sdk-runtime.js";
6
6
 
7
7
  export function registerMatrixAutoJoin(params: {
8
8
  client: MatrixClient;
@@ -26,6 +26,7 @@ export function registerMatrixAutoJoin(params: {
26
26
 
27
27
  if (autoJoin === "always") {
28
28
  // Use the built-in autojoin mixin for "always" mode
29
+ const { AutojoinRoomsMixin } = loadMatrixSdk();
29
30
  AutojoinRoomsMixin.setupOnClient(client);
30
31
  logVerbose("matrix: auto-join enabled for all invites");
31
32
  return;
@@ -0,0 +1,18 @@
1
+ import { createRequire } from "node:module";
2
+
3
+ type MatrixSdkRuntime = typeof import("@vector-im/matrix-bot-sdk");
4
+
5
+ let cachedMatrixSdkRuntime: MatrixSdkRuntime | null = null;
6
+
7
+ export function loadMatrixSdk(): MatrixSdkRuntime {
8
+ if (cachedMatrixSdkRuntime) {
9
+ return cachedMatrixSdkRuntime;
10
+ }
11
+ const req = createRequire(import.meta.url);
12
+ cachedMatrixSdkRuntime = req("@vector-im/matrix-bot-sdk") as MatrixSdkRuntime;
13
+ return cachedMatrixSdkRuntime;
14
+ }
15
+
16
+ export function getMatrixLogService() {
17
+ return loadMatrixSdk().LogService;
18
+ }
@@ -1,3 +1,5 @@
1
+ import { KeyedAsyncQueue } from "openclaw/plugin-sdk/keyed-async-queue";
2
+
1
3
  export const DEFAULT_SEND_GAP_MS = 150;
2
4
 
3
5
  type MatrixSendQueueOptions = {
@@ -6,37 +8,19 @@ type MatrixSendQueueOptions = {
6
8
  };
7
9
 
8
10
  // Serialize sends per room to preserve Matrix delivery order.
9
- const roomQueues = new Map<string, Promise<void>>();
11
+ const roomQueues = new KeyedAsyncQueue();
10
12
 
11
- export async function enqueueSend<T>(
13
+ export function enqueueSend<T>(
12
14
  roomId: string,
13
15
  fn: () => Promise<T>,
14
16
  options?: MatrixSendQueueOptions,
15
17
  ): Promise<T> {
16
18
  const gapMs = options?.gapMs ?? DEFAULT_SEND_GAP_MS;
17
19
  const delayFn = options?.delayFn ?? delay;
18
- const previous = roomQueues.get(roomId) ?? Promise.resolve();
19
-
20
- const next = previous
21
- .catch(() => {})
22
- .then(async () => {
23
- await delayFn(gapMs);
24
- return await fn();
25
- });
26
-
27
- const queueMarker = next.then(
28
- () => {},
29
- () => {},
30
- );
31
- roomQueues.set(roomId, queueMarker);
32
-
33
- queueMarker.finally(() => {
34
- if (roomQueues.get(roomId) === queueMarker) {
35
- roomQueues.delete(roomId);
36
- }
20
+ return roomQueues.enqueue(roomId, async () => {
21
+ await delayFn(gapMs);
22
+ return await fn();
37
23
  });
38
-
39
- return await next;
40
24
  }
41
25
 
42
26
  function delay(ms: number): Promise<void> {
@@ -24,6 +24,10 @@ vi.mock("@vector-im/matrix-bot-sdk", () => ({
24
24
  RustSdkCryptoStorageProvider: vi.fn(),
25
25
  }));
26
26
 
27
+ vi.mock("./send-queue.js", () => ({
28
+ enqueueSend: async <T>(_roomId: string, fn: () => Promise<T>) => await fn(),
29
+ }));
30
+
27
31
  const loadWebMediaMock = vi.fn().mockResolvedValue({
28
32
  buffer: Buffer.from("media"),
29
33
  fileName: "photo.png",
package/src/onboarding.ts CHANGED
@@ -1,9 +1,13 @@
1
1
  import type { DmPolicy } from "openclaw/plugin-sdk";
2
2
  import {
3
3
  addWildcardAllowFrom,
4
+ formatResolvedUnresolvedNote,
4
5
  formatDocsLink,
6
+ hasConfiguredSecretInput,
5
7
  mergeAllowFromEntries,
8
+ promptSingleChannelSecretInput,
6
9
  promptChannelAccessConfig,
10
+ type SecretInput,
7
11
  type ChannelOnboardingAdapter,
8
12
  type ChannelOnboardingDmPolicy,
9
13
  type WizardPrompter,
@@ -265,22 +269,24 @@ export const matrixOnboardingAdapter: ChannelOnboardingAdapter = {
265
269
  ).trim();
266
270
 
267
271
  let accessToken = existing.accessToken ?? "";
268
- let password = existing.password ?? "";
272
+ let password: SecretInput | undefined = existing.password;
269
273
  let userId = existing.userId ?? "";
274
+ const existingPasswordConfigured = hasConfiguredSecretInput(existing.password);
275
+ const passwordConfigured = () => hasConfiguredSecretInput(password);
270
276
 
271
- if (accessToken || password) {
277
+ if (accessToken || passwordConfigured()) {
272
278
  const keep = await prompter.confirm({
273
279
  message: "Matrix credentials already configured. Keep them?",
274
280
  initialValue: true,
275
281
  });
276
282
  if (!keep) {
277
283
  accessToken = "";
278
- password = "";
284
+ password = undefined;
279
285
  userId = "";
280
286
  }
281
287
  }
282
288
 
283
- if (!accessToken && !password) {
289
+ if (!accessToken && !passwordConfigured()) {
284
290
  // Ask auth method FIRST before asking for user ID
285
291
  const authMode = await prompter.select({
286
292
  message: "Matrix auth method",
@@ -321,12 +327,25 @@ export const matrixOnboardingAdapter: ChannelOnboardingAdapter = {
321
327
  },
322
328
  }),
323
329
  ).trim();
324
- password = String(
325
- await prompter.text({
326
- message: "Matrix password",
327
- validate: (value) => (value?.trim() ? undefined : "Required"),
328
- }),
329
- ).trim();
330
+ const passwordResult = await promptSingleChannelSecretInput({
331
+ cfg: next,
332
+ prompter,
333
+ providerHint: "matrix",
334
+ credentialLabel: "password",
335
+ accountConfigured: Boolean(existingPasswordConfigured),
336
+ canUseEnv: Boolean(envPassword?.trim()) && !existingPasswordConfigured,
337
+ hasConfigToken: existingPasswordConfigured,
338
+ envPrompt: "MATRIX_PASSWORD detected. Use env var?",
339
+ keepPrompt: "Matrix password already configured. Keep it?",
340
+ inputPrompt: "Matrix password",
341
+ preferredEnvVar: "MATRIX_PASSWORD",
342
+ });
343
+ if (passwordResult.action === "set") {
344
+ password = passwordResult.value;
345
+ }
346
+ if (passwordResult.action === "use-env") {
347
+ password = undefined;
348
+ }
330
349
  }
331
350
  }
332
351
 
@@ -353,7 +372,7 @@ export const matrixOnboardingAdapter: ChannelOnboardingAdapter = {
353
372
  homeserver,
354
373
  userId: userId || undefined,
355
374
  accessToken: accessToken || undefined,
356
- password: password || undefined,
375
+ password: password,
357
376
  deviceName: deviceName || undefined,
358
377
  encryption: enableEncryption || undefined,
359
378
  },
@@ -408,18 +427,12 @@ export const matrixOnboardingAdapter: ChannelOnboardingAdapter = {
408
427
  }
409
428
  }
410
429
  roomKeys = [...resolvedIds, ...unresolved.map((entry) => entry.trim()).filter(Boolean)];
411
- if (resolvedIds.length > 0 || unresolved.length > 0) {
412
- await prompter.note(
413
- [
414
- resolvedIds.length > 0 ? `Resolved: ${resolvedIds.join(", ")}` : undefined,
415
- unresolved.length > 0
416
- ? `Unresolved (kept as typed): ${unresolved.join(", ")}`
417
- : undefined,
418
- ]
419
- .filter(Boolean)
420
- .join("\n"),
421
- "Matrix rooms",
422
- );
430
+ const resolution = formatResolvedUnresolvedNote({
431
+ resolved: resolvedIds,
432
+ unresolved,
433
+ });
434
+ if (resolution) {
435
+ await prompter.note(resolution, "Matrix rooms");
423
436
  }
424
437
  } catch (err) {
425
438
  await prompter.note(
@@ -0,0 +1,19 @@
1
+ import {
2
+ hasConfiguredSecretInput,
3
+ normalizeResolvedSecretInputString,
4
+ normalizeSecretInputString,
5
+ } from "openclaw/plugin-sdk";
6
+ import { z } from "zod";
7
+
8
+ export { hasConfiguredSecretInput, normalizeResolvedSecretInputString, normalizeSecretInputString };
9
+
10
+ export function buildSecretInputSchema() {
11
+ return z.union([
12
+ z.string(),
13
+ z.object({
14
+ source: z.enum(["env", "file", "exec"]),
15
+ provider: z.string().min(1),
16
+ id: z.string().min(1),
17
+ }),
18
+ ]);
19
+ }
package/src/types.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { DmPolicy, GroupPolicy } from "openclaw/plugin-sdk";
1
+ import type { DmPolicy, GroupPolicy, SecretInput } from "openclaw/plugin-sdk";
2
2
  export type { DmPolicy, GroupPolicy };
3
3
 
4
4
  export type ReplyToMode = "off" | "first" | "all";
@@ -58,7 +58,7 @@ export type MatrixConfig = {
58
58
  /** Matrix access token. */
59
59
  accessToken?: string;
60
60
  /** Matrix password (used only to fetch access token). */
61
- password?: string;
61
+ password?: SecretInput;
62
62
  /** Optional device name when logging in via password. */
63
63
  deviceName?: string;
64
64
  /** Initial sync limit for startup (default: @vector-im/matrix-bot-sdk default). */