@openclaw/matrix 2026.2.25 → 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 +18 -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 +4 -1
- package/src/directory-live.test.ts +11 -0
- package/src/directory-live.ts +2 -1
- package/src/matrix/accounts.test.ts +50 -1
- package/src/matrix/accounts.ts +15 -2
- 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.test.ts +85 -0
- package/src/matrix/client/shared.ts +11 -2
- package/src/matrix/client/startup.test.ts +49 -0
- package/src/matrix/client/startup.ts +29 -0
- package/src/matrix/client-bootstrap.ts +10 -2
- package/src/matrix/deps.test.ts +74 -0
- package/src/matrix/deps.ts +66 -0
- package/src/matrix/monitor/access-policy.ts +127 -0
- package/src/matrix/monitor/allowlist.ts +4 -15
- package/src/matrix/monitor/auto-join.ts +2 -1
- package/src/matrix/monitor/direct.test.ts +65 -0
- package/src/matrix/monitor/direct.ts +20 -7
- package/src/matrix/monitor/events.test.ts +31 -0
- package/src/matrix/monitor/events.ts +20 -0
- package/src/matrix/monitor/handler.body-for-agent.test.ts +142 -0
- package/src/matrix/monitor/handler.ts +69 -63
- package/src/matrix/monitor/inbound-body.test.ts +73 -0
- package/src/matrix/monitor/inbound-body.ts +28 -0
- package/src/matrix/monitor/index.test.ts +18 -0
- package/src/matrix/monitor/index.ts +204 -147
- 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 +5 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2026.3.2
|
|
4
|
+
|
|
5
|
+
### Changes
|
|
6
|
+
|
|
7
|
+
- Version alignment with core OpenClaw release numbers.
|
|
8
|
+
|
|
9
|
+
## 2026.3.1
|
|
10
|
+
|
|
11
|
+
### Changes
|
|
12
|
+
|
|
13
|
+
- Version alignment with core OpenClaw release numbers.
|
|
14
|
+
|
|
15
|
+
## 2026.2.26
|
|
16
|
+
|
|
17
|
+
### Changes
|
|
18
|
+
|
|
19
|
+
- Version alignment with core OpenClaw release numbers.
|
|
20
|
+
|
|
3
21
|
## 2026.2.25
|
|
4
22
|
|
|
5
23
|
### 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
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
|
|
|
@@ -37,11 +38,13 @@ const matrixRoomSchema = z
|
|
|
37
38
|
export const MatrixConfigSchema = z.object({
|
|
38
39
|
name: z.string().optional(),
|
|
39
40
|
enabled: z.boolean().optional(),
|
|
41
|
+
defaultAccount: z.string().optional(),
|
|
42
|
+
accounts: z.record(z.string(), z.unknown()).optional(),
|
|
40
43
|
markdown: MarkdownConfigSchema,
|
|
41
44
|
homeserver: z.string().optional(),
|
|
42
45
|
userId: z.string().optional(),
|
|
43
46
|
accessToken: z.string().optional(),
|
|
44
|
-
password:
|
|
47
|
+
password: buildSecretInputSchema().optional(),
|
|
45
48
|
deviceName: z.string().optional(),
|
|
46
49
|
initialSyncLimit: z.number().optional(),
|
|
47
50
|
encryption: z.boolean().optional(),
|
|
@@ -71,4 +71,15 @@ describe("matrix directory live", () => {
|
|
|
71
71
|
expect(result).toEqual([]);
|
|
72
72
|
expect(resolveMatrixAuth).not.toHaveBeenCalled();
|
|
73
73
|
});
|
|
74
|
+
|
|
75
|
+
it("preserves original casing for room IDs without :server suffix", async () => {
|
|
76
|
+
const mixedCaseId = "!EonMPPbOuhntHEHgZ2dnBO-c_EglMaXlIh2kdo8cgiA";
|
|
77
|
+
const result = await listMatrixDirectoryGroupsLive({
|
|
78
|
+
cfg,
|
|
79
|
+
query: mixedCaseId,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
expect(result).toHaveLength(1);
|
|
83
|
+
expect(result[0].id).toBe(mixedCaseId);
|
|
84
|
+
});
|
|
74
85
|
});
|
package/src/directory-live.ts
CHANGED
|
@@ -174,7 +174,8 @@ export async function listMatrixDirectoryGroupsLive(
|
|
|
174
174
|
}
|
|
175
175
|
|
|
176
176
|
if (query.startsWith("!")) {
|
|
177
|
-
|
|
177
|
+
const originalId = params.query?.trim() ?? query;
|
|
178
|
+
return [createGroupDirectoryEntry({ id: originalId, name: originalId })];
|
|
178
179
|
}
|
|
179
180
|
|
|
180
181
|
const joined = await fetchMatrixJson<MatrixJoinedRoomsResponse>({
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
2
|
import type { CoreConfig } from "../types.js";
|
|
3
|
-
import { resolveMatrixAccount } from "./accounts.js";
|
|
3
|
+
import { resolveDefaultMatrixAccountId, resolveMatrixAccount } from "./accounts.js";
|
|
4
4
|
|
|
5
5
|
vi.mock("./credentials.js", () => ({
|
|
6
6
|
loadMatrixCredentials: () => null,
|
|
@@ -80,3 +80,52 @@ describe("resolveMatrixAccount", () => {
|
|
|
80
80
|
expect(account.configured).toBe(true);
|
|
81
81
|
});
|
|
82
82
|
});
|
|
83
|
+
|
|
84
|
+
describe("resolveDefaultMatrixAccountId", () => {
|
|
85
|
+
it("prefers channels.matrix.defaultAccount when it matches a configured account", () => {
|
|
86
|
+
const cfg: CoreConfig = {
|
|
87
|
+
channels: {
|
|
88
|
+
matrix: {
|
|
89
|
+
defaultAccount: "alerts",
|
|
90
|
+
accounts: {
|
|
91
|
+
default: { homeserver: "https://matrix.example.org", accessToken: "tok-default" },
|
|
92
|
+
alerts: { homeserver: "https://matrix.example.org", accessToken: "tok-alerts" },
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
expect(resolveDefaultMatrixAccountId(cfg)).toBe("alerts");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("normalizes channels.matrix.defaultAccount before lookup", () => {
|
|
102
|
+
const cfg: CoreConfig = {
|
|
103
|
+
channels: {
|
|
104
|
+
matrix: {
|
|
105
|
+
defaultAccount: "Team Alerts",
|
|
106
|
+
accounts: {
|
|
107
|
+
"team-alerts": { homeserver: "https://matrix.example.org", accessToken: "tok-alerts" },
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
expect(resolveDefaultMatrixAccountId(cfg)).toBe("team-alerts");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("falls back when channels.matrix.defaultAccount is not configured", () => {
|
|
117
|
+
const cfg: CoreConfig = {
|
|
118
|
+
channels: {
|
|
119
|
+
matrix: {
|
|
120
|
+
defaultAccount: "missing",
|
|
121
|
+
accounts: {
|
|
122
|
+
default: { homeserver: "https://matrix.example.org", accessToken: "tok-default" },
|
|
123
|
+
alerts: { homeserver: "https://matrix.example.org", accessToken: "tok-alerts" },
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
expect(resolveDefaultMatrixAccountId(cfg)).toBe("default");
|
|
130
|
+
});
|
|
131
|
+
});
|
package/src/matrix/accounts.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_ACCOUNT_ID,
|
|
3
|
+
normalizeAccountId,
|
|
4
|
+
normalizeOptionalAccountId,
|
|
5
|
+
} from "openclaw/plugin-sdk/account-id";
|
|
6
|
+
import { hasConfiguredSecretInput } from "../secret-input.js";
|
|
2
7
|
import type { CoreConfig, MatrixConfig } from "../types.js";
|
|
3
8
|
import { resolveMatrixConfigForAccount } from "./client.js";
|
|
4
9
|
import { credentialsMatchConfig, loadMatrixCredentials } from "./credentials.js";
|
|
@@ -16,6 +21,7 @@ function mergeAccountConfig(base: MatrixConfig, account: MatrixConfig): MatrixCo
|
|
|
16
21
|
}
|
|
17
22
|
// Don't propagate the accounts map into the merged per-account config
|
|
18
23
|
delete (merged as Record<string, unknown>).accounts;
|
|
24
|
+
delete (merged as Record<string, unknown>).defaultAccount;
|
|
19
25
|
return merged;
|
|
20
26
|
}
|
|
21
27
|
|
|
@@ -54,6 +60,13 @@ export function listMatrixAccountIds(cfg: CoreConfig): string[] {
|
|
|
54
60
|
}
|
|
55
61
|
|
|
56
62
|
export function resolveDefaultMatrixAccountId(cfg: CoreConfig): string {
|
|
63
|
+
const preferred = normalizeOptionalAccountId(cfg.channels?.matrix?.defaultAccount);
|
|
64
|
+
if (
|
|
65
|
+
preferred &&
|
|
66
|
+
listMatrixAccountIds(cfg).some((accountId) => normalizeAccountId(accountId) === preferred)
|
|
67
|
+
) {
|
|
68
|
+
return preferred;
|
|
69
|
+
}
|
|
57
70
|
const ids = listMatrixAccountIds(cfg);
|
|
58
71
|
if (ids.includes(DEFAULT_ACCOUNT_ID)) {
|
|
59
72
|
return DEFAULT_ACCOUNT_ID;
|
|
@@ -94,7 +107,7 @@ export function resolveMatrixAccount(params: {
|
|
|
94
107
|
const hasUserId = Boolean(resolved.userId);
|
|
95
108
|
const hasAccessToken = Boolean(resolved.accessToken);
|
|
96
109
|
const hasPassword = Boolean(resolved.password);
|
|
97
|
-
const hasPasswordAuth = hasUserId && hasPassword;
|
|
110
|
+
const hasPasswordAuth = hasUserId && (hasPassword || hasConfiguredSecretInput(base.password));
|
|
98
111
|
const stored = loadMatrixCredentials(process.env, accountId);
|
|
99
112
|
const hasStored =
|
|
100
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
|
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { MatrixClient } from "@vector-im/matrix-bot-sdk";
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { resolveSharedMatrixClient, stopSharedClient } from "./shared.js";
|
|
4
|
+
import type { MatrixAuth } from "./types.js";
|
|
5
|
+
|
|
6
|
+
const createMatrixClientMock = vi.hoisted(() => vi.fn());
|
|
7
|
+
|
|
8
|
+
vi.mock("./create-client.js", () => ({
|
|
9
|
+
createMatrixClient: (...args: unknown[]) => createMatrixClientMock(...args),
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
function makeAuth(suffix: string): MatrixAuth {
|
|
13
|
+
return {
|
|
14
|
+
homeserver: "https://matrix.example.org",
|
|
15
|
+
userId: `@bot-${suffix}:example.org`,
|
|
16
|
+
accessToken: `token-${suffix}`,
|
|
17
|
+
encryption: false,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function createMockClient(startImpl: () => Promise<void>): MatrixClient {
|
|
22
|
+
return {
|
|
23
|
+
start: vi.fn(startImpl),
|
|
24
|
+
stop: vi.fn(),
|
|
25
|
+
getJoinedRooms: vi.fn().mockResolvedValue([]),
|
|
26
|
+
crypto: undefined,
|
|
27
|
+
} as unknown as MatrixClient;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe("resolveSharedMatrixClient startup behavior", () => {
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
stopSharedClient();
|
|
33
|
+
createMatrixClientMock.mockReset();
|
|
34
|
+
vi.useRealTimers();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("propagates the original start error during initialization", async () => {
|
|
38
|
+
vi.useFakeTimers();
|
|
39
|
+
const startError = new Error("bad token");
|
|
40
|
+
const client = createMockClient(
|
|
41
|
+
() =>
|
|
42
|
+
new Promise<void>((_resolve, reject) => {
|
|
43
|
+
setTimeout(() => reject(startError), 1);
|
|
44
|
+
}),
|
|
45
|
+
);
|
|
46
|
+
createMatrixClientMock.mockResolvedValue(client);
|
|
47
|
+
|
|
48
|
+
const startPromise = resolveSharedMatrixClient({
|
|
49
|
+
auth: makeAuth("start-error"),
|
|
50
|
+
});
|
|
51
|
+
const startExpectation = expect(startPromise).rejects.toBe(startError);
|
|
52
|
+
|
|
53
|
+
await vi.advanceTimersByTimeAsync(2001);
|
|
54
|
+
await startExpectation;
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("retries start after a late start-loop failure", async () => {
|
|
58
|
+
vi.useFakeTimers();
|
|
59
|
+
let rejectFirstStart: ((err: unknown) => void) | undefined;
|
|
60
|
+
const firstStart = new Promise<void>((_resolve, reject) => {
|
|
61
|
+
rejectFirstStart = reject;
|
|
62
|
+
});
|
|
63
|
+
const secondStart = new Promise<void>(() => {});
|
|
64
|
+
const startMock = vi.fn().mockReturnValueOnce(firstStart).mockReturnValueOnce(secondStart);
|
|
65
|
+
const client = createMockClient(startMock);
|
|
66
|
+
createMatrixClientMock.mockResolvedValue(client);
|
|
67
|
+
|
|
68
|
+
const firstResolve = resolveSharedMatrixClient({
|
|
69
|
+
auth: makeAuth("late-failure"),
|
|
70
|
+
});
|
|
71
|
+
await vi.advanceTimersByTimeAsync(2000);
|
|
72
|
+
await expect(firstResolve).resolves.toBe(client);
|
|
73
|
+
expect(startMock).toHaveBeenCalledTimes(1);
|
|
74
|
+
|
|
75
|
+
rejectFirstStart?.(new Error("late failure"));
|
|
76
|
+
await Promise.resolve();
|
|
77
|
+
|
|
78
|
+
const secondResolve = resolveSharedMatrixClient({
|
|
79
|
+
auth: makeAuth("late-failure"),
|
|
80
|
+
});
|
|
81
|
+
await vi.advanceTimersByTimeAsync(2000);
|
|
82
|
+
await expect(secondResolve).resolves.toBe(client);
|
|
83
|
+
expect(startMock).toHaveBeenCalledTimes(2);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -1,9 +1,10 @@
|
|
|
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
|
+
import { startMatrixClientWithGrace } from "./startup.js";
|
|
7
8
|
import { DEFAULT_ACCOUNT_KEY } from "./storage.js";
|
|
8
9
|
import type { MatrixAuth } from "./types.js";
|
|
9
10
|
|
|
@@ -80,11 +81,19 @@ async function ensureSharedClientStarted(params: {
|
|
|
80
81
|
params.state.cryptoReady = true;
|
|
81
82
|
}
|
|
82
83
|
} catch (err) {
|
|
84
|
+
const LogService = getMatrixLogService();
|
|
83
85
|
LogService.warn("MatrixClientLite", "Failed to prepare crypto:", err);
|
|
84
86
|
}
|
|
85
87
|
}
|
|
86
88
|
|
|
87
|
-
await
|
|
89
|
+
await startMatrixClientWithGrace({
|
|
90
|
+
client,
|
|
91
|
+
onError: (err: unknown) => {
|
|
92
|
+
params.state.started = false;
|
|
93
|
+
const LogService = getMatrixLogService();
|
|
94
|
+
LogService.error("MatrixClientLite", "client.start() error:", err);
|
|
95
|
+
},
|
|
96
|
+
});
|
|
88
97
|
params.state.started = true;
|
|
89
98
|
})();
|
|
90
99
|
sharedClientStartPromises.set(key, startPromise);
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { MATRIX_CLIENT_STARTUP_GRACE_MS, startMatrixClientWithGrace } from "./startup.js";
|
|
3
|
+
|
|
4
|
+
describe("startMatrixClientWithGrace", () => {
|
|
5
|
+
it("resolves after grace when start loop keeps running", async () => {
|
|
6
|
+
vi.useFakeTimers();
|
|
7
|
+
const client = {
|
|
8
|
+
start: vi.fn().mockReturnValue(new Promise<void>(() => {})),
|
|
9
|
+
};
|
|
10
|
+
const startPromise = startMatrixClientWithGrace({ client });
|
|
11
|
+
await vi.advanceTimersByTimeAsync(MATRIX_CLIENT_STARTUP_GRACE_MS);
|
|
12
|
+
await expect(startPromise).resolves.toBeUndefined();
|
|
13
|
+
vi.useRealTimers();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("rejects when startup fails during grace", async () => {
|
|
17
|
+
vi.useFakeTimers();
|
|
18
|
+
const startError = new Error("invalid token");
|
|
19
|
+
const client = {
|
|
20
|
+
start: vi.fn().mockRejectedValue(startError),
|
|
21
|
+
};
|
|
22
|
+
const startPromise = startMatrixClientWithGrace({ client });
|
|
23
|
+
const startupExpectation = expect(startPromise).rejects.toBe(startError);
|
|
24
|
+
await vi.advanceTimersByTimeAsync(MATRIX_CLIENT_STARTUP_GRACE_MS);
|
|
25
|
+
await startupExpectation;
|
|
26
|
+
vi.useRealTimers();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("calls onError for late failures after startup returns", async () => {
|
|
30
|
+
vi.useFakeTimers();
|
|
31
|
+
const lateError = new Error("late disconnect");
|
|
32
|
+
let rejectStart: ((err: unknown) => void) | undefined;
|
|
33
|
+
const startLoop = new Promise<void>((_resolve, reject) => {
|
|
34
|
+
rejectStart = reject;
|
|
35
|
+
});
|
|
36
|
+
const onError = vi.fn();
|
|
37
|
+
const client = {
|
|
38
|
+
start: vi.fn().mockReturnValue(startLoop),
|
|
39
|
+
};
|
|
40
|
+
const startPromise = startMatrixClientWithGrace({ client, onError });
|
|
41
|
+
await vi.advanceTimersByTimeAsync(MATRIX_CLIENT_STARTUP_GRACE_MS);
|
|
42
|
+
await expect(startPromise).resolves.toBeUndefined();
|
|
43
|
+
|
|
44
|
+
rejectStart?.(lateError);
|
|
45
|
+
await Promise.resolve();
|
|
46
|
+
expect(onError).toHaveBeenCalledWith(lateError);
|
|
47
|
+
vi.useRealTimers();
|
|
48
|
+
});
|
|
49
|
+
});
|