@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 +6 -0
- package/index.ts +5 -0
- package/package.json +1 -1
- package/src/channel.ts +6 -12
- package/src/config-schema.test.ts +26 -0
- package/src/config-schema.ts +2 -1
- package/src/matrix/accounts.ts +2 -1
- package/src/matrix/client/config.ts +55 -29
- package/src/matrix/client/create-client.ts +7 -5
- package/src/matrix/client/logging.ts +17 -7
- package/src/matrix/client/shared.ts +3 -1
- package/src/matrix/client-bootstrap.ts +2 -1
- package/src/matrix/deps.test.ts +74 -0
- package/src/matrix/deps.ts +66 -0
- package/src/matrix/monitor/allowlist.ts +4 -15
- package/src/matrix/monitor/auto-join.ts +2 -1
- package/src/matrix/sdk-runtime.ts +18 -0
- package/src/matrix/send-queue.ts +7 -23
- package/src/matrix/send.test.ts +4 -0
- package/src/onboarding.ts +36 -23
- package/src/secret-input.ts +19 -0
- package/src/types.ts +2 -2
package/CHANGELOG.md
CHANGED
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
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
|
|
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
|
|
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
|
-
|
|
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
|
+
});
|
package/src/config-schema.ts
CHANGED
|
@@ -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:
|
|
47
|
+
password: buildSecretInputSchema().optional(),
|
|
47
48
|
deviceName: z.string().optional(),
|
|
48
49
|
initialSyncLimit: z.number().optional(),
|
|
49
50
|
encryption: z.boolean().optional(),
|
package/src/matrix/accounts.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
9
|
-
return value
|
|
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 =
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const
|
|
59
|
-
|
|
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
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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 {
|
|
3
|
-
|
|
4
|
-
|
|
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 {
|
|
1
|
+
import { loadMatrixSdk } from "../sdk-runtime.js";
|
|
2
2
|
|
|
3
3
|
let matrixSdkLoggingConfigured = false;
|
|
4
|
-
|
|
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
|
|
26
|
-
debug: (module, ...messageOrObject) => matrixSdkBaseLogger
|
|
27
|
-
info: (module, ...messageOrObject) => matrixSdkBaseLogger
|
|
28
|
-
warn: (module, ...messageOrObject) => matrixSdkBaseLogger
|
|
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
|
|
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
|
+
});
|
package/src/matrix/deps.ts
CHANGED
|
@@ -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
|
|
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:
|
|
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
|
-
|
|
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
|
+
}
|
package/src/matrix/send-queue.ts
CHANGED
|
@@ -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
|
|
11
|
+
const roomQueues = new KeyedAsyncQueue();
|
|
10
12
|
|
|
11
|
-
export
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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> {
|
package/src/matrix/send.test.ts
CHANGED
|
@@ -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 ||
|
|
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 && !
|
|
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
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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
|
|
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
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
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?:
|
|
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). */
|