@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,174 @@
1
+ import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import {
3
+ createMockMatrixClient,
4
+ expectExplicitMatrixClientConfig,
5
+ expectOneOffSharedMatrixClient,
6
+ matrixClientResolverMocks,
7
+ primeMatrixClientResolverMocks,
8
+ } from "../client-resolver.test-helpers.js";
9
+
10
+ const {
11
+ getMatrixRuntimeMock,
12
+ getActiveMatrixClientMock,
13
+ acquireSharedMatrixClientMock,
14
+ releaseSharedClientInstanceMock,
15
+ isBunRuntimeMock,
16
+ resolveMatrixAuthContextMock,
17
+ } = matrixClientResolverMocks;
18
+
19
+ vi.mock("../active-client.js", () => ({
20
+ getActiveMatrixClient: (...args: unknown[]) => getActiveMatrixClientMock(...args),
21
+ }));
22
+
23
+ vi.mock("../client.js", () => ({
24
+ acquireSharedMatrixClient: (...args: unknown[]) => acquireSharedMatrixClientMock(...args),
25
+ isBunRuntime: () => isBunRuntimeMock(),
26
+ resolveMatrixAuthContext: resolveMatrixAuthContextMock,
27
+ }));
28
+
29
+ vi.mock("../client/shared.js", () => ({
30
+ releaseSharedClientInstance: (...args: unknown[]) => releaseSharedClientInstanceMock(...args),
31
+ }));
32
+
33
+ vi.mock("../../runtime.js", () => ({
34
+ getMatrixRuntime: () => getMatrixRuntimeMock(),
35
+ }));
36
+
37
+ let withResolvedMatrixControlClient: typeof import("./client.js").withResolvedMatrixControlClient;
38
+ let withResolvedMatrixSendClient: typeof import("./client.js").withResolvedMatrixSendClient;
39
+
40
+ describe("matrix send client helpers", () => {
41
+ beforeAll(async () => {
42
+ ({ withResolvedMatrixControlClient, withResolvedMatrixSendClient } =
43
+ await import("./client.js"));
44
+ });
45
+
46
+ beforeEach(() => {
47
+ primeMatrixClientResolverMocks({
48
+ resolved: {},
49
+ });
50
+ });
51
+
52
+ afterEach(() => {
53
+ vi.unstubAllEnvs();
54
+ });
55
+
56
+ it("stops one-off shared clients when no active monitor client is registered", async () => {
57
+ vi.stubEnv("OPENCLAW_GATEWAY_PORT", "18799");
58
+
59
+ const result = await withResolvedMatrixSendClient({ accountId: "default" }, async () => "ok");
60
+
61
+ await expectOneOffSharedMatrixClient({
62
+ prepareForOneOffCalls: 0,
63
+ startCalls: 1,
64
+ releaseMode: "persist",
65
+ });
66
+ expect(result).toBe("ok");
67
+ });
68
+
69
+ it("reuses active monitor client when available", async () => {
70
+ const activeClient = createMockMatrixClient();
71
+ getActiveMatrixClientMock.mockReturnValue(activeClient);
72
+
73
+ const result = await withResolvedMatrixSendClient({ accountId: "default" }, async (client) => {
74
+ expect(client).toBe(activeClient);
75
+ return "ok";
76
+ });
77
+
78
+ expect(result).toBe("ok");
79
+ expect(acquireSharedMatrixClientMock).not.toHaveBeenCalled();
80
+ expect(activeClient.start).toHaveBeenCalledTimes(1);
81
+ expect(activeClient.stop).not.toHaveBeenCalled();
82
+ expect(activeClient.stopAndPersist).not.toHaveBeenCalled();
83
+ });
84
+
85
+ it("uses the effective account id when auth resolution is implicit", async () => {
86
+ resolveMatrixAuthContextMock.mockReturnValue({
87
+ cfg: {},
88
+ env: process.env,
89
+ accountId: "ops",
90
+ resolved: {},
91
+ });
92
+ await withResolvedMatrixSendClient({}, async () => {});
93
+
94
+ await expectOneOffSharedMatrixClient({
95
+ accountId: "ops",
96
+ prepareForOneOffCalls: 0,
97
+ startCalls: 1,
98
+ releaseMode: "persist",
99
+ });
100
+ });
101
+
102
+ it("uses explicit cfg instead of loading runtime config", async () => {
103
+ const explicitCfg = {
104
+ channels: {
105
+ matrix: {
106
+ defaultAccount: "ops",
107
+ },
108
+ },
109
+ };
110
+
111
+ await withResolvedMatrixSendClient({ cfg: explicitCfg, accountId: "ops" }, async () => {});
112
+
113
+ expectExplicitMatrixClientConfig({
114
+ cfg: explicitCfg,
115
+ accountId: "ops",
116
+ });
117
+ });
118
+
119
+ it("stops shared matrix clients when wrapped sends fail", async () => {
120
+ const sharedClient = createMockMatrixClient();
121
+ acquireSharedMatrixClientMock.mockResolvedValue(sharedClient);
122
+
123
+ await expect(
124
+ withResolvedMatrixSendClient({ accountId: "default" }, async () => {
125
+ throw new Error("boom");
126
+ }),
127
+ ).rejects.toThrow("boom");
128
+
129
+ expect(releaseSharedClientInstanceMock).toHaveBeenCalledWith(sharedClient, "persist");
130
+ });
131
+
132
+ it("starts one-off clients before outbound sends so encrypted rooms can reuse live crypto state", async () => {
133
+ const sharedClient = createMockMatrixClient();
134
+ acquireSharedMatrixClientMock.mockResolvedValue(sharedClient);
135
+
136
+ await withResolvedMatrixSendClient({ accountId: "default" }, async () => "ok");
137
+
138
+ expect(sharedClient.start).toHaveBeenCalledTimes(1);
139
+ expect(sharedClient.prepareForOneOff).not.toHaveBeenCalled();
140
+ });
141
+
142
+ it("keeps one-off control clients lightweight when no active monitor client is registered", async () => {
143
+ const result = await withResolvedMatrixControlClient(
144
+ { accountId: "default" },
145
+ async () => "ok",
146
+ );
147
+
148
+ await expectOneOffSharedMatrixClient({
149
+ prepareForOneOffCalls: 0,
150
+ startCalls: 0,
151
+ releaseMode: "stop",
152
+ });
153
+ expect(result).toBe("ok");
154
+ });
155
+
156
+ it("reuses active monitor clients for control operations without restarting them", async () => {
157
+ const activeClient = createMockMatrixClient();
158
+ getActiveMatrixClientMock.mockReturnValue(activeClient);
159
+
160
+ const result = await withResolvedMatrixControlClient(
161
+ { accountId: "default" },
162
+ async (client) => {
163
+ expect(client).toBe(activeClient);
164
+ return "ok";
165
+ },
166
+ );
167
+
168
+ expect(result).toBe("ok");
169
+ expect(acquireSharedMatrixClientMock).not.toHaveBeenCalled();
170
+ expect(activeClient.start).not.toHaveBeenCalled();
171
+ expect(activeClient.stop).not.toHaveBeenCalled();
172
+ expect(activeClient.stopAndPersist).not.toHaveBeenCalled();
173
+ });
174
+ });
@@ -0,0 +1,90 @@
1
+ import { getMatrixRuntime } from "../../runtime.js";
2
+ import type { CoreConfig } from "../../types.js";
3
+ import { resolveMatrixAccountConfig } from "../account-config.js";
4
+ import type { MatrixClient } from "../sdk.js";
5
+
6
+ const getCore = () => getMatrixRuntime();
7
+
8
+ type MatrixSendClientRuntime = Pick<
9
+ typeof import("../client-bootstrap.js"),
10
+ "withResolvedRuntimeMatrixClient"
11
+ >;
12
+
13
+ let matrixSendClientRuntimePromise: Promise<MatrixSendClientRuntime> | null = null;
14
+
15
+ async function loadMatrixSendClientRuntime(): Promise<MatrixSendClientRuntime> {
16
+ matrixSendClientRuntimePromise ??= import("../client-bootstrap.js");
17
+ return await matrixSendClientRuntimePromise;
18
+ }
19
+
20
+ export function resolveMediaMaxBytes(
21
+ accountId?: string | null,
22
+ cfg?: CoreConfig,
23
+ ): number | undefined {
24
+ const resolvedCfg = cfg ?? (getCore().config.loadConfig() as CoreConfig);
25
+ const matrixCfg = resolveMatrixAccountConfig({ cfg: resolvedCfg, accountId });
26
+ const mediaMaxMb = typeof matrixCfg.mediaMaxMb === "number" ? matrixCfg.mediaMaxMb : undefined;
27
+ if (typeof mediaMaxMb === "number") {
28
+ return mediaMaxMb * 1024 * 1024;
29
+ }
30
+ return undefined;
31
+ }
32
+
33
+ export async function withResolvedMatrixSendClient<T>(
34
+ opts: {
35
+ client?: MatrixClient;
36
+ cfg?: CoreConfig;
37
+ timeoutMs?: number;
38
+ accountId?: string | null;
39
+ },
40
+ run: (client: MatrixClient) => Promise<T>,
41
+ ): Promise<T> {
42
+ return await withResolvedMatrixClient(
43
+ {
44
+ ...opts,
45
+ // One-off outbound sends still need a started client so room encryption
46
+ // state and live crypto sessions are available before sendMessage/sendEvent.
47
+ readiness: "started",
48
+ },
49
+ run,
50
+ // Started one-off send clients should flush sync/crypto state before CLI
51
+ // shutdown paths can tear down the process.
52
+ "persist",
53
+ );
54
+ }
55
+
56
+ export async function withResolvedMatrixControlClient<T>(
57
+ opts: {
58
+ client?: MatrixClient;
59
+ cfg?: CoreConfig;
60
+ timeoutMs?: number;
61
+ accountId?: string | null;
62
+ },
63
+ run: (client: MatrixClient) => Promise<T>,
64
+ ): Promise<T> {
65
+ return await withResolvedMatrixClient(
66
+ {
67
+ ...opts,
68
+ readiness: "none",
69
+ },
70
+ run,
71
+ );
72
+ }
73
+
74
+ async function withResolvedMatrixClient<T>(
75
+ opts: {
76
+ client?: MatrixClient;
77
+ cfg?: CoreConfig;
78
+ timeoutMs?: number;
79
+ accountId?: string | null;
80
+ readiness: "started" | "none";
81
+ },
82
+ run: (client: MatrixClient) => Promise<T>,
83
+ shutdownBehavior?: "persist",
84
+ ): Promise<T> {
85
+ if (opts.client) {
86
+ return await run(opts.client);
87
+ }
88
+ const { withResolvedRuntimeMatrixClient } = await loadMatrixSendClientRuntime();
89
+ return await withResolvedRuntimeMatrixClient(opts, run, shutdownBehavior);
90
+ }
@@ -0,0 +1,189 @@
1
+ import { getMatrixRuntime } from "../../runtime.js";
2
+ import {
3
+ markdownToMatrixHtml,
4
+ resolveMatrixMentionsInMarkdown,
5
+ renderMarkdownToMatrixHtmlWithMentions,
6
+ type MatrixMentions,
7
+ } from "../format.js";
8
+ import type { MatrixClient } from "../sdk.js";
9
+ import {
10
+ MsgType,
11
+ RelationType,
12
+ type MatrixFormattedContent,
13
+ type MatrixMediaMsgType,
14
+ type MatrixRelation,
15
+ type MatrixReplyRelation,
16
+ type MatrixTextContent,
17
+ type MatrixTextMsgType,
18
+ type MatrixThreadRelation,
19
+ } from "./types.js";
20
+
21
+ const getCore = () => getMatrixRuntime();
22
+
23
+ async function renderMatrixFormattedContent(params: {
24
+ client: MatrixClient;
25
+ markdown?: string | null;
26
+ includeMentions?: boolean;
27
+ }): Promise<{ html?: string; mentions?: MatrixMentions }> {
28
+ const markdown = params.markdown ?? "";
29
+ if (params.includeMentions === false) {
30
+ const html = markdownToMatrixHtml(markdown).trimEnd();
31
+ return { html: html || undefined };
32
+ }
33
+ const { html, mentions } = await renderMarkdownToMatrixHtmlWithMentions({
34
+ markdown,
35
+ client: params.client,
36
+ });
37
+ return { html, mentions };
38
+ }
39
+
40
+ export function buildTextContent(
41
+ body: string,
42
+ relation?: MatrixRelation,
43
+ opts: {
44
+ msgtype?: MatrixTextMsgType;
45
+ } = {},
46
+ ): MatrixTextContent {
47
+ const msgtype = opts.msgtype ?? MsgType.Text;
48
+ return relation
49
+ ? {
50
+ msgtype,
51
+ body,
52
+ "m.relates_to": relation,
53
+ }
54
+ : {
55
+ msgtype,
56
+ body,
57
+ };
58
+ }
59
+
60
+ export async function enrichMatrixFormattedContent(params: {
61
+ client: MatrixClient;
62
+ content: MatrixFormattedContent;
63
+ markdown?: string | null;
64
+ includeMentions?: boolean;
65
+ }): Promise<void> {
66
+ const { html, mentions } = await renderMatrixFormattedContent({
67
+ client: params.client,
68
+ markdown: params.markdown,
69
+ includeMentions: params.includeMentions,
70
+ });
71
+ if (mentions) {
72
+ params.content["m.mentions"] = mentions;
73
+ } else {
74
+ delete params.content["m.mentions"];
75
+ }
76
+ if (!html) {
77
+ delete params.content.format;
78
+ delete params.content.formatted_body;
79
+ return;
80
+ }
81
+ params.content.format = "org.matrix.custom.html";
82
+ params.content.formatted_body = html;
83
+ }
84
+
85
+ export async function resolveMatrixMentionsForBody(params: {
86
+ client: MatrixClient;
87
+ body: string;
88
+ }): Promise<MatrixMentions> {
89
+ return await resolveMatrixMentionsInMarkdown({
90
+ markdown: params.body ?? "",
91
+ client: params.client,
92
+ });
93
+ }
94
+
95
+ function normalizeMentionUserIds(value: unknown): string[] {
96
+ return Array.isArray(value)
97
+ ? value.filter((entry): entry is string => typeof entry === "string" && entry.trim().length > 0)
98
+ : [];
99
+ }
100
+
101
+ export function extractMatrixMentions(
102
+ content: Record<string, unknown> | undefined,
103
+ ): MatrixMentions {
104
+ const rawMentions = content?.["m.mentions"];
105
+ if (!rawMentions || typeof rawMentions !== "object") {
106
+ return {};
107
+ }
108
+ const mentions = rawMentions as { room?: unknown; user_ids?: unknown };
109
+ const normalized: MatrixMentions = {};
110
+ const userIds = normalizeMentionUserIds(mentions.user_ids);
111
+ if (userIds.length > 0) {
112
+ normalized.user_ids = userIds;
113
+ }
114
+ if (mentions.room === true) {
115
+ normalized.room = true;
116
+ }
117
+ return normalized;
118
+ }
119
+
120
+ export function diffMatrixMentions(
121
+ current: MatrixMentions,
122
+ previous: MatrixMentions,
123
+ ): MatrixMentions {
124
+ const previousUserIds = new Set(previous.user_ids ?? []);
125
+ const newUserIds = (current.user_ids ?? []).filter((userId) => !previousUserIds.has(userId));
126
+ const delta: MatrixMentions = {};
127
+ if (newUserIds.length > 0) {
128
+ delta.user_ids = newUserIds;
129
+ }
130
+ if (current.room && !previous.room) {
131
+ delta.room = true;
132
+ }
133
+ return delta;
134
+ }
135
+
136
+ export function buildReplyRelation(replyToId?: string): MatrixReplyRelation | undefined {
137
+ const trimmed = replyToId?.trim();
138
+ if (!trimmed) {
139
+ return undefined;
140
+ }
141
+ return { "m.in_reply_to": { event_id: trimmed } };
142
+ }
143
+
144
+ export function buildThreadRelation(threadId: string, replyToId?: string): MatrixThreadRelation {
145
+ const trimmed = threadId.trim();
146
+ return {
147
+ rel_type: RelationType.Thread,
148
+ event_id: trimmed,
149
+ is_falling_back: true,
150
+ "m.in_reply_to": { event_id: replyToId?.trim() || trimmed },
151
+ };
152
+ }
153
+
154
+ export function resolveMatrixMsgType(contentType?: string, _fileName?: string): MatrixMediaMsgType {
155
+ const kind = getCore().media.mediaKindFromMime(contentType ?? "");
156
+ switch (kind) {
157
+ case "image":
158
+ return MsgType.Image;
159
+ case "audio":
160
+ return MsgType.Audio;
161
+ case "video":
162
+ return MsgType.Video;
163
+ default:
164
+ return MsgType.File;
165
+ }
166
+ }
167
+
168
+ export function resolveMatrixVoiceDecision(opts: {
169
+ wantsVoice: boolean;
170
+ contentType?: string;
171
+ fileName?: string;
172
+ }): { useVoice: boolean } {
173
+ if (!opts.wantsVoice) {
174
+ return { useVoice: false };
175
+ }
176
+ if (isMatrixVoiceCompatibleAudio(opts)) {
177
+ return { useVoice: true };
178
+ }
179
+ return { useVoice: false };
180
+ }
181
+
182
+ function isMatrixVoiceCompatibleAudio(opts: { contentType?: string; fileName?: string }): boolean {
183
+ // Matrix currently shares the core voice compatibility policy.
184
+ // Keep this wrapper as the seam if Matrix policy diverges later.
185
+ return getCore().media.isVoiceCompatibleAudio({
186
+ contentType: opts.contentType,
187
+ fileName: opts.fileName,
188
+ });
189
+ }
@@ -0,0 +1,244 @@
1
+ import { parseBuffer, type IFileInfo } from "music-metadata";
2
+ import { getMatrixRuntime } from "../../runtime.js";
3
+ import type {
4
+ DimensionalFileInfo,
5
+ EncryptedFile,
6
+ FileWithThumbnailInfo,
7
+ MatrixClient,
8
+ TimedFileInfo,
9
+ VideoFileInfo,
10
+ } from "../sdk.js";
11
+ import {
12
+ type MatrixMediaContent,
13
+ type MatrixMediaInfo,
14
+ type MatrixMediaMsgType,
15
+ type MatrixRelation,
16
+ type MediaKind,
17
+ } from "./types.js";
18
+
19
+ const getCore = () => getMatrixRuntime();
20
+
21
+ export function buildMatrixMediaInfo(params: {
22
+ size: number;
23
+ mimetype?: string;
24
+ durationMs?: number;
25
+ imageInfo?: DimensionalFileInfo;
26
+ }): MatrixMediaInfo | undefined {
27
+ const base: FileWithThumbnailInfo = {};
28
+ if (Number.isFinite(params.size)) {
29
+ base.size = params.size;
30
+ }
31
+ if (params.mimetype) {
32
+ base.mimetype = params.mimetype;
33
+ }
34
+ if (params.imageInfo) {
35
+ const dimensional: DimensionalFileInfo = {
36
+ ...base,
37
+ ...params.imageInfo,
38
+ };
39
+ if (typeof params.durationMs === "number") {
40
+ const videoInfo: VideoFileInfo = {
41
+ ...dimensional,
42
+ duration: params.durationMs,
43
+ };
44
+ return videoInfo;
45
+ }
46
+ return dimensional;
47
+ }
48
+ if (typeof params.durationMs === "number") {
49
+ const timedInfo: TimedFileInfo = {
50
+ ...base,
51
+ duration: params.durationMs,
52
+ };
53
+ return timedInfo;
54
+ }
55
+ if (Object.keys(base).length === 0) {
56
+ return undefined;
57
+ }
58
+ return base;
59
+ }
60
+
61
+ export function buildMediaContent(params: {
62
+ msgtype: MatrixMediaMsgType;
63
+ body: string;
64
+ url?: string;
65
+ filename?: string;
66
+ mimetype?: string;
67
+ size: number;
68
+ relation?: MatrixRelation;
69
+ isVoice?: boolean;
70
+ durationMs?: number;
71
+ imageInfo?: DimensionalFileInfo;
72
+ file?: EncryptedFile;
73
+ }): MatrixMediaContent {
74
+ const info = buildMatrixMediaInfo({
75
+ size: params.size,
76
+ mimetype: params.mimetype,
77
+ durationMs: params.durationMs,
78
+ imageInfo: params.imageInfo,
79
+ });
80
+ const base: MatrixMediaContent = {
81
+ msgtype: params.msgtype,
82
+ body: params.body,
83
+ filename: params.filename,
84
+ info: info ?? undefined,
85
+ };
86
+ // Encrypted media should only include the "file" payload, not top-level "url".
87
+ if (!params.file && params.url) {
88
+ base.url = params.url;
89
+ }
90
+ // For encrypted files, add the file object
91
+ if (params.file) {
92
+ base.file = params.file;
93
+ }
94
+ if (params.isVoice) {
95
+ base["org.matrix.msc3245.voice"] = {};
96
+ if (typeof params.durationMs === "number") {
97
+ base["org.matrix.msc1767.audio"] = {
98
+ duration: params.durationMs,
99
+ };
100
+ }
101
+ }
102
+ if (params.relation) {
103
+ base["m.relates_to"] = params.relation;
104
+ }
105
+ return base;
106
+ }
107
+
108
+ const THUMBNAIL_MAX_SIDE = 800;
109
+ const THUMBNAIL_QUALITY = 80;
110
+
111
+ export async function prepareImageInfo(params: {
112
+ buffer: Buffer;
113
+ client: MatrixClient;
114
+ encrypted?: boolean;
115
+ }): Promise<DimensionalFileInfo | undefined> {
116
+ const meta = await getCore()
117
+ .media.getImageMetadata(params.buffer)
118
+ .catch(() => null);
119
+ if (!meta) {
120
+ return undefined;
121
+ }
122
+ const imageInfo: DimensionalFileInfo = { w: meta.width, h: meta.height };
123
+ const maxDim = Math.max(meta.width, meta.height);
124
+ if (maxDim > THUMBNAIL_MAX_SIDE) {
125
+ try {
126
+ const thumbBuffer = await getCore().media.resizeToJpeg({
127
+ buffer: params.buffer,
128
+ maxSide: THUMBNAIL_MAX_SIDE,
129
+ quality: THUMBNAIL_QUALITY,
130
+ withoutEnlargement: true,
131
+ });
132
+ const thumbMeta = await getCore()
133
+ .media.getImageMetadata(thumbBuffer)
134
+ .catch(() => null);
135
+ const result = await uploadMediaWithEncryption(params.client, thumbBuffer, {
136
+ contentType: "image/jpeg",
137
+ filename: "thumbnail.jpg",
138
+ encrypted: params.encrypted === true,
139
+ });
140
+ if (result.file) {
141
+ imageInfo.thumbnail_file = result.file;
142
+ } else {
143
+ imageInfo.thumbnail_url = result.url;
144
+ }
145
+ if (thumbMeta) {
146
+ imageInfo.thumbnail_info = {
147
+ w: thumbMeta.width,
148
+ h: thumbMeta.height,
149
+ mimetype: "image/jpeg",
150
+ size: thumbBuffer.byteLength,
151
+ };
152
+ }
153
+ } catch {
154
+ // Thumbnail generation failed, continue without it
155
+ }
156
+ }
157
+ return imageInfo;
158
+ }
159
+
160
+ export async function resolveMediaDurationMs(params: {
161
+ buffer: Buffer;
162
+ contentType?: string;
163
+ fileName?: string;
164
+ kind: MediaKind;
165
+ }): Promise<number | undefined> {
166
+ if (params.kind !== "audio" && params.kind !== "video") {
167
+ return undefined;
168
+ }
169
+ try {
170
+ const fileInfo: IFileInfo | string | undefined =
171
+ params.contentType || params.fileName
172
+ ? {
173
+ mimeType: params.contentType,
174
+ size: params.buffer.byteLength,
175
+ path: params.fileName,
176
+ }
177
+ : undefined;
178
+ const metadata = await parseBuffer(params.buffer, fileInfo, {
179
+ duration: true,
180
+ skipCovers: true,
181
+ });
182
+ const durationSeconds = metadata.format.duration;
183
+ if (typeof durationSeconds === "number" && Number.isFinite(durationSeconds)) {
184
+ return Math.max(0, Math.round(durationSeconds * 1000));
185
+ }
186
+ } catch {
187
+ // Duration is optional; ignore parse failures.
188
+ }
189
+ return undefined;
190
+ }
191
+
192
+ async function uploadFile(
193
+ client: MatrixClient,
194
+ file: Buffer,
195
+ params: {
196
+ contentType?: string;
197
+ filename?: string;
198
+ },
199
+ ): Promise<string> {
200
+ return await client.uploadContent(file, params.contentType, params.filename);
201
+ }
202
+
203
+ async function uploadMediaWithEncryption(
204
+ client: MatrixClient,
205
+ buffer: Buffer,
206
+ params: {
207
+ contentType?: string;
208
+ filename?: string;
209
+ encrypted: boolean;
210
+ },
211
+ ): Promise<{ url: string; file?: EncryptedFile }> {
212
+ if (params.encrypted && client.crypto) {
213
+ const encrypted = await client.crypto.encryptMedia(buffer);
214
+ const mxc = await client.uploadContent(encrypted.buffer, params.contentType, params.filename);
215
+ const file: EncryptedFile = { url: mxc, ...encrypted.file };
216
+ return {
217
+ url: mxc,
218
+ file,
219
+ };
220
+ }
221
+
222
+ const mxc = await uploadFile(client, buffer, params);
223
+ return { url: mxc };
224
+ }
225
+
226
+ /**
227
+ * Upload media with optional encryption for E2EE rooms.
228
+ */
229
+ export async function uploadMediaMaybeEncrypted(
230
+ client: MatrixClient,
231
+ roomId: string,
232
+ buffer: Buffer,
233
+ params: {
234
+ contentType?: string;
235
+ filename?: string;
236
+ },
237
+ ): Promise<{ url: string; file?: EncryptedFile }> {
238
+ // Check if room is encrypted and crypto is available
239
+ const isEncrypted = Boolean(client.crypto && (await client.crypto.isRoomEncrypted(roomId)));
240
+ return await uploadMediaWithEncryption(client, buffer, {
241
+ ...params,
242
+ encrypted: isEncrypted,
243
+ });
244
+ }