@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,257 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+ import type { CoreConfig } from "../../types.js";
3
+ import type { MatrixAccountPatch } from "../config-update.js";
4
+ import type { MatrixManagedDeviceInfo } from "../device-health.js";
5
+ import type { MatrixProfileSyncResult } from "../profile.js";
6
+ import type { MatrixOwnDeviceVerificationStatus } from "../sdk.js";
7
+ import type { MatrixLegacyCryptoRestoreResult } from "./legacy-crypto-restore.js";
8
+ import type { MatrixStartupVerificationOutcome } from "./startup-verification.js";
9
+ import type { MatrixStartupMaintenanceDeps } from "./startup.js";
10
+ import { runMatrixStartupMaintenance } from "./startup.js";
11
+
12
+ function createVerificationStatus(
13
+ overrides: Partial<MatrixOwnDeviceVerificationStatus> = {},
14
+ ): MatrixOwnDeviceVerificationStatus {
15
+ return {
16
+ encryptionEnabled: true,
17
+ userId: "@bot:example.org",
18
+ deviceId: "DEVICE",
19
+ verified: false,
20
+ localVerified: false,
21
+ crossSigningVerified: false,
22
+ signedByOwner: false,
23
+ recoveryKeyStored: false,
24
+ recoveryKeyCreatedAt: null,
25
+ recoveryKeyId: null,
26
+ backupVersion: null,
27
+ backup: {
28
+ serverVersion: null,
29
+ activeVersion: null,
30
+ trusted: null,
31
+ matchesDecryptionKey: null,
32
+ decryptionKeyCached: null,
33
+ keyLoadAttempted: false,
34
+ keyLoadError: null,
35
+ },
36
+ ...overrides,
37
+ };
38
+ }
39
+
40
+ function createProfileSyncResult(
41
+ overrides: Partial<MatrixProfileSyncResult> = {},
42
+ ): MatrixProfileSyncResult {
43
+ return {
44
+ skipped: false,
45
+ displayNameUpdated: false,
46
+ avatarUpdated: false,
47
+ resolvedAvatarUrl: null,
48
+ uploadedAvatarSource: null,
49
+ convertedAvatarFromHttp: false,
50
+ ...overrides,
51
+ };
52
+ }
53
+
54
+ function createStartupVerificationOutcome(
55
+ kind: Exclude<MatrixStartupVerificationOutcome["kind"], "unsupported">,
56
+ overrides: Partial<Extract<MatrixStartupVerificationOutcome, { kind: typeof kind }>> = {},
57
+ ): MatrixStartupVerificationOutcome {
58
+ return {
59
+ kind,
60
+ verification: createVerificationStatus({ verified: kind === "verified" }),
61
+ ...overrides,
62
+ } as MatrixStartupVerificationOutcome;
63
+ }
64
+
65
+ function createLegacyCryptoRestoreResult(
66
+ overrides: Partial<MatrixLegacyCryptoRestoreResult> = {},
67
+ ): MatrixLegacyCryptoRestoreResult {
68
+ return {
69
+ kind: "skipped",
70
+ ...overrides,
71
+ } as MatrixLegacyCryptoRestoreResult;
72
+ }
73
+
74
+ function createDeps(
75
+ overrides: Partial<MatrixStartupMaintenanceDeps> = {},
76
+ ): MatrixStartupMaintenanceDeps {
77
+ return {
78
+ maybeRestoreLegacyMatrixBackup: vi.fn(async () => createLegacyCryptoRestoreResult()),
79
+ summarizeMatrixDeviceHealth: vi.fn(() => ({
80
+ currentDeviceId: null,
81
+ staleOpenClawDevices: [] as MatrixManagedDeviceInfo[],
82
+ currentOpenClawDevices: [] as MatrixManagedDeviceInfo[],
83
+ })),
84
+ syncMatrixOwnProfile: vi.fn(async () => createProfileSyncResult()),
85
+ ensureMatrixStartupVerification: vi.fn(async () =>
86
+ createStartupVerificationOutcome("verified"),
87
+ ),
88
+ updateMatrixAccountConfig: vi.fn(
89
+ (cfg: CoreConfig, _accountId: string, _patch: MatrixAccountPatch) => cfg,
90
+ ),
91
+ ...overrides,
92
+ };
93
+ }
94
+
95
+ describe("runMatrixStartupMaintenance", () => {
96
+ let deps: MatrixStartupMaintenanceDeps;
97
+
98
+ beforeEach(() => {
99
+ deps = createDeps();
100
+ });
101
+
102
+ function createParams(): Parameters<typeof runMatrixStartupMaintenance>[0] {
103
+ return {
104
+ client: {
105
+ crypto: {},
106
+ listOwnDevices: vi.fn(async () => []),
107
+ getOwnDeviceVerificationStatus: vi.fn(async () => createVerificationStatus()),
108
+ } as never,
109
+ auth: {
110
+ accountId: "ops",
111
+ homeserver: "https://matrix.example.org",
112
+ userId: "@bot:example.org",
113
+ accessToken: "token",
114
+ encryption: false,
115
+ },
116
+ accountId: "ops",
117
+ effectiveAccountId: "ops",
118
+ accountConfig: {
119
+ name: "Ops Bot",
120
+ avatarUrl: "https://example.org/avatar.png",
121
+ },
122
+ logger: {
123
+ info: vi.fn(),
124
+ warn: vi.fn(),
125
+ debug: vi.fn(),
126
+ error: vi.fn(),
127
+ },
128
+ logVerboseMessage: vi.fn(),
129
+ loadConfig: vi.fn(() => ({ channels: { matrix: {} } })),
130
+ writeConfigFile: vi.fn(async () => {}),
131
+ loadWebMedia: vi.fn(async () => ({
132
+ buffer: Buffer.from("avatar"),
133
+ contentType: "image/png",
134
+ fileName: "avatar.png",
135
+ })),
136
+ abortSignal: undefined,
137
+ env: {},
138
+ };
139
+ }
140
+
141
+ it("persists converted avatar URLs after profile sync", async () => {
142
+ const params = createParams();
143
+ const updatedCfg = { channels: { matrix: { avatarUrl: "mxc://avatar" } } };
144
+ vi.mocked(deps.syncMatrixOwnProfile).mockResolvedValue(
145
+ createProfileSyncResult({
146
+ avatarUpdated: true,
147
+ resolvedAvatarUrl: "mxc://avatar",
148
+ uploadedAvatarSource: "http",
149
+ convertedAvatarFromHttp: true,
150
+ }),
151
+ );
152
+ vi.mocked(deps.updateMatrixAccountConfig).mockReturnValue(updatedCfg);
153
+
154
+ await runMatrixStartupMaintenance(params, deps);
155
+
156
+ expect(deps.syncMatrixOwnProfile).toHaveBeenCalledWith(
157
+ expect.objectContaining({
158
+ userId: "@bot:example.org",
159
+ displayName: "Ops Bot",
160
+ avatarUrl: "https://example.org/avatar.png",
161
+ }),
162
+ );
163
+ expect(deps.updateMatrixAccountConfig).toHaveBeenCalledWith(
164
+ { channels: { matrix: {} } },
165
+ "ops",
166
+ { avatarUrl: "mxc://avatar" },
167
+ );
168
+ expect(params.writeConfigFile).toHaveBeenCalledWith(updatedCfg as never);
169
+ expect(params.logVerboseMessage).toHaveBeenCalledWith(
170
+ "matrix: persisted converted avatar URL for account ops (mxc://avatar)",
171
+ );
172
+ });
173
+
174
+ it("reports stale devices, pending verification, and restored legacy backups", async () => {
175
+ const params = createParams();
176
+ params.auth.encryption = true;
177
+ vi.mocked(deps.summarizeMatrixDeviceHealth).mockReturnValue({
178
+ currentDeviceId: null,
179
+ staleOpenClawDevices: [
180
+ { deviceId: "DEV123", displayName: "OpenClaw Device", current: false },
181
+ ],
182
+ currentOpenClawDevices: [],
183
+ });
184
+ vi.mocked(deps.ensureMatrixStartupVerification).mockResolvedValue(
185
+ createStartupVerificationOutcome("pending"),
186
+ );
187
+ vi.mocked(deps.maybeRestoreLegacyMatrixBackup).mockResolvedValue(
188
+ createLegacyCryptoRestoreResult({
189
+ kind: "restored",
190
+ imported: 2,
191
+ total: 3,
192
+ localOnlyKeys: 1,
193
+ }),
194
+ );
195
+
196
+ await runMatrixStartupMaintenance(params, deps);
197
+
198
+ expect(params.logger.warn).toHaveBeenCalledWith(
199
+ "matrix: stale OpenClaw devices detected for @bot:example.org: DEV123. Run 'openclaw matrix devices prune-stale --account ops' to keep encrypted-room trust healthy.",
200
+ );
201
+ expect(params.logger.info).toHaveBeenCalledWith(
202
+ "matrix: device not verified — run 'openclaw matrix verify device <key>' to enable E2EE",
203
+ );
204
+ expect(params.logger.info).toHaveBeenCalledWith(
205
+ "matrix: startup verification request is already pending; finish it in another Matrix client",
206
+ );
207
+ expect(params.logger.info).toHaveBeenCalledWith(
208
+ "matrix: restored 2/3 room key(s) from legacy encrypted-state backup",
209
+ );
210
+ expect(params.logger.warn).toHaveBeenCalledWith(
211
+ "matrix: 1 legacy local-only room key(s) were never backed up and could not be restored automatically",
212
+ );
213
+ });
214
+
215
+ it("logs cooldown and request-failure verification outcomes without throwing", async () => {
216
+ const params = createParams();
217
+ params.auth.encryption = true;
218
+ vi.mocked(deps.ensureMatrixStartupVerification).mockResolvedValueOnce(
219
+ createStartupVerificationOutcome("cooldown", { retryAfterMs: 321 }),
220
+ );
221
+
222
+ await runMatrixStartupMaintenance(params, deps);
223
+
224
+ expect(params.logVerboseMessage).toHaveBeenCalledWith(
225
+ "matrix: skipped startup verification request due to cooldown (retryAfterMs=321)",
226
+ );
227
+
228
+ vi.mocked(deps.ensureMatrixStartupVerification).mockResolvedValueOnce(
229
+ createStartupVerificationOutcome("request-failed", { error: "boom" }),
230
+ );
231
+
232
+ await runMatrixStartupMaintenance(params, deps);
233
+
234
+ expect(params.logger.debug).toHaveBeenCalledWith(
235
+ "Matrix startup verification request failed (non-fatal)",
236
+ { error: "boom" },
237
+ );
238
+ });
239
+
240
+ it("aborts maintenance before later startup steps continue", async () => {
241
+ const params = createParams();
242
+ params.auth.encryption = true;
243
+ const abortController = new AbortController();
244
+ params.abortSignal = abortController.signal;
245
+ vi.mocked(deps.syncMatrixOwnProfile).mockImplementation(async () => {
246
+ abortController.abort();
247
+ return createProfileSyncResult();
248
+ });
249
+
250
+ await expect(runMatrixStartupMaintenance(params, deps)).rejects.toMatchObject({
251
+ message: "Matrix startup aborted",
252
+ name: "AbortError",
253
+ });
254
+ expect(deps.ensureMatrixStartupVerification).not.toHaveBeenCalled();
255
+ expect(deps.maybeRestoreLegacyMatrixBackup).not.toHaveBeenCalled();
256
+ });
257
+ });
@@ -0,0 +1,218 @@
1
+ import type { RuntimeLogger } from "../../runtime-api.js";
2
+ import type { CoreConfig, MatrixConfig } from "../../types.js";
3
+ import type { MatrixAuth } from "../client.js";
4
+ import type { MatrixClient } from "../sdk.js";
5
+ import { isMatrixStartupAbortError, throwIfMatrixStartupAborted } from "../startup-abort.js";
6
+
7
+ type MatrixStartupClient = Pick<
8
+ MatrixClient,
9
+ | "crypto"
10
+ | "getOwnDeviceVerificationStatus"
11
+ | "getUserProfile"
12
+ | "listOwnDevices"
13
+ | "restoreRoomKeyBackup"
14
+ | "setAvatarUrl"
15
+ | "setDisplayName"
16
+ | "uploadContent"
17
+ >;
18
+
19
+ export type MatrixStartupMaintenanceDeps = {
20
+ updateMatrixAccountConfig: typeof import("../config-update.js").updateMatrixAccountConfig;
21
+ summarizeMatrixDeviceHealth: typeof import("../device-health.js").summarizeMatrixDeviceHealth;
22
+ syncMatrixOwnProfile: typeof import("../profile.js").syncMatrixOwnProfile;
23
+ maybeRestoreLegacyMatrixBackup: typeof import("./legacy-crypto-restore.js").maybeRestoreLegacyMatrixBackup;
24
+ ensureMatrixStartupVerification: typeof import("./startup-verification.js").ensureMatrixStartupVerification;
25
+ };
26
+
27
+ let matrixStartupMaintenanceDepsPromise: Promise<MatrixStartupMaintenanceDeps> | undefined;
28
+
29
+ async function loadMatrixStartupMaintenanceDeps(): Promise<MatrixStartupMaintenanceDeps> {
30
+ matrixStartupMaintenanceDepsPromise ??= Promise.all([
31
+ import("../config-update.js"),
32
+ import("../device-health.js"),
33
+ import("../profile.js"),
34
+ import("./legacy-crypto-restore.js"),
35
+ import("./startup-verification.js"),
36
+ ]).then(
37
+ ([
38
+ configUpdateModule,
39
+ deviceHealthModule,
40
+ profileModule,
41
+ legacyCryptoRestoreModule,
42
+ startupVerificationModule,
43
+ ]) => ({
44
+ updateMatrixAccountConfig: configUpdateModule.updateMatrixAccountConfig,
45
+ summarizeMatrixDeviceHealth: deviceHealthModule.summarizeMatrixDeviceHealth,
46
+ syncMatrixOwnProfile: profileModule.syncMatrixOwnProfile,
47
+ maybeRestoreLegacyMatrixBackup: legacyCryptoRestoreModule.maybeRestoreLegacyMatrixBackup,
48
+ ensureMatrixStartupVerification: startupVerificationModule.ensureMatrixStartupVerification,
49
+ }),
50
+ );
51
+ return await matrixStartupMaintenanceDepsPromise;
52
+ }
53
+
54
+ export async function runMatrixStartupMaintenance(
55
+ params: {
56
+ client: MatrixStartupClient;
57
+ auth: MatrixAuth;
58
+ accountId: string;
59
+ effectiveAccountId: string;
60
+ accountConfig: MatrixConfig;
61
+ logger: RuntimeLogger;
62
+ logVerboseMessage: (message: string) => void;
63
+ loadConfig: () => CoreConfig;
64
+ writeConfigFile: (cfg: never) => Promise<void>;
65
+ loadWebMedia: (
66
+ url: string,
67
+ maxBytes: number,
68
+ ) => Promise<{ buffer: Buffer; contentType?: string; fileName?: string }>;
69
+ env?: NodeJS.ProcessEnv;
70
+ abortSignal?: AbortSignal;
71
+ },
72
+ deps?: MatrixStartupMaintenanceDeps,
73
+ ): Promise<void> {
74
+ const runtimeDeps = deps ?? (await loadMatrixStartupMaintenanceDeps());
75
+ throwIfMatrixStartupAborted(params.abortSignal);
76
+ try {
77
+ const profileSync = await runtimeDeps.syncMatrixOwnProfile({
78
+ client: params.client,
79
+ userId: params.auth.userId,
80
+ displayName: params.accountConfig.name,
81
+ avatarUrl: params.accountConfig.avatarUrl,
82
+ loadAvatarFromUrl: async (url, maxBytes) => await params.loadWebMedia(url, maxBytes),
83
+ });
84
+ throwIfMatrixStartupAborted(params.abortSignal);
85
+ if (profileSync.displayNameUpdated) {
86
+ params.logger.info(`matrix: profile display name updated for ${params.auth.userId}`);
87
+ }
88
+ if (profileSync.avatarUpdated) {
89
+ params.logger.info(`matrix: profile avatar updated for ${params.auth.userId}`);
90
+ }
91
+ if (
92
+ profileSync.convertedAvatarFromHttp &&
93
+ profileSync.resolvedAvatarUrl &&
94
+ params.accountConfig.avatarUrl !== profileSync.resolvedAvatarUrl
95
+ ) {
96
+ const latestCfg = params.loadConfig();
97
+ const updatedCfg = runtimeDeps.updateMatrixAccountConfig(latestCfg, params.accountId, {
98
+ avatarUrl: profileSync.resolvedAvatarUrl,
99
+ });
100
+ await params.writeConfigFile(updatedCfg as never);
101
+ throwIfMatrixStartupAborted(params.abortSignal);
102
+ params.logVerboseMessage(
103
+ `matrix: persisted converted avatar URL for account ${params.accountId} (${profileSync.resolvedAvatarUrl})`,
104
+ );
105
+ }
106
+ } catch (err) {
107
+ if (isMatrixStartupAbortError(err)) {
108
+ throw err;
109
+ }
110
+ params.logger.warn("matrix: failed to sync profile from config", { error: String(err) });
111
+ }
112
+
113
+ if (!(params.auth.encryption && params.client.crypto)) {
114
+ return;
115
+ }
116
+
117
+ try {
118
+ throwIfMatrixStartupAborted(params.abortSignal);
119
+ const deviceHealth = runtimeDeps.summarizeMatrixDeviceHealth(
120
+ await params.client.listOwnDevices(),
121
+ );
122
+ if (deviceHealth.staleOpenClawDevices.length > 0) {
123
+ params.logger.warn(
124
+ `matrix: stale OpenClaw devices detected for ${params.auth.userId}: ${deviceHealth.staleOpenClawDevices.map((device) => device.deviceId).join(", ")}. Run 'openclaw matrix devices prune-stale --account ${params.effectiveAccountId}' to keep encrypted-room trust healthy.`,
125
+ );
126
+ }
127
+ } catch (err) {
128
+ if (isMatrixStartupAbortError(err)) {
129
+ throw err;
130
+ }
131
+ params.logger.debug?.("Failed to inspect matrix device hygiene (non-fatal)", {
132
+ error: String(err),
133
+ });
134
+ }
135
+
136
+ try {
137
+ throwIfMatrixStartupAborted(params.abortSignal);
138
+ const startupVerification = await runtimeDeps.ensureMatrixStartupVerification({
139
+ client: params.client,
140
+ auth: params.auth,
141
+ accountConfig: params.accountConfig,
142
+ env: params.env,
143
+ });
144
+ throwIfMatrixStartupAborted(params.abortSignal);
145
+ if (startupVerification.kind === "verified") {
146
+ params.logger.info("matrix: device is verified by its owner and ready for encrypted rooms");
147
+ } else if (
148
+ startupVerification.kind === "disabled" ||
149
+ startupVerification.kind === "cooldown" ||
150
+ startupVerification.kind === "pending" ||
151
+ startupVerification.kind === "request-failed"
152
+ ) {
153
+ params.logger.info(
154
+ "matrix: device not verified — run 'openclaw matrix verify device <key>' to enable E2EE",
155
+ );
156
+ if (startupVerification.kind === "pending") {
157
+ params.logger.info(
158
+ "matrix: startup verification request is already pending; finish it in another Matrix client",
159
+ );
160
+ } else if (startupVerification.kind === "cooldown") {
161
+ params.logVerboseMessage(
162
+ `matrix: skipped startup verification request due to cooldown (retryAfterMs=${startupVerification.retryAfterMs ?? 0})`,
163
+ );
164
+ } else if (startupVerification.kind === "request-failed") {
165
+ params.logger.debug?.("Matrix startup verification request failed (non-fatal)", {
166
+ error: startupVerification.error ?? "unknown",
167
+ });
168
+ }
169
+ } else if (startupVerification.kind === "requested") {
170
+ params.logger.info(
171
+ "matrix: device not verified — requested verification in another Matrix client",
172
+ );
173
+ }
174
+ } catch (err) {
175
+ if (isMatrixStartupAbortError(err)) {
176
+ throw err;
177
+ }
178
+ params.logger.debug?.("Failed to resolve matrix verification status (non-fatal)", {
179
+ error: String(err),
180
+ });
181
+ }
182
+
183
+ try {
184
+ throwIfMatrixStartupAborted(params.abortSignal);
185
+ const legacyCryptoRestore = await runtimeDeps.maybeRestoreLegacyMatrixBackup({
186
+ client: params.client,
187
+ auth: params.auth,
188
+ env: params.env,
189
+ });
190
+ throwIfMatrixStartupAborted(params.abortSignal);
191
+ if (legacyCryptoRestore.kind === "restored") {
192
+ params.logger.info(
193
+ `matrix: restored ${legacyCryptoRestore.imported}/${legacyCryptoRestore.total} room key(s) from legacy encrypted-state backup`,
194
+ );
195
+ if (legacyCryptoRestore.localOnlyKeys > 0) {
196
+ params.logger.warn(
197
+ `matrix: ${legacyCryptoRestore.localOnlyKeys} legacy local-only room key(s) were never backed up and could not be restored automatically`,
198
+ );
199
+ }
200
+ } else if (legacyCryptoRestore.kind === "failed") {
201
+ params.logger.warn(
202
+ `matrix: failed restoring room keys from legacy encrypted-state backup: ${legacyCryptoRestore.error}`,
203
+ );
204
+ if (legacyCryptoRestore.localOnlyKeys > 0) {
205
+ params.logger.warn(
206
+ `matrix: ${legacyCryptoRestore.localOnlyKeys} legacy local-only room key(s) were never backed up and may remain unavailable until manually recovered`,
207
+ );
208
+ }
209
+ }
210
+ } catch (err) {
211
+ if (isMatrixStartupAbortError(err)) {
212
+ throw err;
213
+ }
214
+ params.logger.warn("matrix: failed restoring legacy encrypted-state backup", {
215
+ error: String(err),
216
+ });
217
+ }
218
+ }
@@ -0,0 +1,111 @@
1
+ import type { ChannelAccountSnapshot } from "openclaw/plugin-sdk/channel-contract";
2
+ import { createConnectedChannelStatusPatch } from "openclaw/plugin-sdk/gateway-runtime";
3
+ import { formatMatrixErrorMessage } from "../errors.js";
4
+ import {
5
+ isMatrixDisconnectedSyncState,
6
+ isMatrixReadySyncState,
7
+ type MatrixSyncState,
8
+ } from "../sync-state.js";
9
+
10
+ type MatrixMonitorStatusSink = (patch: ChannelAccountSnapshot) => void;
11
+
12
+ function cloneLastDisconnect(
13
+ value: ChannelAccountSnapshot["lastDisconnect"],
14
+ ): ChannelAccountSnapshot["lastDisconnect"] {
15
+ if (!value || typeof value === "string") {
16
+ return value ?? null;
17
+ }
18
+ return { ...value };
19
+ }
20
+
21
+ function formatSyncError(error: unknown): string | null {
22
+ if (!error) {
23
+ return null;
24
+ }
25
+ if (error instanceof Error) {
26
+ return error.message || error.name || "unknown";
27
+ }
28
+ return formatMatrixErrorMessage(error);
29
+ }
30
+
31
+ export type MatrixMonitorStatusController = ReturnType<typeof createMatrixMonitorStatusController>;
32
+
33
+ export function createMatrixMonitorStatusController(params: {
34
+ accountId: string;
35
+ baseUrl?: string;
36
+ statusSink?: MatrixMonitorStatusSink;
37
+ }) {
38
+ const status: ChannelAccountSnapshot = {
39
+ accountId: params.accountId,
40
+ ...(params.baseUrl ? { baseUrl: params.baseUrl } : {}),
41
+ connected: false,
42
+ lastConnectedAt: null,
43
+ lastDisconnect: null,
44
+ lastError: null,
45
+ healthState: "starting",
46
+ };
47
+
48
+ const emit = () => {
49
+ params.statusSink?.({
50
+ ...status,
51
+ lastDisconnect: cloneLastDisconnect(status.lastDisconnect),
52
+ });
53
+ };
54
+
55
+ const noteConnected = (at = Date.now()) => {
56
+ if (status.connected === true) {
57
+ status.lastEventAt = at;
58
+ } else {
59
+ Object.assign(status, createConnectedChannelStatusPatch(at));
60
+ }
61
+ status.lastError = null;
62
+ status.lastDisconnect = null;
63
+ status.healthState = "healthy";
64
+ emit();
65
+ };
66
+
67
+ const noteDisconnected = (params: { state: MatrixSyncState; at?: number; error?: unknown }) => {
68
+ const at = params.at ?? Date.now();
69
+ const error = formatSyncError(params.error);
70
+ status.connected = false;
71
+ status.lastEventAt = at;
72
+ status.lastDisconnect = {
73
+ at,
74
+ ...(error ? { error } : {}),
75
+ };
76
+ status.lastError = error;
77
+ status.healthState = params.state.toLowerCase();
78
+ emit();
79
+ };
80
+
81
+ emit();
82
+
83
+ return {
84
+ noteSyncState(state: MatrixSyncState, error?: unknown, at = Date.now()) {
85
+ if (isMatrixReadySyncState(state)) {
86
+ noteConnected(at);
87
+ return;
88
+ }
89
+ if (isMatrixDisconnectedSyncState(state)) {
90
+ noteDisconnected({ state, at, error });
91
+ return;
92
+ }
93
+ // Unknown future SDK states inherit the current connectivity bit until the
94
+ // SDK classifies them as ready or disconnected. Avoid guessing here.
95
+ status.lastEventAt = at;
96
+ status.healthState = state.toLowerCase();
97
+ emit();
98
+ },
99
+ noteUnexpectedError(error: unknown, at = Date.now()) {
100
+ noteDisconnected({ state: "ERROR", at, error });
101
+ },
102
+ markStopped(at = Date.now()) {
103
+ status.connected = false;
104
+ status.lastEventAt = at;
105
+ if (status.healthState !== "error") {
106
+ status.healthState = "stopped";
107
+ }
108
+ emit();
109
+ },
110
+ };
111
+ }