@archipelagolab/lobi 1.0.0

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.
Files changed (315) hide show
  1. package/CHANGELOG.md +164 -0
  2. package/ENDOFFILE +0 -0
  3. package/EOF +0 -0
  4. package/LICENSE +21 -0
  5. package/SPEC-SUPPORT.md +116 -0
  6. package/YAMLEND +0 -0
  7. package/api.ts +18 -0
  8. package/archipelagolab-lobi-1.0.0.tgz +0 -0
  9. package/auth-presence.ts +56 -0
  10. package/channel-plugin-api.ts +3 -0
  11. package/cli-metadata.ts +11 -0
  12. package/contract-api.ts +17 -0
  13. package/docs/CHECKLIST.md +83 -0
  14. package/docs/FORK_SDK_GUIDE.md +279 -0
  15. package/helper-api.ts +3 -0
  16. package/index.test.ts +61 -0
  17. package/index.ts +65 -0
  18. package/openclaw.plugin.json +23 -0
  19. package/package.json +52 -0
  20. package/plugin-entry.handlers.runtime.ts +1 -0
  21. package/runtime-api.ts +54 -0
  22. package/runtime-heavy-api.ts +1 -0
  23. package/scripts/migrate-to-lobi.sh +72 -0
  24. package/secret-contract-api.ts +5 -0
  25. package/setup-entry.ts +13 -0
  26. package/src/account-selection.test.ts +124 -0
  27. package/src/account-selection.ts +226 -0
  28. package/src/actions.account-propagation.test.ts +251 -0
  29. package/src/actions.test.ts +251 -0
  30. package/src/actions.ts +336 -0
  31. package/src/approval-auth.test.ts +23 -0
  32. package/src/approval-auth.ts +25 -0
  33. package/src/approval-handler.runtime.test.ts +46 -0
  34. package/src/approval-handler.runtime.ts +400 -0
  35. package/src/approval-ids.ts +6 -0
  36. package/src/approval-native.test.ts +329 -0
  37. package/src/approval-native.ts +336 -0
  38. package/src/approval-reactions.test.ts +107 -0
  39. package/src/approval-reactions.ts +158 -0
  40. package/src/auth-precedence.ts +61 -0
  41. package/src/channel-account-paths.ts +92 -0
  42. package/src/channel.account-paths.test.ts +102 -0
  43. package/src/channel.directory.test.ts +601 -0
  44. package/src/channel.resolve.test.ts +38 -0
  45. package/src/channel.runtime.ts +16 -0
  46. package/src/channel.setup.test.ts +269 -0
  47. package/src/channel.ts +570 -0
  48. package/src/cli-metadata.ts +19 -0
  49. package/src/cli.test.ts +1015 -0
  50. package/src/cli.ts +1198 -0
  51. package/src/config-adapter.ts +41 -0
  52. package/src/config-schema.test.ts +90 -0
  53. package/src/config-schema.ts +114 -0
  54. package/src/directory-live.test.ts +200 -0
  55. package/src/directory-live.ts +238 -0
  56. package/src/doctor-contract.ts +287 -0
  57. package/src/doctor.test.ts +440 -0
  58. package/src/doctor.ts +262 -0
  59. package/src/env-vars.ts +92 -0
  60. package/src/exec-approval-resolver.test.ts +68 -0
  61. package/src/exec-approval-resolver.ts +23 -0
  62. package/src/exec-approvals.test.ts +483 -0
  63. package/src/exec-approvals.ts +290 -0
  64. package/src/group-mentions.ts +41 -0
  65. package/src/legacy-crypto-inspector-availability.test.ts +81 -0
  66. package/src/legacy-crypto-inspector-availability.ts +60 -0
  67. package/src/legacy-crypto.test.ts +234 -0
  68. package/src/legacy-crypto.ts +549 -0
  69. package/src/legacy-state.test.ts +86 -0
  70. package/src/legacy-state.ts +156 -0
  71. package/src/matrix/account-config.ts +150 -0
  72. package/src/matrix/accounts.readiness.test.ts +27 -0
  73. package/src/matrix/accounts.test.ts +757 -0
  74. package/src/matrix/accounts.ts +194 -0
  75. package/src/matrix/actions/client.test.ts +215 -0
  76. package/src/matrix/actions/client.ts +31 -0
  77. package/src/matrix/actions/devices.test.ts +114 -0
  78. package/src/matrix/actions/devices.ts +34 -0
  79. package/src/matrix/actions/limits.test.ts +15 -0
  80. package/src/matrix/actions/limits.ts +6 -0
  81. package/src/matrix/actions/messages.test.ts +289 -0
  82. package/src/matrix/actions/messages.ts +123 -0
  83. package/src/matrix/actions/pins.test.ts +74 -0
  84. package/src/matrix/actions/pins.ts +64 -0
  85. package/src/matrix/actions/polls.test.ts +71 -0
  86. package/src/matrix/actions/polls.ts +109 -0
  87. package/src/matrix/actions/profile.test.ts +109 -0
  88. package/src/matrix/actions/profile.ts +37 -0
  89. package/src/matrix/actions/reactions.test.ts +135 -0
  90. package/src/matrix/actions/reactions.ts +59 -0
  91. package/src/matrix/actions/room.test.ts +79 -0
  92. package/src/matrix/actions/room.ts +71 -0
  93. package/src/matrix/actions/summary.test.ts +87 -0
  94. package/src/matrix/actions/summary.ts +88 -0
  95. package/src/matrix/actions/types.ts +82 -0
  96. package/src/matrix/actions/verification.test.ts +105 -0
  97. package/src/matrix/actions/verification.ts +237 -0
  98. package/src/matrix/actions.ts +37 -0
  99. package/src/matrix/active-client.ts +26 -0
  100. package/src/matrix/async-lock.ts +18 -0
  101. package/src/matrix/backup-health.ts +115 -0
  102. package/src/matrix/client/config-runtime-api.ts +14 -0
  103. package/src/matrix/client/config-secret-input.runtime.ts +1 -0
  104. package/src/matrix/client/config.ts +982 -0
  105. package/src/matrix/client/create-client.test.ts +115 -0
  106. package/src/matrix/client/create-client.ts +101 -0
  107. package/src/matrix/client/env-auth.ts +6 -0
  108. package/src/matrix/client/file-sync-store.test.ts +265 -0
  109. package/src/matrix/client/file-sync-store.ts +289 -0
  110. package/src/matrix/client/logging.ts +123 -0
  111. package/src/matrix/client/migration-snapshot.runtime.ts +1 -0
  112. package/src/matrix/client/private-network-host.ts +56 -0
  113. package/src/matrix/client/runtime.ts +4 -0
  114. package/src/matrix/client/shared.test.ts +344 -0
  115. package/src/matrix/client/shared.ts +306 -0
  116. package/src/matrix/client/storage.test.ts +634 -0
  117. package/src/matrix/client/storage.ts +544 -0
  118. package/src/matrix/client/types.ts +50 -0
  119. package/src/matrix/client-bootstrap.test.ts +84 -0
  120. package/src/matrix/client-bootstrap.ts +164 -0
  121. package/src/matrix/client-resolver.test-helpers.ts +147 -0
  122. package/src/matrix/client.test.ts +1521 -0
  123. package/src/matrix/client.ts +23 -0
  124. package/src/matrix/config-paths.ts +31 -0
  125. package/src/matrix/config-update.test.ts +237 -0
  126. package/src/matrix/config-update.ts +291 -0
  127. package/src/matrix/credentials-read.ts +206 -0
  128. package/src/matrix/credentials-write.runtime.ts +26 -0
  129. package/src/matrix/credentials.test.ts +501 -0
  130. package/src/matrix/credentials.ts +95 -0
  131. package/src/matrix/deps.test.ts +74 -0
  132. package/src/matrix/deps.ts +225 -0
  133. package/src/matrix/device-health.test.ts +45 -0
  134. package/src/matrix/device-health.ts +31 -0
  135. package/src/matrix/direct-management.test.ts +350 -0
  136. package/src/matrix/direct-management.ts +347 -0
  137. package/src/matrix/direct-room.test.ts +61 -0
  138. package/src/matrix/direct-room.ts +128 -0
  139. package/src/matrix/draft-stream.test.ts +406 -0
  140. package/src/matrix/draft-stream.ts +216 -0
  141. package/src/matrix/encryption-guidance.ts +27 -0
  142. package/src/matrix/errors.ts +21 -0
  143. package/src/matrix/format.test.ts +340 -0
  144. package/src/matrix/format.ts +428 -0
  145. package/src/matrix/legacy-crypto-inspector.ts +95 -0
  146. package/src/matrix/media-errors.ts +20 -0
  147. package/src/matrix/media-text.ts +169 -0
  148. package/src/matrix/monitor/access-state.test.ts +45 -0
  149. package/src/matrix/monitor/access-state.ts +77 -0
  150. package/src/matrix/monitor/ack-config.test.ts +57 -0
  151. package/src/matrix/monitor/ack-config.ts +26 -0
  152. package/src/matrix/monitor/allowlist.test.ts +45 -0
  153. package/src/matrix/monitor/allowlist.ts +94 -0
  154. package/src/matrix/monitor/auto-join.test.ts +203 -0
  155. package/src/matrix/monitor/auto-join.ts +86 -0
  156. package/src/matrix/monitor/config.test.ts +197 -0
  157. package/src/matrix/monitor/config.ts +303 -0
  158. package/src/matrix/monitor/context-summary.ts +43 -0
  159. package/src/matrix/monitor/direct.test.ts +529 -0
  160. package/src/matrix/monitor/direct.ts +270 -0
  161. package/src/matrix/monitor/events.test.ts +1524 -0
  162. package/src/matrix/monitor/events.ts +213 -0
  163. package/src/matrix/monitor/handler.body-for-agent.test.ts +396 -0
  164. package/src/matrix/monitor/handler.group-history.test.ts +648 -0
  165. package/src/matrix/monitor/handler.media-failure.test.ts +267 -0
  166. package/src/matrix/monitor/handler.test-helpers.ts +308 -0
  167. package/src/matrix/monitor/handler.test.ts +2952 -0
  168. package/src/matrix/monitor/handler.thread-root-media.test.ts +82 -0
  169. package/src/matrix/monitor/handler.ts +1679 -0
  170. package/src/matrix/monitor/inbound-dedupe.test.ts +146 -0
  171. package/src/matrix/monitor/inbound-dedupe.ts +267 -0
  172. package/src/matrix/monitor/index.test.ts +920 -0
  173. package/src/matrix/monitor/index.ts +434 -0
  174. package/src/matrix/monitor/legacy-crypto-restore.test.ts +206 -0
  175. package/src/matrix/monitor/legacy-crypto-restore.ts +139 -0
  176. package/src/matrix/monitor/location.ts +100 -0
  177. package/src/matrix/monitor/media.test.ts +159 -0
  178. package/src/matrix/monitor/media.ts +119 -0
  179. package/src/matrix/monitor/mentions.test.ts +289 -0
  180. package/src/matrix/monitor/mentions.ts +177 -0
  181. package/src/matrix/monitor/reaction-events.test.ts +326 -0
  182. package/src/matrix/monitor/reaction-events.ts +187 -0
  183. package/src/matrix/monitor/recent-invite.test.ts +92 -0
  184. package/src/matrix/monitor/recent-invite.ts +30 -0
  185. package/src/matrix/monitor/replies.test.ts +265 -0
  186. package/src/matrix/monitor/replies.ts +136 -0
  187. package/src/matrix/monitor/reply-context.test.ts +276 -0
  188. package/src/matrix/monitor/reply-context.ts +92 -0
  189. package/src/matrix/monitor/room-history.test.ts +258 -0
  190. package/src/matrix/monitor/room-history.ts +301 -0
  191. package/src/matrix/monitor/room-info.test.ts +201 -0
  192. package/src/matrix/monitor/room-info.ts +126 -0
  193. package/src/matrix/monitor/rooms.test.ts +121 -0
  194. package/src/matrix/monitor/rooms.ts +52 -0
  195. package/src/matrix/monitor/route.test.ts +255 -0
  196. package/src/matrix/monitor/route.ts +178 -0
  197. package/src/matrix/monitor/runtime-api.ts +31 -0
  198. package/src/matrix/monitor/startup-verification.test.ts +294 -0
  199. package/src/matrix/monitor/startup-verification.ts +237 -0
  200. package/src/matrix/monitor/startup.test.ts +257 -0
  201. package/src/matrix/monitor/startup.ts +218 -0
  202. package/src/matrix/monitor/status.ts +111 -0
  203. package/src/matrix/monitor/sync-lifecycle.test.ts +224 -0
  204. package/src/matrix/monitor/sync-lifecycle.ts +91 -0
  205. package/src/matrix/monitor/task-runner.ts +38 -0
  206. package/src/matrix/monitor/thread-context.test.ts +149 -0
  207. package/src/matrix/monitor/thread-context.ts +108 -0
  208. package/src/matrix/monitor/threads.test.ts +68 -0
  209. package/src/matrix/monitor/threads.ts +85 -0
  210. package/src/matrix/monitor/types.ts +30 -0
  211. package/src/matrix/monitor/verification-events.ts +627 -0
  212. package/src/matrix/monitor/verification-utils.test.ts +47 -0
  213. package/src/matrix/monitor/verification-utils.ts +46 -0
  214. package/src/matrix/outbound-media-runtime.ts +1 -0
  215. package/src/matrix/poll-summary.ts +110 -0
  216. package/src/matrix/poll-types.test.ts +205 -0
  217. package/src/matrix/poll-types.ts +433 -0
  218. package/src/matrix/probe.runtime.ts +4 -0
  219. package/src/matrix/probe.test.ts +154 -0
  220. package/src/matrix/probe.ts +96 -0
  221. package/src/matrix/profile.test.ts +154 -0
  222. package/src/matrix/profile.ts +184 -0
  223. package/src/matrix/reaction-common.test.ts +96 -0
  224. package/src/matrix/reaction-common.ts +147 -0
  225. package/src/matrix/sdk/crypto-bootstrap.test.ts +505 -0
  226. package/src/matrix/sdk/crypto-bootstrap.ts +341 -0
  227. package/src/matrix/sdk/crypto-facade.test.ts +197 -0
  228. package/src/matrix/sdk/crypto-facade.ts +207 -0
  229. package/src/matrix/sdk/crypto-node.runtime.test.ts +27 -0
  230. package/src/matrix/sdk/crypto-node.runtime.ts +9 -0
  231. package/src/matrix/sdk/crypto-runtime.ts +11 -0
  232. package/src/matrix/sdk/decrypt-bridge.ts +356 -0
  233. package/src/matrix/sdk/event-helpers.test.ts +60 -0
  234. package/src/matrix/sdk/event-helpers.ts +71 -0
  235. package/src/matrix/sdk/http-client.test.ts +134 -0
  236. package/src/matrix/sdk/http-client.ts +87 -0
  237. package/src/matrix/sdk/idb-persistence-lock.ts +51 -0
  238. package/src/matrix/sdk/idb-persistence.lock-order.test.ts +108 -0
  239. package/src/matrix/sdk/idb-persistence.test-helpers.ts +88 -0
  240. package/src/matrix/sdk/idb-persistence.test.ts +149 -0
  241. package/src/matrix/sdk/idb-persistence.ts +283 -0
  242. package/src/matrix/sdk/logger.test.ts +25 -0
  243. package/src/matrix/sdk/logger.ts +108 -0
  244. package/src/matrix/sdk/read-response-with-limit.ts +19 -0
  245. package/src/matrix/sdk/recovery-key-store.test.ts +385 -0
  246. package/src/matrix/sdk/recovery-key-store.ts +430 -0
  247. package/src/matrix/sdk/transport.test.ts +161 -0
  248. package/src/matrix/sdk/transport.ts +344 -0
  249. package/src/matrix/sdk/types.ts +236 -0
  250. package/src/matrix/sdk/verification-manager.test.ts +509 -0
  251. package/src/matrix/sdk/verification-manager.ts +694 -0
  252. package/src/matrix/sdk/verification-status.ts +23 -0
  253. package/src/matrix/sdk.test.ts +2568 -0
  254. package/src/matrix/sdk.ts +1789 -0
  255. package/src/matrix/send/client.test.ts +174 -0
  256. package/src/matrix/send/client.ts +90 -0
  257. package/src/matrix/send/formatting.ts +189 -0
  258. package/src/matrix/send/media.ts +244 -0
  259. package/src/matrix/send/targets.test.ts +254 -0
  260. package/src/matrix/send/targets.ts +104 -0
  261. package/src/matrix/send/types.ts +134 -0
  262. package/src/matrix/send.test.ts +958 -0
  263. package/src/matrix/send.ts +609 -0
  264. package/src/matrix/session-store-metadata.ts +108 -0
  265. package/src/matrix/startup-abort.ts +44 -0
  266. package/src/matrix/sync-state.ts +27 -0
  267. package/src/matrix/target-ids.ts +102 -0
  268. package/src/matrix/thread-bindings-shared.ts +201 -0
  269. package/src/matrix/thread-bindings.test.ts +673 -0
  270. package/src/matrix/thread-bindings.ts +577 -0
  271. package/src/matrix-migration.runtime.ts +9 -0
  272. package/src/migration-config.test.ts +228 -0
  273. package/src/migration-config.ts +243 -0
  274. package/src/migration-snapshot-backup.ts +117 -0
  275. package/src/migration-snapshot.test.ts +184 -0
  276. package/src/migration-snapshot.ts +55 -0
  277. package/src/onboarding.resolve.test.ts +55 -0
  278. package/src/onboarding.test-harness.ts +158 -0
  279. package/src/onboarding.test.ts +665 -0
  280. package/src/onboarding.ts +773 -0
  281. package/src/outbound.test.ts +173 -0
  282. package/src/outbound.ts +78 -0
  283. package/src/plugin-entry.runtime.js +159 -0
  284. package/src/plugin-entry.runtime.test.ts +108 -0
  285. package/src/plugin-entry.runtime.ts +68 -0
  286. package/src/profile-update.ts +68 -0
  287. package/src/record-shared.ts +3 -0
  288. package/src/resolve-targets.test.ts +178 -0
  289. package/src/resolve-targets.ts +175 -0
  290. package/src/resolver.ts +21 -0
  291. package/src/runtime-api.ts +144 -0
  292. package/src/runtime.ts +7 -0
  293. package/src/secret-contract.ts +174 -0
  294. package/src/session-route.test.ts +315 -0
  295. package/src/session-route.ts +113 -0
  296. package/src/setup-bootstrap.ts +94 -0
  297. package/src/setup-config.ts +222 -0
  298. package/src/setup-contract.ts +89 -0
  299. package/src/setup-core.test.ts +326 -0
  300. package/src/setup-core.ts +50 -0
  301. package/src/setup-surface.ts +4 -0
  302. package/src/startup-maintenance.test.ts +227 -0
  303. package/src/startup-maintenance.ts +114 -0
  304. package/src/storage-paths.ts +92 -0
  305. package/src/test-helpers.ts +42 -0
  306. package/src/test-mocks.ts +55 -0
  307. package/src/test-runtime.ts +72 -0
  308. package/src/test-support/monitor-route-test-support.ts +8 -0
  309. package/src/tool-actions.runtime.ts +1 -0
  310. package/src/tool-actions.test.ts +422 -0
  311. package/src/tool-actions.ts +498 -0
  312. package/src/types.ts +230 -0
  313. package/test-api.ts +2 -0
  314. package/thread-bindings-runtime.ts +4 -0
  315. package/tsconfig.json +16 -0
