@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,294 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { describe, expect, it, vi } from "vitest";
5
+ import { ensureMatrixStartupVerification } from "./startup-verification.js";
6
+
7
+ function createTempStateDir(): string {
8
+ return fs.mkdtempSync(path.join(os.tmpdir(), "matrix-startup-verify-"));
9
+ }
10
+
11
+ function createStateFilePath(rootDir: string): string {
12
+ return path.join(rootDir, "startup-verification.json");
13
+ }
14
+
15
+ function createAuth(accountId = "default") {
16
+ return {
17
+ accountId,
18
+ homeserver: "https://matrix.example.org",
19
+ userId: "@bot:example.org",
20
+ accessToken: "token",
21
+ encryption: true,
22
+ };
23
+ }
24
+
25
+ type VerificationSummaryLike = {
26
+ id: string;
27
+ transactionId?: string;
28
+ isSelfVerification: boolean;
29
+ completed: boolean;
30
+ pending: boolean;
31
+ };
32
+
33
+ function createHarness(params?: {
34
+ verified?: boolean;
35
+ localVerified?: boolean;
36
+ crossSigningVerified?: boolean;
37
+ signedByOwner?: boolean;
38
+ requestVerification?: () => Promise<{ id: string; transactionId?: string }>;
39
+ listVerifications?: () => Promise<VerificationSummaryLike[]>;
40
+ }) {
41
+ const requestVerification =
42
+ params?.requestVerification ??
43
+ (async () => ({
44
+ id: "verification-1",
45
+ transactionId: "txn-1",
46
+ }));
47
+ const listVerifications = params?.listVerifications ?? (async () => []);
48
+ const getOwnDeviceVerificationStatus = vi.fn(async () => ({
49
+ encryptionEnabled: true,
50
+ userId: "@bot:example.org",
51
+ deviceId: "DEVICE123",
52
+ verified: params?.verified === true,
53
+ localVerified: params?.localVerified ?? params?.verified === true,
54
+ crossSigningVerified: params?.crossSigningVerified ?? params?.verified === true,
55
+ signedByOwner: params?.signedByOwner ?? params?.verified === true,
56
+ recoveryKeyStored: false,
57
+ recoveryKeyCreatedAt: null,
58
+ recoveryKeyId: null,
59
+ backupVersion: null,
60
+ backup: {
61
+ serverVersion: null,
62
+ activeVersion: null,
63
+ trusted: null,
64
+ matchesDecryptionKey: null,
65
+ decryptionKeyCached: null,
66
+ keyLoadAttempted: false,
67
+ keyLoadError: null,
68
+ },
69
+ }));
70
+ return {
71
+ client: {
72
+ getOwnDeviceVerificationStatus,
73
+ crypto: {
74
+ listVerifications: vi.fn(listVerifications),
75
+ requestVerification: vi.fn(requestVerification),
76
+ },
77
+ },
78
+ getOwnDeviceVerificationStatus,
79
+ };
80
+ }
81
+
82
+ describe("ensureMatrixStartupVerification", () => {
83
+ it("skips automatic requests when the device is already verified", async () => {
84
+ const tempHome = createTempStateDir();
85
+ const harness = createHarness({ verified: true });
86
+
87
+ const result = await ensureMatrixStartupVerification({
88
+ client: harness.client as never,
89
+ auth: createAuth(),
90
+ accountConfig: {},
91
+ stateFilePath: createStateFilePath(tempHome),
92
+ });
93
+
94
+ expect(result.kind).toBe("verified");
95
+ expect(harness.client.crypto.requestVerification).not.toHaveBeenCalled();
96
+ });
97
+
98
+ it("still requests startup verification when trust is only local", async () => {
99
+ const tempHome = createTempStateDir();
100
+ const harness = createHarness({
101
+ verified: false,
102
+ localVerified: true,
103
+ crossSigningVerified: false,
104
+ signedByOwner: false,
105
+ });
106
+
107
+ const result = await ensureMatrixStartupVerification({
108
+ client: harness.client as never,
109
+ auth: createAuth(),
110
+ accountConfig: {},
111
+ stateFilePath: createStateFilePath(tempHome),
112
+ });
113
+
114
+ expect(result.kind).toBe("requested");
115
+ expect(harness.client.crypto.requestVerification).toHaveBeenCalledWith({ ownUser: true });
116
+ });
117
+
118
+ it("skips automatic requests when a self verification is already pending", async () => {
119
+ const tempHome = createTempStateDir();
120
+ const harness = createHarness({
121
+ listVerifications: async () => [
122
+ {
123
+ id: "verification-1",
124
+ transactionId: "txn-1",
125
+ isSelfVerification: true,
126
+ completed: false,
127
+ pending: true,
128
+ },
129
+ ],
130
+ });
131
+
132
+ const result = await ensureMatrixStartupVerification({
133
+ client: harness.client as never,
134
+ auth: createAuth(),
135
+ accountConfig: {},
136
+ stateFilePath: createStateFilePath(tempHome),
137
+ });
138
+
139
+ expect(result.kind).toBe("pending");
140
+ expect(harness.client.crypto.requestVerification).not.toHaveBeenCalled();
141
+ });
142
+
143
+ it("respects the startup verification cooldown", async () => {
144
+ const tempHome = createTempStateDir();
145
+ const harness = createHarness();
146
+ const initialNowMs = Date.parse("2026-03-08T12:00:00.000Z");
147
+ await ensureMatrixStartupVerification({
148
+ client: harness.client as never,
149
+ auth: createAuth(),
150
+ accountConfig: {},
151
+ stateFilePath: createStateFilePath(tempHome),
152
+ nowMs: initialNowMs,
153
+ });
154
+ expect(harness.client.crypto.requestVerification).toHaveBeenCalledTimes(1);
155
+
156
+ const second = await ensureMatrixStartupVerification({
157
+ client: harness.client as never,
158
+ auth: createAuth(),
159
+ accountConfig: {},
160
+ stateFilePath: createStateFilePath(tempHome),
161
+ nowMs: initialNowMs + 60_000,
162
+ });
163
+
164
+ expect(second.kind).toBe("cooldown");
165
+ expect(harness.client.crypto.requestVerification).toHaveBeenCalledTimes(1);
166
+ });
167
+
168
+ it("supports disabling startup verification requests", async () => {
169
+ const tempHome = createTempStateDir();
170
+ const harness = createHarness();
171
+ const stateFilePath = createStateFilePath(tempHome);
172
+ fs.writeFileSync(stateFilePath, JSON.stringify({ attemptedAt: "2026-03-08T12:00:00.000Z" }));
173
+
174
+ const result = await ensureMatrixStartupVerification({
175
+ client: harness.client as never,
176
+ auth: createAuth(),
177
+ accountConfig: {
178
+ startupVerification: "off",
179
+ },
180
+ stateFilePath,
181
+ });
182
+
183
+ expect(result.kind).toBe("disabled");
184
+ expect(harness.client.crypto.requestVerification).not.toHaveBeenCalled();
185
+ expect(fs.existsSync(stateFilePath)).toBe(false);
186
+ });
187
+
188
+ it("persists a successful startup verification request", async () => {
189
+ const tempHome = createTempStateDir();
190
+ const harness = createHarness();
191
+
192
+ const result = await ensureMatrixStartupVerification({
193
+ client: harness.client as never,
194
+ auth: createAuth(),
195
+ accountConfig: {},
196
+ stateFilePath: createStateFilePath(tempHome),
197
+ nowMs: Date.parse("2026-03-08T12:00:00.000Z"),
198
+ });
199
+
200
+ expect(result.kind).toBe("requested");
201
+ expect(harness.client.crypto.requestVerification).toHaveBeenCalledWith({ ownUser: true });
202
+
203
+ expect(fs.existsSync(createStateFilePath(tempHome))).toBe(true);
204
+ });
205
+
206
+ it("keeps startup verification failures non-fatal", async () => {
207
+ const tempHome = createTempStateDir();
208
+ const harness = createHarness({
209
+ requestVerification: async () => {
210
+ throw new Error("no other verified session");
211
+ },
212
+ });
213
+
214
+ const result = await ensureMatrixStartupVerification({
215
+ client: harness.client as never,
216
+ auth: createAuth(),
217
+ accountConfig: {},
218
+ stateFilePath: createStateFilePath(tempHome),
219
+ });
220
+
221
+ expect(result.kind).toBe("request-failed");
222
+ if (result.kind !== "request-failed") {
223
+ throw new Error(`Unexpected startup verification result: ${result.kind}`);
224
+ }
225
+ expect(result.error).toContain("no other verified session");
226
+
227
+ const cooledDown = await ensureMatrixStartupVerification({
228
+ client: harness.client as never,
229
+ auth: createAuth(),
230
+ accountConfig: {},
231
+ stateFilePath: createStateFilePath(tempHome),
232
+ nowMs: Date.now() + 60_000,
233
+ });
234
+
235
+ expect(cooledDown.kind).toBe("cooldown");
236
+ });
237
+
238
+ it("retries failed startup verification requests sooner than successful ones", async () => {
239
+ const tempHome = createTempStateDir();
240
+ const stateFilePath = createStateFilePath(tempHome);
241
+ const failingHarness = createHarness({
242
+ requestVerification: async () => {
243
+ throw new Error("no other verified session");
244
+ },
245
+ });
246
+
247
+ await ensureMatrixStartupVerification({
248
+ client: failingHarness.client as never,
249
+ auth: createAuth(),
250
+ accountConfig: {},
251
+ stateFilePath,
252
+ nowMs: Date.parse("2026-03-08T12:00:00.000Z"),
253
+ });
254
+
255
+ const retryingHarness = createHarness();
256
+ const result = await ensureMatrixStartupVerification({
257
+ client: retryingHarness.client as never,
258
+ auth: createAuth(),
259
+ accountConfig: {},
260
+ stateFilePath,
261
+ nowMs: Date.parse("2026-03-08T13:30:00.000Z"),
262
+ });
263
+
264
+ expect(result.kind).toBe("requested");
265
+ expect(retryingHarness.client.crypto.requestVerification).toHaveBeenCalledTimes(1);
266
+ });
267
+
268
+ it("clears the persisted startup state after verification succeeds", async () => {
269
+ const tempHome = createTempStateDir();
270
+ const stateFilePath = createStateFilePath(tempHome);
271
+ const unverified = createHarness();
272
+
273
+ await ensureMatrixStartupVerification({
274
+ client: unverified.client as never,
275
+ auth: createAuth(),
276
+ accountConfig: {},
277
+ stateFilePath,
278
+ nowMs: Date.parse("2026-03-08T12:00:00.000Z"),
279
+ });
280
+
281
+ expect(fs.existsSync(stateFilePath)).toBe(true);
282
+
283
+ const verified = createHarness({ verified: true });
284
+ const result = await ensureMatrixStartupVerification({
285
+ client: verified.client as never,
286
+ auth: createAuth(),
287
+ accountConfig: {},
288
+ stateFilePath,
289
+ });
290
+
291
+ expect(result.kind).toBe("verified");
292
+ expect(fs.existsSync(stateFilePath)).toBe(false);
293
+ });
294
+ });
@@ -0,0 +1,237 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { readJsonFileWithFallback, writeJsonFileAtomically } from "openclaw/plugin-sdk/json-store";
4
+ import type { MatrixConfig } from "../../types.js";
5
+ import { resolveMatrixStoragePaths } from "../client/storage.js";
6
+ import type { MatrixAuth } from "../client/types.js";
7
+ import { formatMatrixErrorMessage } from "../errors.js";
8
+ import type { MatrixClient, MatrixOwnDeviceVerificationStatus } from "../sdk.js";
9
+
10
+ const STARTUP_VERIFICATION_STATE_FILENAME = "startup-verification.json";
11
+ const DEFAULT_STARTUP_VERIFICATION_MODE = "if-unverified" as const;
12
+ const DEFAULT_STARTUP_VERIFICATION_COOLDOWN_HOURS = 24;
13
+ const DEFAULT_STARTUP_VERIFICATION_FAILURE_COOLDOWN_MS = 60 * 60 * 1000;
14
+
15
+ type MatrixStartupVerificationState = {
16
+ userId?: string | null;
17
+ deviceId?: string | null;
18
+ attemptedAt?: string;
19
+ outcome?: "requested" | "failed";
20
+ requestId?: string;
21
+ transactionId?: string;
22
+ error?: string;
23
+ };
24
+
25
+ export type MatrixStartupVerificationOutcome =
26
+ | {
27
+ kind: "disabled" | "verified" | "cooldown" | "pending" | "requested" | "request-failed";
28
+ verification: MatrixOwnDeviceVerificationStatus;
29
+ requestId?: string;
30
+ transactionId?: string;
31
+ error?: string;
32
+ retryAfterMs?: number;
33
+ }
34
+ | {
35
+ kind: "unsupported";
36
+ verification?: undefined;
37
+ };
38
+
39
+ function normalizeCooldownHours(value: number | undefined): number {
40
+ if (typeof value !== "number" || !Number.isFinite(value)) {
41
+ return DEFAULT_STARTUP_VERIFICATION_COOLDOWN_HOURS;
42
+ }
43
+ return Math.max(0, value);
44
+ }
45
+
46
+ function resolveStartupVerificationStatePath(params: {
47
+ auth: MatrixAuth;
48
+ env?: NodeJS.ProcessEnv;
49
+ }): string {
50
+ const storagePaths = resolveMatrixStoragePaths({
51
+ homeserver: params.auth.homeserver,
52
+ userId: params.auth.userId,
53
+ accessToken: params.auth.accessToken,
54
+ accountId: params.auth.accountId,
55
+ deviceId: params.auth.deviceId,
56
+ env: params.env,
57
+ });
58
+ return path.join(storagePaths.rootDir, STARTUP_VERIFICATION_STATE_FILENAME);
59
+ }
60
+
61
+ async function readStartupVerificationState(
62
+ filePath: string,
63
+ ): Promise<MatrixStartupVerificationState | null> {
64
+ const { value } = await readJsonFileWithFallback<MatrixStartupVerificationState | null>(
65
+ filePath,
66
+ null,
67
+ );
68
+ return value && typeof value === "object" ? value : null;
69
+ }
70
+
71
+ async function clearStartupVerificationState(filePath: string): Promise<void> {
72
+ await fs.rm(filePath, { force: true }).catch(() => {});
73
+ }
74
+
75
+ function resolveStateCooldownMs(
76
+ state: MatrixStartupVerificationState | null,
77
+ cooldownMs: number,
78
+ ): number {
79
+ if (state?.outcome === "failed") {
80
+ return Math.min(cooldownMs, DEFAULT_STARTUP_VERIFICATION_FAILURE_COOLDOWN_MS);
81
+ }
82
+ return cooldownMs;
83
+ }
84
+
85
+ function resolveRetryAfterMs(params: {
86
+ attemptedAt?: string;
87
+ cooldownMs: number;
88
+ nowMs: number;
89
+ }): number | undefined {
90
+ const attemptedAtMs = Date.parse(params.attemptedAt ?? "");
91
+ if (!Number.isFinite(attemptedAtMs)) {
92
+ return undefined;
93
+ }
94
+ const remaining = attemptedAtMs + params.cooldownMs - params.nowMs;
95
+ return remaining > 0 ? remaining : undefined;
96
+ }
97
+
98
+ function shouldHonorCooldown(params: {
99
+ state: MatrixStartupVerificationState | null;
100
+ verification: MatrixOwnDeviceVerificationStatus;
101
+ stateCooldownMs: number;
102
+ nowMs: number;
103
+ }): boolean {
104
+ if (!params.state || params.stateCooldownMs <= 0) {
105
+ return false;
106
+ }
107
+ if (
108
+ params.state.userId &&
109
+ params.verification.userId &&
110
+ params.state.userId !== params.verification.userId
111
+ ) {
112
+ return false;
113
+ }
114
+ if (
115
+ params.state.deviceId &&
116
+ params.verification.deviceId &&
117
+ params.state.deviceId !== params.verification.deviceId
118
+ ) {
119
+ return false;
120
+ }
121
+ return (
122
+ resolveRetryAfterMs({
123
+ attemptedAt: params.state.attemptedAt,
124
+ cooldownMs: params.stateCooldownMs,
125
+ nowMs: params.nowMs,
126
+ }) !== undefined
127
+ );
128
+ }
129
+
130
+ function hasPendingSelfVerification(
131
+ verifications: Array<{
132
+ isSelfVerification: boolean;
133
+ completed: boolean;
134
+ pending: boolean;
135
+ }>,
136
+ ): boolean {
137
+ return verifications.some(
138
+ (entry) => entry.isSelfVerification && !entry.completed && entry.pending,
139
+ );
140
+ }
141
+
142
+ export async function ensureMatrixStartupVerification(params: {
143
+ client: Pick<MatrixClient, "crypto" | "getOwnDeviceVerificationStatus">;
144
+ auth: MatrixAuth;
145
+ accountConfig: Pick<MatrixConfig, "startupVerification" | "startupVerificationCooldownHours">;
146
+ env?: NodeJS.ProcessEnv;
147
+ nowMs?: number;
148
+ stateFilePath?: string;
149
+ }): Promise<MatrixStartupVerificationOutcome> {
150
+ if (params.auth.encryption !== true || !params.client.crypto) {
151
+ return { kind: "unsupported" };
152
+ }
153
+
154
+ const verification = await params.client.getOwnDeviceVerificationStatus();
155
+ const statePath =
156
+ params.stateFilePath ??
157
+ resolveStartupVerificationStatePath({
158
+ auth: params.auth,
159
+ env: params.env,
160
+ });
161
+
162
+ if (verification.verified) {
163
+ await clearStartupVerificationState(statePath);
164
+ return {
165
+ kind: "verified",
166
+ verification,
167
+ };
168
+ }
169
+
170
+ const mode = params.accountConfig.startupVerification ?? DEFAULT_STARTUP_VERIFICATION_MODE;
171
+ if (mode === "off") {
172
+ await clearStartupVerificationState(statePath);
173
+ return {
174
+ kind: "disabled",
175
+ verification,
176
+ };
177
+ }
178
+
179
+ const verifications = await params.client.crypto.listVerifications().catch(() => []);
180
+ if (hasPendingSelfVerification(verifications)) {
181
+ return {
182
+ kind: "pending",
183
+ verification,
184
+ };
185
+ }
186
+
187
+ const cooldownHours = normalizeCooldownHours(
188
+ params.accountConfig.startupVerificationCooldownHours,
189
+ );
190
+ const cooldownMs = cooldownHours * 60 * 60 * 1000;
191
+ const nowMs = params.nowMs ?? Date.now();
192
+ const state = await readStartupVerificationState(statePath);
193
+ const stateCooldownMs = resolveStateCooldownMs(state, cooldownMs);
194
+ if (shouldHonorCooldown({ state, verification, stateCooldownMs, nowMs })) {
195
+ return {
196
+ kind: "cooldown",
197
+ verification,
198
+ retryAfterMs: resolveRetryAfterMs({
199
+ attemptedAt: state?.attemptedAt,
200
+ cooldownMs: stateCooldownMs,
201
+ nowMs,
202
+ }),
203
+ };
204
+ }
205
+
206
+ try {
207
+ const request = await params.client.crypto.requestVerification({ ownUser: true });
208
+ await writeJsonFileAtomically(statePath, {
209
+ userId: verification.userId,
210
+ deviceId: verification.deviceId,
211
+ attemptedAt: new Date(nowMs).toISOString(),
212
+ outcome: "requested",
213
+ requestId: request.id,
214
+ transactionId: request.transactionId,
215
+ } satisfies MatrixStartupVerificationState);
216
+ return {
217
+ kind: "requested",
218
+ verification,
219
+ requestId: request.id,
220
+ transactionId: request.transactionId ?? undefined,
221
+ };
222
+ } catch (err) {
223
+ const error = formatMatrixErrorMessage(err);
224
+ await writeJsonFileAtomically(statePath, {
225
+ userId: verification.userId,
226
+ deviceId: verification.deviceId,
227
+ attemptedAt: new Date(nowMs).toISOString(),
228
+ outcome: "failed",
229
+ error,
230
+ } satisfies MatrixStartupVerificationState).catch(() => {});
231
+ return {
232
+ kind: "request-failed",
233
+ verification,
234
+ error,
235
+ };
236
+ }
237
+ }