@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,544 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { normalizeAccountId } from "openclaw/plugin-sdk/account-id";
5
+ import {
6
+ requiresExplicitMatrixDefaultAccount,
7
+ resolveMatrixDefaultOrOnlyAccountId,
8
+ } from "../../account-selection.js";
9
+ import { getMatrixRuntime } from "../../runtime.js";
10
+ import {
11
+ resolveMatrixAccountStorageRoot,
12
+ resolveMatrixLegacyFlatStoragePaths,
13
+ } from "../../storage-paths.js";
14
+ import type { MatrixAuth } from "./types.js";
15
+ import type { MatrixStoragePaths } from "./types.js";
16
+
17
+ export const DEFAULT_ACCOUNT_KEY = "default";
18
+ const STORAGE_META_FILENAME = "storage-meta.json";
19
+ const THREAD_BINDINGS_FILENAME = "thread-bindings.json";
20
+ const LEGACY_CRYPTO_MIGRATION_FILENAME = "legacy-crypto-migration.json";
21
+ const RECOVERY_KEY_FILENAME = "recovery-key.json";
22
+ const IDB_SNAPSHOT_FILENAME = "crypto-idb-snapshot.json";
23
+ const STARTUP_VERIFICATION_FILENAME = "startup-verification.json";
24
+
25
+ type LegacyMoveRecord = {
26
+ sourcePath: string;
27
+ targetPath: string;
28
+ label: string;
29
+ };
30
+
31
+ type StoredRootMetadata = {
32
+ homeserver?: string;
33
+ userId?: string;
34
+ accountId?: string;
35
+ accessTokenHash?: string;
36
+ deviceId?: string | null;
37
+ currentTokenStateClaimed?: boolean;
38
+ createdAt?: string;
39
+ };
40
+
41
+ function resolveLegacyStoragePaths(env: NodeJS.ProcessEnv = process.env): {
42
+ storagePath: string;
43
+ cryptoPath: string;
44
+ } {
45
+ const stateDir = getMatrixRuntime().state.resolveStateDir(env, os.homedir);
46
+ const legacy = resolveMatrixLegacyFlatStoragePaths(stateDir);
47
+ return { storagePath: legacy.storagePath, cryptoPath: legacy.cryptoPath };
48
+ }
49
+
50
+ function assertLegacyMigrationAccountSelection(params: { accountKey: string }): void {
51
+ const cfg = getMatrixRuntime().config.loadConfig();
52
+ if (!cfg.channels?.matrix || typeof cfg.channels.lobi !== "object") {
53
+ return;
54
+ }
55
+ if (requiresExplicitMatrixDefaultAccount(cfg)) {
56
+ throw new Error(
57
+ "Legacy Matrix client storage cannot be migrated automatically because multiple Matrix accounts are configured and channels.lobi.defaultAccount is not set.",
58
+ );
59
+ }
60
+
61
+ const selectedAccountId = normalizeAccountId(resolveMatrixDefaultOrOnlyAccountId(cfg));
62
+ const currentAccountId = normalizeAccountId(params.accountKey);
63
+ if (selectedAccountId !== currentAccountId) {
64
+ throw new Error(
65
+ `Legacy Matrix client storage targets account "${selectedAccountId}", but the current client is starting account "${currentAccountId}". Start the selected account first so flat legacy storage is not migrated into the wrong account directory.`,
66
+ );
67
+ }
68
+ }
69
+
70
+ function scoreStorageRoot(rootDir: string): number {
71
+ let score = 0;
72
+ if (fs.existsSync(path.join(rootDir, "bot-storage.json"))) {
73
+ score += 8;
74
+ }
75
+ if (fs.existsSync(path.join(rootDir, "crypto"))) {
76
+ score += 8;
77
+ }
78
+ if (fs.existsSync(path.join(rootDir, THREAD_BINDINGS_FILENAME))) {
79
+ score += 4;
80
+ }
81
+ if (fs.existsSync(path.join(rootDir, LEGACY_CRYPTO_MIGRATION_FILENAME))) {
82
+ score += 3;
83
+ }
84
+ if (fs.existsSync(path.join(rootDir, RECOVERY_KEY_FILENAME))) {
85
+ score += 2;
86
+ }
87
+ if (fs.existsSync(path.join(rootDir, IDB_SNAPSHOT_FILENAME))) {
88
+ score += 2;
89
+ }
90
+ if (fs.existsSync(path.join(rootDir, STORAGE_META_FILENAME))) {
91
+ score += 1;
92
+ }
93
+ return score;
94
+ }
95
+
96
+ function resolveStorageRootMtimeMs(rootDir: string): number {
97
+ try {
98
+ return fs.statSync(rootDir).mtimeMs;
99
+ } catch {
100
+ return 0;
101
+ }
102
+ }
103
+
104
+ function readStoredRootMetadata(rootDir: string): StoredRootMetadata {
105
+ const metadata: StoredRootMetadata = {};
106
+
107
+ try {
108
+ const parsed = JSON.parse(
109
+ fs.readFileSync(path.join(rootDir, STORAGE_META_FILENAME), "utf8"),
110
+ ) as Partial<StoredRootMetadata>;
111
+ if (typeof parsed.homeserver === "string" && parsed.homeserver.trim()) {
112
+ metadata.homeserver = parsed.homeserver.trim();
113
+ }
114
+ if (typeof parsed.userId === "string" && parsed.userId.trim()) {
115
+ metadata.userId = parsed.userId.trim();
116
+ }
117
+ if (typeof parsed.accountId === "string" && parsed.accountId.trim()) {
118
+ metadata.accountId = parsed.accountId.trim();
119
+ }
120
+ if (typeof parsed.accessTokenHash === "string" && parsed.accessTokenHash.trim()) {
121
+ metadata.accessTokenHash = parsed.accessTokenHash.trim();
122
+ }
123
+ if (typeof parsed.deviceId === "string" && parsed.deviceId.trim()) {
124
+ metadata.deviceId = parsed.deviceId.trim();
125
+ }
126
+ if (parsed.currentTokenStateClaimed === true) {
127
+ metadata.currentTokenStateClaimed = true;
128
+ }
129
+ if (typeof parsed.createdAt === "string" && parsed.createdAt.trim()) {
130
+ metadata.createdAt = parsed.createdAt.trim();
131
+ }
132
+ } catch {
133
+ // ignore missing or malformed storage metadata
134
+ }
135
+
136
+ try {
137
+ const parsed = JSON.parse(
138
+ fs.readFileSync(path.join(rootDir, STARTUP_VERIFICATION_FILENAME), "utf8"),
139
+ ) as { deviceId?: unknown };
140
+ if (!metadata.deviceId && typeof parsed.deviceId === "string" && parsed.deviceId.trim()) {
141
+ metadata.deviceId = parsed.deviceId.trim();
142
+ }
143
+ } catch {
144
+ // ignore missing or malformed verification state
145
+ }
146
+
147
+ return metadata;
148
+ }
149
+
150
+ function isCompatibleStorageRoot(params: {
151
+ candidateRootDir: string;
152
+ homeserver: string;
153
+ userId: string;
154
+ accountKey: string;
155
+ deviceId?: string | null;
156
+ requireExplicitDeviceMatch?: boolean;
157
+ }): boolean {
158
+ const metadata = readStoredRootMetadata(params.candidateRootDir);
159
+ if (metadata.homeserver && metadata.homeserver !== params.homeserver) {
160
+ return false;
161
+ }
162
+ if (metadata.userId && metadata.userId !== params.userId) {
163
+ return false;
164
+ }
165
+ if (
166
+ metadata.accountId &&
167
+ normalizeAccountId(metadata.accountId) !== normalizeAccountId(params.accountKey)
168
+ ) {
169
+ return false;
170
+ }
171
+ if (
172
+ params.deviceId &&
173
+ metadata.deviceId &&
174
+ metadata.deviceId.trim() &&
175
+ metadata.deviceId.trim() !== params.deviceId.trim()
176
+ ) {
177
+ return false;
178
+ }
179
+ if (
180
+ params.requireExplicitDeviceMatch &&
181
+ params.deviceId &&
182
+ (!metadata.deviceId || metadata.deviceId.trim() !== params.deviceId.trim())
183
+ ) {
184
+ return false;
185
+ }
186
+ return true;
187
+ }
188
+
189
+ function resolvePreferredMatrixStorageRoot(params: {
190
+ canonicalRootDir: string;
191
+ canonicalTokenHash: string;
192
+ homeserver: string;
193
+ userId: string;
194
+ accountKey: string;
195
+ deviceId?: string | null;
196
+ }): {
197
+ rootDir: string;
198
+ tokenHash: string;
199
+ } {
200
+ const parentDir = path.dirname(params.canonicalRootDir);
201
+ const bestCurrentScore = scoreStorageRoot(params.canonicalRootDir);
202
+ let best = {
203
+ rootDir: params.canonicalRootDir,
204
+ tokenHash: params.canonicalTokenHash,
205
+ score: bestCurrentScore,
206
+ mtimeMs: resolveStorageRootMtimeMs(params.canonicalRootDir),
207
+ };
208
+
209
+ // Without a confirmed device identity, reusing a populated sibling root after
210
+ // token rotation can silently bind this run to the wrong Matrix device state.
211
+ if (!params.deviceId?.trim()) {
212
+ return {
213
+ rootDir: best.rootDir,
214
+ tokenHash: best.tokenHash,
215
+ };
216
+ }
217
+
218
+ const canonicalMetadata = readStoredRootMetadata(params.canonicalRootDir);
219
+ if (
220
+ canonicalMetadata.accessTokenHash === params.canonicalTokenHash &&
221
+ canonicalMetadata.deviceId?.trim() === params.deviceId.trim() &&
222
+ canonicalMetadata.currentTokenStateClaimed === true
223
+ ) {
224
+ return {
225
+ rootDir: best.rootDir,
226
+ tokenHash: best.tokenHash,
227
+ };
228
+ }
229
+
230
+ let siblingEntries: fs.Dirent[] = [];
231
+ try {
232
+ siblingEntries = fs.readdirSync(parentDir, { withFileTypes: true });
233
+ } catch {
234
+ return {
235
+ rootDir: best.rootDir,
236
+ tokenHash: best.tokenHash,
237
+ };
238
+ }
239
+
240
+ for (const entry of siblingEntries) {
241
+ if (!entry.isDirectory()) {
242
+ continue;
243
+ }
244
+ if (entry.name === params.canonicalTokenHash) {
245
+ continue;
246
+ }
247
+ const candidateRootDir = path.join(parentDir, entry.name);
248
+ if (
249
+ !isCompatibleStorageRoot({
250
+ candidateRootDir,
251
+ homeserver: params.homeserver,
252
+ userId: params.userId,
253
+ accountKey: params.accountKey,
254
+ deviceId: params.deviceId,
255
+ // Once auth resolves a concrete device, only sibling roots that explicitly
256
+ // declare that same device are safe to reuse across token rotations.
257
+ requireExplicitDeviceMatch: Boolean(params.deviceId),
258
+ })
259
+ ) {
260
+ continue;
261
+ }
262
+ const candidateScore = scoreStorageRoot(candidateRootDir);
263
+ if (candidateScore <= 0) {
264
+ continue;
265
+ }
266
+ const candidateMtimeMs = resolveStorageRootMtimeMs(candidateRootDir);
267
+ if (
268
+ candidateScore > best.score ||
269
+ (best.rootDir !== params.canonicalRootDir &&
270
+ candidateScore === best.score &&
271
+ candidateMtimeMs > best.mtimeMs)
272
+ ) {
273
+ best = {
274
+ rootDir: candidateRootDir,
275
+ tokenHash: entry.name,
276
+ score: candidateScore,
277
+ mtimeMs: candidateMtimeMs,
278
+ };
279
+ }
280
+ }
281
+
282
+ return {
283
+ rootDir: best.rootDir,
284
+ tokenHash: best.tokenHash,
285
+ };
286
+ }
287
+
288
+ export function resolveMatrixStoragePaths(params: {
289
+ homeserver: string;
290
+ userId: string;
291
+ accessToken: string;
292
+ accountId?: string | null;
293
+ deviceId?: string | null;
294
+ env?: NodeJS.ProcessEnv;
295
+ stateDir?: string;
296
+ }): MatrixStoragePaths {
297
+ const env = params.env ?? process.env;
298
+ const stateDir = params.stateDir ?? getMatrixRuntime().state.resolveStateDir(env, os.homedir);
299
+ const canonical = resolveMatrixAccountStorageRoot({
300
+ stateDir,
301
+ homeserver: params.homeserver,
302
+ userId: params.userId,
303
+ accessToken: params.accessToken,
304
+ accountId: params.accountId,
305
+ });
306
+ const { rootDir, tokenHash } = resolvePreferredMatrixStorageRoot({
307
+ canonicalRootDir: canonical.rootDir,
308
+ canonicalTokenHash: canonical.tokenHash,
309
+ homeserver: params.homeserver,
310
+ userId: params.userId,
311
+ accountKey: canonical.accountKey,
312
+ deviceId: params.deviceId,
313
+ });
314
+ return {
315
+ rootDir,
316
+ storagePath: path.join(rootDir, "bot-storage.json"),
317
+ cryptoPath: path.join(rootDir, "crypto"),
318
+ metaPath: path.join(rootDir, STORAGE_META_FILENAME),
319
+ recoveryKeyPath: path.join(rootDir, "recovery-key.json"),
320
+ idbSnapshotPath: path.join(rootDir, IDB_SNAPSHOT_FILENAME),
321
+ accountKey: canonical.accountKey,
322
+ tokenHash,
323
+ };
324
+ }
325
+
326
+ export function resolveMatrixStateFilePath(params: {
327
+ auth: MatrixAuth;
328
+ filename: string;
329
+ accountId?: string | null;
330
+ env?: NodeJS.ProcessEnv;
331
+ stateDir?: string;
332
+ }): string {
333
+ const storagePaths = resolveMatrixStoragePaths({
334
+ homeserver: params.auth.homeserver,
335
+ userId: params.auth.userId,
336
+ accessToken: params.auth.accessToken,
337
+ accountId: params.accountId ?? params.auth.accountId,
338
+ deviceId: params.auth.deviceId,
339
+ env: params.env,
340
+ stateDir: params.stateDir,
341
+ });
342
+ return path.join(storagePaths.rootDir, params.filename);
343
+ }
344
+
345
+ export async function maybeMigrateLegacyStorage(params: {
346
+ storagePaths: MatrixStoragePaths;
347
+ env?: NodeJS.ProcessEnv;
348
+ }): Promise<void> {
349
+ const legacy = resolveLegacyStoragePaths(params.env);
350
+ const hasLegacyStorage = fs.existsSync(legacy.storagePath);
351
+ const hasLegacyCrypto = fs.existsSync(legacy.cryptoPath);
352
+ if (!hasLegacyStorage && !hasLegacyCrypto) {
353
+ return;
354
+ }
355
+ const hasTargetStorage = fs.existsSync(params.storagePaths.storagePath);
356
+ const hasTargetCrypto = fs.existsSync(params.storagePaths.cryptoPath);
357
+ // Continue partial migrations one artifact at a time; only skip items whose targets already exist.
358
+ const shouldMigrateStorage = hasLegacyStorage && !hasTargetStorage;
359
+ const shouldMigrateCrypto = hasLegacyCrypto && !hasTargetCrypto;
360
+ if (!shouldMigrateStorage && !shouldMigrateCrypto) {
361
+ return;
362
+ }
363
+
364
+ assertLegacyMigrationAccountSelection({
365
+ accountKey: params.storagePaths.accountKey,
366
+ });
367
+
368
+ const logger = getMatrixRuntime().logging.getChildLogger({ module: "matrix-storage" });
369
+ const { maybeCreateMatrixMigrationSnapshot } = await import("./migration-snapshot.runtime.js");
370
+ await maybeCreateMatrixMigrationSnapshot({
371
+ trigger: "matrix-client-fallback",
372
+ env: params.env,
373
+ log: logger,
374
+ });
375
+ fs.mkdirSync(params.storagePaths.rootDir, { recursive: true });
376
+ const moved: LegacyMoveRecord[] = [];
377
+ const skippedExistingTargets: string[] = [];
378
+ try {
379
+ if (shouldMigrateStorage) {
380
+ moveLegacyStoragePathOrThrow({
381
+ sourcePath: legacy.storagePath,
382
+ targetPath: params.storagePaths.storagePath,
383
+ label: "sync store",
384
+ moved,
385
+ });
386
+ } else if (hasLegacyStorage) {
387
+ skippedExistingTargets.push(
388
+ `- sync store remains at ${legacy.storagePath} because ${params.storagePaths.storagePath} already exists`,
389
+ );
390
+ }
391
+ if (shouldMigrateCrypto) {
392
+ moveLegacyStoragePathOrThrow({
393
+ sourcePath: legacy.cryptoPath,
394
+ targetPath: params.storagePaths.cryptoPath,
395
+ label: "crypto store",
396
+ moved,
397
+ });
398
+ } else if (hasLegacyCrypto) {
399
+ skippedExistingTargets.push(
400
+ `- crypto store remains at ${legacy.cryptoPath} because ${params.storagePaths.cryptoPath} already exists`,
401
+ );
402
+ }
403
+ } catch (err) {
404
+ const rollbackError = rollbackLegacyMoves(moved);
405
+ throw new Error(
406
+ rollbackError
407
+ ? `Failed migrating legacy Matrix client storage: ${String(err)}. Rollback also failed: ${rollbackError}`
408
+ : `Failed migrating legacy Matrix client storage: ${String(err)}`,
409
+ { cause: err },
410
+ );
411
+ }
412
+ if (moved.length > 0) {
413
+ logger.info(
414
+ `matrix: migrated legacy client storage into ${params.storagePaths.rootDir}\n${moved
415
+ .map((entry) => `- ${entry.label}: ${entry.sourcePath} -> ${entry.targetPath}`)
416
+ .join("\n")}`,
417
+ );
418
+ }
419
+ if (skippedExistingTargets.length > 0) {
420
+ logger.warn?.(
421
+ `matrix: legacy client storage still exists in the flat path because some account-scoped targets already existed.\n${skippedExistingTargets.join("\n")}`,
422
+ );
423
+ }
424
+ }
425
+
426
+ function moveLegacyStoragePathOrThrow(params: {
427
+ sourcePath: string;
428
+ targetPath: string;
429
+ label: string;
430
+ moved: LegacyMoveRecord[];
431
+ }): void {
432
+ if (!fs.existsSync(params.sourcePath)) {
433
+ return;
434
+ }
435
+ if (fs.existsSync(params.targetPath)) {
436
+ throw new Error(
437
+ `legacy Matrix ${params.label} target already exists (${params.targetPath}); refusing to overwrite it automatically`,
438
+ );
439
+ }
440
+ fs.renameSync(params.sourcePath, params.targetPath);
441
+ params.moved.push({
442
+ sourcePath: params.sourcePath,
443
+ targetPath: params.targetPath,
444
+ label: params.label,
445
+ });
446
+ }
447
+
448
+ function rollbackLegacyMoves(moved: LegacyMoveRecord[]): string | null {
449
+ for (const entry of moved.toReversed()) {
450
+ try {
451
+ if (!fs.existsSync(entry.targetPath) || fs.existsSync(entry.sourcePath)) {
452
+ continue;
453
+ }
454
+ fs.renameSync(entry.targetPath, entry.sourcePath);
455
+ } catch (err) {
456
+ return `${entry.label} (${entry.targetPath} -> ${entry.sourcePath}): ${String(err)}`;
457
+ }
458
+ }
459
+ return null;
460
+ }
461
+
462
+ function writeStoredRootMetadata(
463
+ metaPath: string,
464
+ payload: {
465
+ homeserver?: string;
466
+ userId?: string;
467
+ accountId: string;
468
+ accessTokenHash?: string;
469
+ deviceId: string | null;
470
+ currentTokenStateClaimed: boolean;
471
+ createdAt: string;
472
+ },
473
+ ): boolean {
474
+ try {
475
+ fs.mkdirSync(path.dirname(metaPath), { recursive: true });
476
+ fs.writeFileSync(metaPath, JSON.stringify(payload, null, 2), "utf-8");
477
+ return true;
478
+ } catch {
479
+ return false;
480
+ }
481
+ }
482
+
483
+ export function writeStorageMeta(params: {
484
+ storagePaths: MatrixStoragePaths;
485
+ homeserver: string;
486
+ userId: string;
487
+ accountId?: string | null;
488
+ deviceId?: string | null;
489
+ currentTokenStateClaimed?: boolean;
490
+ }): boolean {
491
+ const existing = readStoredRootMetadata(params.storagePaths.rootDir);
492
+ return writeStoredRootMetadata(params.storagePaths.metaPath, {
493
+ homeserver: params.homeserver,
494
+ userId: params.userId,
495
+ accountId: params.accountId ?? DEFAULT_ACCOUNT_KEY,
496
+ accessTokenHash: params.storagePaths.tokenHash,
497
+ deviceId: params.deviceId ?? null,
498
+ currentTokenStateClaimed:
499
+ params.currentTokenStateClaimed ?? existing.currentTokenStateClaimed === true,
500
+ createdAt: existing.createdAt ?? new Date().toISOString(),
501
+ });
502
+ }
503
+
504
+ export function claimCurrentTokenStorageState(params: { rootDir: string }): boolean {
505
+ const metadata = readStoredRootMetadata(params.rootDir);
506
+ if (!metadata.accessTokenHash?.trim()) {
507
+ return false;
508
+ }
509
+ return writeStoredRootMetadata(path.join(params.rootDir, STORAGE_META_FILENAME), {
510
+ homeserver: metadata.homeserver,
511
+ userId: metadata.userId,
512
+ accountId: metadata.accountId ?? DEFAULT_ACCOUNT_KEY,
513
+ accessTokenHash: metadata.accessTokenHash,
514
+ deviceId: metadata.deviceId ?? null,
515
+ currentTokenStateClaimed: true,
516
+ createdAt: metadata.createdAt ?? new Date().toISOString(),
517
+ });
518
+ }
519
+
520
+ export function repairCurrentTokenStorageMetaDeviceId(params: {
521
+ homeserver: string;
522
+ userId: string;
523
+ accessToken: string;
524
+ accountId?: string | null;
525
+ deviceId: string;
526
+ env?: NodeJS.ProcessEnv;
527
+ stateDir?: string;
528
+ }): boolean {
529
+ const storagePaths = resolveMatrixStoragePaths({
530
+ homeserver: params.homeserver,
531
+ userId: params.userId,
532
+ accessToken: params.accessToken,
533
+ accountId: params.accountId,
534
+ env: params.env,
535
+ stateDir: params.stateDir,
536
+ });
537
+ return writeStorageMeta({
538
+ storagePaths,
539
+ homeserver: params.homeserver,
540
+ userId: params.userId,
541
+ accountId: params.accountId,
542
+ deviceId: params.deviceId,
543
+ });
544
+ }
@@ -0,0 +1,50 @@
1
+ import type { PinnedDispatcherPolicy } from "openclaw/plugin-sdk/infra-runtime";
2
+ import type { SsrFPolicy } from "../../runtime-api.js";
3
+
4
+ export type MatrixResolvedConfig = {
5
+ homeserver: string;
6
+ userId: string;
7
+ accessToken?: string;
8
+ deviceId?: string;
9
+ password?: string;
10
+ deviceName?: string;
11
+ initialSyncLimit?: number;
12
+ encryption?: boolean;
13
+ allowPrivateNetwork?: boolean;
14
+ ssrfPolicy?: SsrFPolicy;
15
+ dispatcherPolicy?: PinnedDispatcherPolicy;
16
+ };
17
+
18
+ /**
19
+ * Authenticated Matrix configuration.
20
+ * Note: deviceId is NOT included here because it's implicit in the accessToken.
21
+ * Matrix storage reuses the most complete account-scoped root it can find for the
22
+ * same homeserver/user/account tuple so token refreshes do not strand prior state.
23
+ * If the device identity itself changes or crypto storage is lost, crypto state may
24
+ * still need to be recreated together with the new access token.
25
+ */
26
+ export type MatrixAuth = {
27
+ accountId: string;
28
+ homeserver: string;
29
+ userId: string;
30
+ accessToken: string;
31
+ password?: string;
32
+ deviceId?: string;
33
+ deviceName?: string;
34
+ initialSyncLimit?: number;
35
+ encryption?: boolean;
36
+ allowPrivateNetwork?: boolean;
37
+ ssrfPolicy?: SsrFPolicy;
38
+ dispatcherPolicy?: PinnedDispatcherPolicy;
39
+ };
40
+
41
+ export type MatrixStoragePaths = {
42
+ rootDir: string;
43
+ storagePath: string;
44
+ cryptoPath: string;
45
+ metaPath: string;
46
+ recoveryKeyPath: string;
47
+ idbSnapshotPath: string;
48
+ accountKey: string;
49
+ tokenHash: string;
50
+ };
@@ -0,0 +1,84 @@
1
+ import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import {
3
+ createMockMatrixClient,
4
+ matrixClientResolverMocks,
5
+ primeMatrixClientResolverMocks,
6
+ } from "./client-resolver.test-helpers.js";
7
+
8
+ const {
9
+ getMatrixRuntimeMock,
10
+ getActiveMatrixClientMock,
11
+ acquireSharedMatrixClientMock,
12
+ releaseSharedClientInstanceMock,
13
+ isBunRuntimeMock,
14
+ resolveMatrixAuthContextMock,
15
+ } = matrixClientResolverMocks;
16
+
17
+ vi.mock("../runtime.js", () => ({
18
+ getMatrixRuntime: () => getMatrixRuntimeMock(),
19
+ }));
20
+
21
+ vi.mock("./active-client.js", () => ({
22
+ getActiveMatrixClient: (...args: unknown[]) => getActiveMatrixClientMock(...args),
23
+ }));
24
+
25
+ vi.mock("./client.js", () => ({
26
+ acquireSharedMatrixClient: (...args: unknown[]) => acquireSharedMatrixClientMock(...args),
27
+ isBunRuntime: () => isBunRuntimeMock(),
28
+ resolveMatrixAuthContext: resolveMatrixAuthContextMock,
29
+ }));
30
+
31
+ vi.mock("./client/shared.js", () => ({
32
+ releaseSharedClientInstance: (...args: unknown[]) => releaseSharedClientInstanceMock(...args),
33
+ }));
34
+
35
+ let resolveRuntimeMatrixClientWithReadiness: typeof import("./client-bootstrap.js").resolveRuntimeMatrixClientWithReadiness;
36
+ let withResolvedRuntimeMatrixClient: typeof import("./client-bootstrap.js").withResolvedRuntimeMatrixClient;
37
+
38
+ describe("client bootstrap", () => {
39
+ beforeAll(async () => {
40
+ ({ resolveRuntimeMatrixClientWithReadiness, withResolvedRuntimeMatrixClient } =
41
+ await import("./client-bootstrap.js"));
42
+ });
43
+
44
+ beforeEach(() => {
45
+ primeMatrixClientResolverMocks({ resolved: {} });
46
+ });
47
+
48
+ afterEach(() => {
49
+ vi.unstubAllEnvs();
50
+ });
51
+
52
+ it("releases leased shared clients when readiness setup fails", async () => {
53
+ const sharedClient = createMockMatrixClient();
54
+ vi.mocked(sharedClient.prepareForOneOff).mockRejectedValue(new Error("prepare failed"));
55
+ acquireSharedMatrixClientMock.mockResolvedValue(sharedClient);
56
+
57
+ await expect(
58
+ resolveRuntimeMatrixClientWithReadiness({
59
+ accountId: "default",
60
+ readiness: "prepared",
61
+ }),
62
+ ).rejects.toThrow("prepare failed");
63
+
64
+ expect(releaseSharedClientInstanceMock).toHaveBeenCalledWith(sharedClient, "stop");
65
+ });
66
+
67
+ it("releases leased shared clients when the wrapped action throws during readiness", async () => {
68
+ const sharedClient = createMockMatrixClient();
69
+ vi.mocked(sharedClient.start).mockRejectedValue(new Error("start failed"));
70
+ acquireSharedMatrixClientMock.mockResolvedValue(sharedClient);
71
+
72
+ await expect(
73
+ withResolvedRuntimeMatrixClient(
74
+ {
75
+ accountId: "default",
76
+ readiness: "started",
77
+ },
78
+ async () => "ok",
79
+ ),
80
+ ).rejects.toThrow("start failed");
81
+
82
+ expect(releaseSharedClientInstanceMock).toHaveBeenCalledWith(sharedClient, "stop");
83
+ });
84
+ });