@@ -0,0 +1,344 @@
1
+ import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import type { MatrixAuth } from "./types.js";
3
+
4
+ const resolveMatrixAuthMock = vi.hoisted(() => vi.fn());
5
+ const resolveMatrixAuthContextMock = vi.hoisted(() => vi.fn());
6
+ const createMatrixClientMock = vi.hoisted(() => vi.fn());
7
+
8
+ vi.mock("./config.js", () => ({
9
+ resolveMatrixAuth: resolveMatrixAuthMock,
10
+ resolveMatrixAuthContext: resolveMatrixAuthContextMock,
11
+ }));
12
+
13
+ vi.mock("./create-client.js", () => ({
14
+ createMatrixClient: createMatrixClientMock,
15
+ }));
16
+
17
+ let acquireSharedMatrixClient: typeof import("./shared.js").acquireSharedMatrixClient;
18
+ let releaseSharedClientInstance: typeof import("./shared.js").releaseSharedClientInstance;
19
+ let resolveSharedMatrixClient: typeof import("./shared.js").resolveSharedMatrixClient;
20
+ let stopSharedClient: typeof import("./shared.js").stopSharedClient;
21
+ let stopSharedClientForAccount: typeof import("./shared.js").stopSharedClientForAccount;
22
+ let stopSharedClientInstance: typeof import("./shared.js").stopSharedClientInstance;
23
+
24
+ function authFor(accountId: string): MatrixAuth {
25
+ return {
26
+ accountId,
27
+ homeserver: "https://matrix.example.org",
28
+ userId: `@${accountId}:example.org`,
29
+ accessToken: `token-${accountId}`,
30
+ password: "secret", // pragma: allowlist secret
31
+ deviceId: `${accountId.toUpperCase()}-DEVICE`,
32
+ deviceName: `${accountId} device`,
33
+ initialSyncLimit: undefined,
34
+ encryption: false,
35
+ };
36
+ }
37
+
38
+ function createMockClient(name: string) {
39
+ const client = {
40
+ name,
41
+ start: vi.fn(async () => undefined),
42
+ stop: vi.fn(() => undefined),
43
+ getJoinedRooms: vi.fn(async () => [] as string[]),
44
+ crypto: undefined,
45
+ };
46
+ return client;
47
+ }
48
+
49
+ function primeAccountClientMocks(params?: {
50
+ mainAuth?: MatrixAuth;
51
+ opsAuth?: MatrixAuth;
52
+ mainClient?: ReturnType<typeof createMockClient>;
53
+ opsClient?: ReturnType<typeof createMockClient>;
54
+ }) {
55
+ const mainAuth = params?.mainAuth ?? authFor("main");
56
+ const opsAuth = params?.opsAuth ?? authFor("ops");
57
+ const mainClient = params?.mainClient ?? createMockClient("main");
58
+ const opsClient = params?.opsClient ?? createMockClient("ops");
59
+
60
+ resolveMatrixAuthMock.mockImplementation(async ({ accountId }: { accountId?: string }) =>
61
+ accountId === "ops" ? opsAuth : mainAuth,
62
+ );
63
+ createMatrixClientMock.mockImplementation(async ({ accountId }: { accountId?: string }) => {
64
+ if (accountId === "ops") {
65
+ return opsClient;
66
+ }
67
+ return mainClient;
68
+ });
69
+
70
+ return { mainAuth, opsAuth, mainClient, opsClient };
71
+ }
72
+
73
+ describe("resolveSharedMatrixClient", () => {
74
+ beforeAll(async () => {
75
+ ({
76
+ acquireSharedMatrixClient,
77
+ releaseSharedClientInstance,
78
+ resolveSharedMatrixClient,
79
+ stopSharedClient,
80
+ stopSharedClientForAccount,
81
+ stopSharedClientInstance,
82
+ } = await import("./shared.js"));
83
+ });
84
+
85
+ beforeEach(() => {
86
+ resolveMatrixAuthMock.mockReset();
87
+ resolveMatrixAuthContextMock.mockReset();
88
+ createMatrixClientMock.mockReset();
89
+ resolveMatrixAuthContextMock.mockImplementation(
90
+ ({ accountId }: { accountId?: string | null } = {}) => ({
91
+ cfg: undefined,
92
+ env: undefined,
93
+ accountId: accountId ?? "default",
94
+ resolved: {},
95
+ }),
96
+ );
97
+ });
98
+
99
+ afterEach(() => {
100
+ stopSharedClient();
101
+ vi.clearAllMocks();
102
+ });
103
+
104
+ it("keeps account clients isolated when resolves are interleaved", async () => {
105
+ const { mainClient, opsClient } = primeAccountClientMocks();
106
+
107
+ const firstMain = await resolveSharedMatrixClient({ accountId: "main", startClient: false });
108
+ const firstPoe = await resolveSharedMatrixClient({ accountId: "ops", startClient: false });
109
+ const secondMain = await resolveSharedMatrixClient({ accountId: "main" });
110
+
111
+ expect(firstMain).toBe(mainClient);
112
+ expect(firstPoe).toBe(opsClient);
113
+ expect(secondMain).toBe(mainClient);
114
+ expect(createMatrixClientMock).toHaveBeenCalledTimes(2);
115
+ expect(mainClient.start).toHaveBeenCalledTimes(1);
116
+ expect(opsClient.start).toHaveBeenCalledTimes(0);
117
+ });
118
+
119
+ it("stops only the targeted account client", async () => {
120
+ const { mainAuth, mainClient, opsClient } = primeAccountClientMocks();
121
+
122
+ await resolveSharedMatrixClient({ accountId: "main", startClient: false });
123
+ await resolveSharedMatrixClient({ accountId: "ops", startClient: false });
124
+
125
+ stopSharedClientForAccount(mainAuth);
126
+
127
+ expect(mainClient.stop).toHaveBeenCalledTimes(1);
128
+ expect(opsClient.stop).toHaveBeenCalledTimes(0);
129
+
130
+ stopSharedClient();
131
+
132
+ expect(opsClient.stop).toHaveBeenCalledTimes(1);
133
+ });
134
+
135
+ it("drops stopped shared clients by instance so the next resolve recreates them", async () => {
136
+ const mainAuth = authFor("main");
137
+ const firstMainClient = createMockClient("main-first");
138
+ const secondMainClient = createMockClient("main-second");
139
+
140
+ resolveMatrixAuthMock.mockResolvedValue(mainAuth);
141
+ createMatrixClientMock
142
+ .mockResolvedValueOnce(firstMainClient)
143
+ .mockResolvedValueOnce(secondMainClient);
144
+
145
+ const first = await resolveSharedMatrixClient({ accountId: "main", startClient: false });
146
+ stopSharedClientInstance(first as unknown as import("../sdk.js").MatrixClient);
147
+ const second = await resolveSharedMatrixClient({ accountId: "main", startClient: false });
148
+
149
+ expect(first).toBe(firstMainClient);
150
+ expect(second).toBe(secondMainClient);
151
+ expect(firstMainClient.stop).toHaveBeenCalledTimes(1);
152
+ expect(createMatrixClientMock).toHaveBeenCalledTimes(2);
153
+ });
154
+
155
+ it("reuses the effective implicit account instead of keying it as default", async () => {
156
+ const poeAuth = authFor("ops");
157
+ const poeClient = createMockClient("ops");
158
+
159
+ resolveMatrixAuthContextMock.mockReturnValue({
160
+ cfg: undefined,
161
+ env: undefined,
162
+ accountId: "ops",
163
+ resolved: {},
164
+ });
165
+ resolveMatrixAuthMock.mockResolvedValue(poeAuth);
166
+ createMatrixClientMock.mockResolvedValue(poeClient);
167
+
168
+ const first = await resolveSharedMatrixClient({ startClient: false });
169
+ const second = await resolveSharedMatrixClient({ startClient: false });
170
+
171
+ expect(first).toBe(poeClient);
172
+ expect(second).toBe(poeClient);
173
+ expect(resolveMatrixAuthMock).toHaveBeenCalledWith({
174
+ cfg: undefined,
175
+ env: undefined,
176
+ accountId: "ops",
177
+ });
178
+ expect(createMatrixClientMock).toHaveBeenCalledTimes(1);
179
+ expect(createMatrixClientMock).toHaveBeenCalledWith(
180
+ expect.objectContaining({
181
+ accountId: "ops",
182
+ }),
183
+ );
184
+ });
185
+
186
+ it("honors startClient false even when the caller acquires a shared lease", async () => {
187
+ const mainAuth = authFor("main");
188
+ const mainClient = createMockClient("main");
189
+
190
+ resolveMatrixAuthMock.mockResolvedValue(mainAuth);
191
+ createMatrixClientMock.mockResolvedValue(mainClient);
192
+
193
+ const client = await acquireSharedMatrixClient({ accountId: "main", startClient: false });
194
+
195
+ expect(client).toBe(mainClient);
196
+ expect(mainClient.start).not.toHaveBeenCalled();
197
+ });
198
+
199
+ it("keeps shared clients alive until the last one-off lease releases", async () => {
200
+ const mainAuth = authFor("main");
201
+ const mainClient = {
202
+ ...createMockClient("main"),
203
+ stopAndPersist: vi.fn(async () => undefined),
204
+ };
205
+
206
+ resolveMatrixAuthMock.mockResolvedValue(mainAuth);
207
+ createMatrixClientMock.mockResolvedValue(mainClient);
208
+
209
+ const first = await acquireSharedMatrixClient({ accountId: "main", startClient: false });
210
+ const second = await acquireSharedMatrixClient({ accountId: "main", startClient: false });
211
+
212
+ expect(first).toBe(mainClient);
213
+ expect(second).toBe(mainClient);
214
+
215
+ expect(
216
+ await releaseSharedClientInstance(mainClient as unknown as import("../sdk.js").MatrixClient),
217
+ ).toBe(false);
218
+ expect(mainClient.stop).not.toHaveBeenCalled();
219
+
220
+ expect(
221
+ await releaseSharedClientInstance(mainClient as unknown as import("../sdk.js").MatrixClient),
222
+ ).toBe(true);
223
+ expect(mainClient.stop).toHaveBeenCalledTimes(1);
224
+ });
225
+
226
+ it("rejects mismatched explicit account ids when auth is already resolved", async () => {
227
+ await expect(
228
+ resolveSharedMatrixClient({
229
+ auth: authFor("ops"),
230
+ accountId: "main",
231
+ startClient: false,
232
+ }),
233
+ ).rejects.toThrow("Matrix shared client account mismatch");
234
+ });
235
+
236
+ it("lets a later waiter abort while shared startup continues for the owner", async () => {
237
+ const mainAuth = authFor("main");
238
+ let resolveStartup: (() => void) | undefined;
239
+ const mainClient = {
240
+ ...createMockClient("main"),
241
+ start: vi.fn(
242
+ async () =>
243
+ await new Promise<void>((resolve) => {
244
+ resolveStartup = resolve;
245
+ }),
246
+ ),
247
+ };
248
+
249
+ resolveMatrixAuthMock.mockResolvedValue(mainAuth);
250
+ createMatrixClientMock.mockResolvedValue(mainClient);
251
+
252
+ const ownerPromise = resolveSharedMatrixClient({ accountId: "main" });
253
+ await vi.waitFor(() => {
254
+ expect(mainClient.start).toHaveBeenCalledTimes(1);
255
+ expect(resolveStartup).toEqual(expect.any(Function));
256
+ });
257
+
258
+ const abortController = new AbortController();
259
+ const canceledWaiter = resolveSharedMatrixClient({
260
+ accountId: "main",
261
+ abortSignal: abortController.signal,
262
+ });
263
+ abortController.abort();
264
+
265
+ await expect(canceledWaiter).rejects.toMatchObject({
266
+ message: "Matrix startup aborted",
267
+ name: "AbortError",
268
+ });
269
+
270
+ resolveStartup?.();
271
+ await expect(ownerPromise).resolves.toBe(mainClient);
272
+ });
273
+
274
+ it("keeps the shared startup lock while an aborted waiter exits early", async () => {
275
+ const mainAuth = authFor("main");
276
+ let resolveStartup: (() => void) | undefined;
277
+ const mainClient = {
278
+ ...createMockClient("main"),
279
+ start: vi.fn(
280
+ async () =>
281
+ await new Promise<void>((resolve) => {
282
+ resolveStartup = resolve;
283
+ }),
284
+ ),
285
+ };
286
+
287
+ resolveMatrixAuthMock.mockResolvedValue(mainAuth);
288
+ createMatrixClientMock.mockResolvedValue(mainClient);
289
+
290
+ const ownerPromise = resolveSharedMatrixClient({ accountId: "main" });
291
+ await vi.waitFor(() => {
292
+ expect(mainClient.start).toHaveBeenCalledTimes(1);
293
+ expect(resolveStartup).toEqual(expect.any(Function));
294
+ });
295
+
296
+ const abortController = new AbortController();
297
+ const abortedWaiter = resolveSharedMatrixClient({
298
+ accountId: "main",
299
+ abortSignal: abortController.signal,
300
+ });
301
+ abortController.abort();
302
+ await expect(abortedWaiter).rejects.toMatchObject({
303
+ message: "Matrix startup aborted",
304
+ name: "AbortError",
305
+ });
306
+
307
+ const followerPromise = resolveSharedMatrixClient({ accountId: "main" });
308
+ expect(mainClient.start).toHaveBeenCalledTimes(1);
309
+
310
+ resolveStartup?.();
311
+ await expect(ownerPromise).resolves.toBe(mainClient);
312
+ await expect(followerPromise).resolves.toBe(mainClient);
313
+ expect(mainClient.start).toHaveBeenCalledTimes(1);
314
+ });
315
+
316
+ it("recreates the shared client when dispatcherPolicy changes", async () => {
317
+ const firstAuth = {
318
+ ...authFor("main"),
319
+ dispatcherPolicy: {
320
+ mode: "explicit-proxy" as const,
321
+ proxyUrl: "http://127.0.0.1:7890",
322
+ },
323
+ };
324
+ const secondAuth = {
325
+ ...authFor("main"),
326
+ dispatcherPolicy: {
327
+ mode: "explicit-proxy" as const,
328
+ proxyUrl: "http://127.0.0.1:7891",
329
+ },
330
+ };
331
+ const firstClient = createMockClient("main-first");
332
+ const secondClient = createMockClient("main-second");
333
+
334
+ resolveMatrixAuthMock.mockResolvedValueOnce(firstAuth).mockResolvedValueOnce(secondAuth);
335
+ createMatrixClientMock.mockResolvedValueOnce(firstClient).mockResolvedValueOnce(secondClient);
336
+
337
+ const first = await resolveSharedMatrixClient({ accountId: "main", startClient: false });
338
+ const second = await resolveSharedMatrixClient({ accountId: "main", startClient: false });
339
+
340
+ expect(first).toBe(firstClient);
341
+ expect(second).toBe(secondClient);
342
+ expect(createMatrixClientMock).toHaveBeenCalledTimes(2);
343
+ });
344
+ });
@@ -0,0 +1,306 @@
1
+ import { normalizeOptionalAccountId } from "openclaw/plugin-sdk/account-id";
2
+ import type { CoreConfig } from "../../types.js";
3
+ import type { MatrixClient } from "../sdk.js";
4
+ import { LogService } from "../sdk/logger.js";
5
+ import { awaitMatrixStartupWithAbort } from "../startup-abort.js";
6
+ import { resolveMatrixAuth, resolveMatrixAuthContext } from "./config.js";
7
+ import type { MatrixAuth } from "./types.js";
8
+
9
+ type MatrixCreateClientDeps = {
10
+ createMatrixClient: typeof import("./create-client.js").createMatrixClient;
11
+ };
12
+
13
+ let matrixCreateClientDepsPromise: Promise<MatrixCreateClientDeps> | undefined;
14
+
15
+ async function loadMatrixCreateClientDeps(): Promise<MatrixCreateClientDeps> {
16
+ matrixCreateClientDepsPromise ??= import("./create-client.js").then((runtime) => ({
17
+ createMatrixClient: runtime.createMatrixClient,
18
+ }));
19
+ return await matrixCreateClientDepsPromise;
20
+ }
21
+
22
+ type SharedMatrixClientState = {
23
+ client: MatrixClient;
24
+ key: string;
25
+ started: boolean;
26
+ cryptoReady: boolean;
27
+ startPromise: Promise<void> | null;
28
+ leases: number;
29
+ };
30
+
31
+ const sharedClientStates = new Map<string, SharedMatrixClientState>();
32
+ const sharedClientPromises = new Map<string, Promise<SharedMatrixClientState>>();
33
+
34
+ function serializeDispatcherPolicyKey(auth: MatrixAuth): string {
35
+ return JSON.stringify(auth.dispatcherPolicy ?? null);
36
+ }
37
+
38
+ function buildSharedClientKey(auth: MatrixAuth): string {
39
+ return [
40
+ auth.homeserver,
41
+ auth.userId,
42
+ auth.accessToken,
43
+ auth.encryption ? "e2ee" : "plain",
44
+ auth.allowPrivateNetwork ? "private-net" : "strict-net",
45
+ serializeDispatcherPolicyKey(auth),
46
+ auth.accountId,
47
+ ].join("|");
48
+ }
49
+
50
+ async function createSharedMatrixClient(params: {
51
+ auth: MatrixAuth;
52
+ timeoutMs?: number;
53
+ }): Promise<SharedMatrixClientState> {
54
+ const { createMatrixClient } = await loadMatrixCreateClientDeps();
55
+ const client = await createMatrixClient({
56
+ homeserver: params.auth.homeserver,
57
+ userId: params.auth.userId,
58
+ accessToken: params.auth.accessToken,
59
+ password: params.auth.password,
60
+ deviceId: params.auth.deviceId,
61
+ encryption: params.auth.encryption,
62
+ localTimeoutMs: params.timeoutMs,
63
+ initialSyncLimit: params.auth.initialSyncLimit,
64
+ accountId: params.auth.accountId,
65
+ allowPrivateNetwork: params.auth.allowPrivateNetwork,
66
+ ssrfPolicy: params.auth.ssrfPolicy,
67
+ dispatcherPolicy: params.auth.dispatcherPolicy,
68
+ });
69
+ return {
70
+ client,
71
+ key: buildSharedClientKey(params.auth),
72
+ started: false,
73
+ cryptoReady: false,
74
+ startPromise: null,
75
+ leases: 0,
76
+ };
77
+ }
78
+
79
+ function findSharedClientStateByInstance(client: MatrixClient): SharedMatrixClientState | null {
80
+ for (const state of sharedClientStates.values()) {
81
+ if (state.client === client) {
82
+ return state;
83
+ }
84
+ }
85
+ return null;
86
+ }
87
+
88
+ function deleteSharedClientState(state: SharedMatrixClientState): void {
89
+ sharedClientStates.delete(state.key);
90
+ sharedClientPromises.delete(state.key);
91
+ }
92
+
93
+ async function ensureSharedClientStarted(params: {
94
+ state: SharedMatrixClientState;
95
+ encryption?: boolean;
96
+ abortSignal?: AbortSignal;
97
+ }): Promise<void> {
98
+ const waitForStart = async (startPromise: Promise<void>) => {
99
+ await awaitMatrixStartupWithAbort(startPromise, params.abortSignal);
100
+ };
101
+
102
+ if (params.state.started) {
103
+ return;
104
+ }
105
+ if (params.state.startPromise) {
106
+ await waitForStart(params.state.startPromise);
107
+ return;
108
+ }
109
+
110
+ const startPromise = (async () => {
111
+ const client = params.state.client;
112
+
113
+ // Initialize crypto if enabled
114
+ if (params.encryption && !params.state.cryptoReady) {
115
+ try {
116
+ const joinedRooms = await client.getJoinedRooms();
117
+ if (client.crypto) {
118
+ await client.crypto.prepare(joinedRooms);
119
+ params.state.cryptoReady = true;
120
+ }
121
+ } catch (err) {
122
+ LogService.warn("MatrixClientLite", "Failed to prepare crypto:", err);
123
+ }
124
+ }
125
+
126
+ await client.start({ abortSignal: params.abortSignal });
127
+ params.state.started = true;
128
+ })();
129
+ // Keep the shared startup lock until the underlying start fully settles, even
130
+ // if one waiter aborts early while another caller still owns the startup.
131
+ const guardedStart = startPromise.finally(() => {
132
+ if (params.state.startPromise === guardedStart) {
133
+ params.state.startPromise = null;
134
+ }
135
+ });
136
+ params.state.startPromise = guardedStart;
137
+
138
+ await waitForStart(guardedStart);
139
+ }
140
+
141
+ async function resolveSharedMatrixClientState(
142
+ params: {
143
+ cfg?: CoreConfig;
144
+ env?: NodeJS.ProcessEnv;
145
+ timeoutMs?: number;
146
+ auth?: MatrixAuth;
147
+ startClient?: boolean;
148
+ accountId?: string | null;
149
+ abortSignal?: AbortSignal;
150
+ } = {},
151
+ ): Promise<SharedMatrixClientState> {
152
+ const requestedAccountId = normalizeOptionalAccountId(params.accountId);
153
+ if (params.auth && requestedAccountId && requestedAccountId !== params.auth.accountId) {
154
+ throw new Error(
155
+ `Matrix shared client account mismatch: requested ${requestedAccountId}, auth resolved ${params.auth.accountId}`,
156
+ );
157
+ }
158
+ const authContext = params.auth
159
+ ? null
160
+ : resolveMatrixAuthContext({
161
+ cfg: params.cfg,
162
+ env: params.env,
163
+ accountId: params.accountId,
164
+ });
165
+ const auth =
166
+ params.auth ??
167
+ (await resolveMatrixAuth({
168
+ cfg: authContext?.cfg ?? params.cfg,
169
+ env: authContext?.env ?? params.env,
170
+ accountId: authContext?.accountId,
171
+ }));
172
+ const key = buildSharedClientKey(auth);
173
+ const shouldStart = params.startClient !== false;
174
+
175
+ const existingState = sharedClientStates.get(key);
176
+ if (existingState) {
177
+ if (shouldStart) {
178
+ await ensureSharedClientStarted({
179
+ state: existingState,
180
+ encryption: auth.encryption,
181
+ abortSignal: params.abortSignal,
182
+ });
183
+ }
184
+ return existingState;
185
+ }
186
+
187
+ const existingPromise = sharedClientPromises.get(key);
188
+ if (existingPromise) {
189
+ const pending = await existingPromise;
190
+ if (shouldStart) {
191
+ await ensureSharedClientStarted({
192
+ state: pending,
193
+ encryption: auth.encryption,
194
+ abortSignal: params.abortSignal,
195
+ });
196
+ }
197
+ return pending;
198
+ }
199
+
200
+ const creationPromise = createSharedMatrixClient({
201
+ auth,
202
+ timeoutMs: params.timeoutMs,
203
+ });
204
+ sharedClientPromises.set(key, creationPromise);
205
+
206
+ try {
207
+ const created = await creationPromise;
208
+ sharedClientStates.set(key, created);
209
+ if (shouldStart) {
210
+ await ensureSharedClientStarted({
211
+ state: created,
212
+ encryption: auth.encryption,
213
+ abortSignal: params.abortSignal,
214
+ });
215
+ }
216
+ return created;
217
+ } finally {
218
+ sharedClientPromises.delete(key);
219
+ }
220
+ }
221
+
222
+ export async function resolveSharedMatrixClient(
223
+ params: {
224
+ cfg?: CoreConfig;
225
+ env?: NodeJS.ProcessEnv;
226
+ timeoutMs?: number;
227
+ auth?: MatrixAuth;
228
+ startClient?: boolean;
229
+ accountId?: string | null;
230
+ abortSignal?: AbortSignal;
231
+ } = {},
232
+ ): Promise<MatrixClient> {
233
+ const state = await resolveSharedMatrixClientState(params);
234
+ return state.client;
235
+ }
236
+
237
+ export async function acquireSharedMatrixClient(
238
+ params: {
239
+ cfg?: CoreConfig;
240
+ env?: NodeJS.ProcessEnv;
241
+ timeoutMs?: number;
242
+ auth?: MatrixAuth;
243
+ startClient?: boolean;
244
+ accountId?: string | null;
245
+ abortSignal?: AbortSignal;
246
+ } = {},
247
+ ): Promise<MatrixClient> {
248
+ const state = await resolveSharedMatrixClientState(params);
249
+ state.leases += 1;
250
+ return state.client;
251
+ }
252
+
253
+ export function stopSharedClient(): void {
254
+ for (const state of sharedClientStates.values()) {
255
+ state.client.stop();
256
+ }
257
+ sharedClientStates.clear();
258
+ sharedClientPromises.clear();
259
+ }
260
+
261
+ export function stopSharedClientForAccount(auth: MatrixAuth): void {
262
+ const key = buildSharedClientKey(auth);
263
+ const state = sharedClientStates.get(key);
264
+ if (!state) {
265
+ return;
266
+ }
267
+ state.client.stop();
268
+ deleteSharedClientState(state);
269
+ }
270
+
271
+ export function removeSharedClientInstance(client: MatrixClient): boolean {
272
+ const state = findSharedClientStateByInstance(client);
273
+ if (!state) {
274
+ return false;
275
+ }
276
+ deleteSharedClientState(state);
277
+ return true;
278
+ }
279
+
280
+ export function stopSharedClientInstance(client: MatrixClient): void {
281
+ if (!removeSharedClientInstance(client)) {
282
+ return;
283
+ }
284
+ client.stop();
285
+ }
286
+
287
+ export async function releaseSharedClientInstance(
288
+ client: MatrixClient,
289
+ mode: "stop" | "persist" = "stop",
290
+ ): Promise<boolean> {
291
+ const state = findSharedClientStateByInstance(client);
292
+ if (!state) {
293
+ return false;
294
+ }
295
+ state.leases = Math.max(0, state.leases - 1);
296
+ if (state.leases > 0) {
297
+ return false;
298
+ }
299
+ deleteSharedClientState(state);
300
+ if (mode === "persist") {
301
+ await client.stopAndPersist();
302
+ } else {
303
+ client.stop();
304
+ }
305
+ return true;
306
+ }