@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,385 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { encodeRecoveryKey } from "@archipelagolab/lobi-js-sdk/lib/crypto-api/recovery-key.js";
5
+ import { beforeEach, describe, expect, it, vi } from "vitest";
6
+ import { MatrixRecoveryKeyStore } from "./recovery-key-store.js";
7
+ import type { MatrixCryptoBootstrapApi, MatrixSecretStorageStatus } from "./types.js";
8
+
9
+ function createTempRecoveryKeyPath(): string {
10
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "matrix-recovery-key-store-"));
11
+ return path.join(dir, "recovery-key.json");
12
+ }
13
+
14
+ function createGeneratedRecoveryKey(params: {
15
+ keyId: string;
16
+ name: string;
17
+ bytes: number[];
18
+ encodedPrivateKey: string;
19
+ }) {
20
+ return {
21
+ keyId: params.keyId,
22
+ keyInfo: { name: params.name },
23
+ privateKey: new Uint8Array(params.bytes),
24
+ encodedPrivateKey: params.encodedPrivateKey,
25
+ };
26
+ }
27
+
28
+ function createBootstrapSecretStorageMock(errorMessage?: string) {
29
+ return vi.fn(
30
+ async (opts?: {
31
+ setupNewSecretStorage?: boolean;
32
+ createSecretStorageKey?: () => Promise<unknown>;
33
+ }) => {
34
+ if (opts?.setupNewSecretStorage || !errorMessage) {
35
+ await opts?.createSecretStorageKey?.();
36
+ return;
37
+ }
38
+ throw new Error(errorMessage);
39
+ },
40
+ );
41
+ }
42
+
43
+ function createRecoveryKeyCrypto(params: {
44
+ bootstrapSecretStorage: ReturnType<typeof vi.fn>;
45
+ createRecoveryKeyFromPassphrase: ReturnType<typeof vi.fn>;
46
+ status: MatrixSecretStorageStatus;
47
+ }): MatrixCryptoBootstrapApi {
48
+ return {
49
+ on: vi.fn(),
50
+ bootstrapCrossSigning: vi.fn(async () => {}),
51
+ bootstrapSecretStorage: params.bootstrapSecretStorage,
52
+ createRecoveryKeyFromPassphrase: params.createRecoveryKeyFromPassphrase,
53
+ getSecretStorageStatus: vi.fn(async () => params.status),
54
+ requestOwnUserVerification: vi.fn(async () => null),
55
+ } as unknown as MatrixCryptoBootstrapApi;
56
+ }
57
+
58
+ async function runSecretStorageBootstrapScenario(params: {
59
+ generated: ReturnType<typeof createGeneratedRecoveryKey>;
60
+ status: MatrixSecretStorageStatus;
61
+ allowSecretStorageRecreateWithoutRecoveryKey?: boolean;
62
+ firstBootstrapError?: string;
63
+ }) {
64
+ const recoveryKeyPath = createTempRecoveryKeyPath();
65
+ const store = new MatrixRecoveryKeyStore(recoveryKeyPath);
66
+ const createRecoveryKeyFromPassphrase = vi.fn(async () => params.generated);
67
+ const bootstrapSecretStorage = createBootstrapSecretStorageMock(params.firstBootstrapError);
68
+ const crypto = createRecoveryKeyCrypto({
69
+ bootstrapSecretStorage,
70
+ createRecoveryKeyFromPassphrase,
71
+ status: params.status,
72
+ });
73
+
74
+ await store.bootstrapSecretStorageWithRecoveryKey(crypto, {
75
+ allowSecretStorageRecreateWithoutRecoveryKey:
76
+ params.allowSecretStorageRecreateWithoutRecoveryKey ?? false,
77
+ });
78
+
79
+ return {
80
+ store,
81
+ createRecoveryKeyFromPassphrase,
82
+ bootstrapSecretStorage,
83
+ };
84
+ }
85
+
86
+ describe("MatrixRecoveryKeyStore", () => {
87
+ beforeEach(() => {
88
+ vi.restoreAllMocks();
89
+ });
90
+
91
+ it("loads a stored recovery key for requested secret-storage keys", async () => {
92
+ const recoveryKeyPath = createTempRecoveryKeyPath();
93
+ fs.writeFileSync(
94
+ recoveryKeyPath,
95
+ JSON.stringify({
96
+ version: 1,
97
+ createdAt: new Date().toISOString(),
98
+ keyId: "SSSS",
99
+ privateKeyBase64: Buffer.from([1, 2, 3, 4]).toString("base64"),
100
+ }),
101
+ "utf8",
102
+ );
103
+
104
+ const store = new MatrixRecoveryKeyStore(recoveryKeyPath);
105
+ const callbacks = store.buildCryptoCallbacks();
106
+ const resolved = await callbacks.getSecretStorageKey?.(
107
+ { keys: { SSSS: { name: "test" } } },
108
+ "m.cross_signing.master",
109
+ );
110
+
111
+ expect(resolved?.[0]).toBe("SSSS");
112
+ expect(Array.from(resolved?.[1] ?? [])).toEqual([1, 2, 3, 4]);
113
+ });
114
+
115
+ it("persists cached secret-storage keys with secure file permissions", () => {
116
+ const recoveryKeyPath = createTempRecoveryKeyPath();
117
+ const store = new MatrixRecoveryKeyStore(recoveryKeyPath);
118
+ const callbacks = store.buildCryptoCallbacks();
119
+
120
+ callbacks.cacheSecretStorageKey?.(
121
+ "KEY123",
122
+ {
123
+ name: "openclaw",
124
+ },
125
+ new Uint8Array([9, 8, 7]),
126
+ );
127
+
128
+ const saved = JSON.parse(fs.readFileSync(recoveryKeyPath, "utf8")) as {
129
+ keyId?: string;
130
+ privateKeyBase64?: string;
131
+ };
132
+ expect(saved.keyId).toBe("KEY123");
133
+ expect(saved.privateKeyBase64).toBe(Buffer.from([9, 8, 7]).toString("base64"));
134
+
135
+ const mode = fs.statSync(recoveryKeyPath).mode & 0o777;
136
+ expect(mode).toBe(0o600);
137
+ });
138
+
139
+ it("creates and persists a recovery key when secret storage is missing", async () => {
140
+ const { store, createRecoveryKeyFromPassphrase, bootstrapSecretStorage } =
141
+ await runSecretStorageBootstrapScenario({
142
+ generated: createGeneratedRecoveryKey({
143
+ keyId: "GENERATED",
144
+ name: "generated",
145
+ bytes: [5, 6, 7, 8],
146
+ encodedPrivateKey: "encoded-generated-key", // pragma: allowlist secret
147
+ }),
148
+ status: { ready: false, defaultKeyId: null },
149
+ });
150
+
151
+ expect(createRecoveryKeyFromPassphrase).toHaveBeenCalledTimes(1);
152
+ expect(bootstrapSecretStorage).toHaveBeenCalledWith(
153
+ expect.objectContaining({
154
+ setupNewSecretStorage: true,
155
+ }),
156
+ );
157
+ expect(store.getRecoveryKeySummary()).toMatchObject({
158
+ keyId: "GENERATED",
159
+ encodedPrivateKey: "encoded-generated-key", // pragma: allowlist secret
160
+ });
161
+ });
162
+
163
+ it("rebinds stored recovery key to server default key id when it changes", async () => {
164
+ const recoveryKeyPath = createTempRecoveryKeyPath();
165
+ fs.writeFileSync(
166
+ recoveryKeyPath,
167
+ JSON.stringify({
168
+ version: 1,
169
+ createdAt: new Date().toISOString(),
170
+ keyId: "OLD",
171
+ privateKeyBase64: Buffer.from([1, 2, 3, 4]).toString("base64"),
172
+ }),
173
+ "utf8",
174
+ );
175
+ const store = new MatrixRecoveryKeyStore(recoveryKeyPath);
176
+
177
+ const bootstrapSecretStorage = vi.fn(async () => {});
178
+ const createRecoveryKeyFromPassphrase = vi.fn(async () => {
179
+ throw new Error("should not be called");
180
+ });
181
+ const crypto = {
182
+ on: vi.fn(),
183
+ bootstrapCrossSigning: vi.fn(async () => {}),
184
+ bootstrapSecretStorage,
185
+ createRecoveryKeyFromPassphrase,
186
+ getSecretStorageStatus: vi.fn(async () => ({ ready: true, defaultKeyId: "NEW" })),
187
+ requestOwnUserVerification: vi.fn(async () => null),
188
+ } as unknown as MatrixCryptoBootstrapApi;
189
+
190
+ await store.bootstrapSecretStorageWithRecoveryKey(crypto);
191
+
192
+ expect(createRecoveryKeyFromPassphrase).not.toHaveBeenCalled();
193
+ expect(store.getRecoveryKeySummary()).toMatchObject({
194
+ keyId: "NEW",
195
+ });
196
+ });
197
+
198
+ it("recreates secret storage when default key exists but is not usable locally", async () => {
199
+ const { store, createRecoveryKeyFromPassphrase, bootstrapSecretStorage } =
200
+ await runSecretStorageBootstrapScenario({
201
+ generated: createGeneratedRecoveryKey({
202
+ keyId: "RECOVERED",
203
+ name: "recovered",
204
+ bytes: [1, 1, 2, 3],
205
+ encodedPrivateKey: "encoded-recovered-key", // pragma: allowlist secret
206
+ }),
207
+ status: { ready: false, defaultKeyId: "LEGACY" },
208
+ });
209
+
210
+ expect(createRecoveryKeyFromPassphrase).toHaveBeenCalledTimes(1);
211
+ expect(bootstrapSecretStorage).toHaveBeenCalledWith(
212
+ expect.objectContaining({
213
+ setupNewSecretStorage: true,
214
+ }),
215
+ );
216
+ expect(store.getRecoveryKeySummary()).toMatchObject({
217
+ keyId: "RECOVERED",
218
+ encodedPrivateKey: "encoded-recovered-key", // pragma: allowlist secret
219
+ });
220
+ });
221
+
222
+ it("recreates secret storage during explicit bootstrap when the server key exists but no local recovery key is available", async () => {
223
+ const { store, createRecoveryKeyFromPassphrase, bootstrapSecretStorage } =
224
+ await runSecretStorageBootstrapScenario({
225
+ generated: createGeneratedRecoveryKey({
226
+ keyId: "REPAIRED",
227
+ name: "repaired",
228
+ bytes: [7, 7, 8, 9],
229
+ encodedPrivateKey: "encoded-repaired-key", // pragma: allowlist secret
230
+ }),
231
+ status: {
232
+ ready: true,
233
+ defaultKeyId: "LEGACY",
234
+ secretStorageKeyValidityMap: { LEGACY: true },
235
+ },
236
+ allowSecretStorageRecreateWithoutRecoveryKey: true,
237
+ firstBootstrapError: "getSecretStorageKey callback returned falsey",
238
+ });
239
+
240
+ expect(createRecoveryKeyFromPassphrase).toHaveBeenCalledTimes(1);
241
+ expect(bootstrapSecretStorage).toHaveBeenCalledTimes(2);
242
+ expect(bootstrapSecretStorage).toHaveBeenLastCalledWith(
243
+ expect.objectContaining({
244
+ setupNewSecretStorage: true,
245
+ }),
246
+ );
247
+ expect(store.getRecoveryKeySummary()).toMatchObject({
248
+ keyId: "REPAIRED",
249
+ encodedPrivateKey: "encoded-repaired-key", // pragma: allowlist secret
250
+ });
251
+ });
252
+
253
+ it("recreates secret storage during explicit bootstrap when decrypting a stored secret fails with bad MAC", async () => {
254
+ const { createRecoveryKeyFromPassphrase, bootstrapSecretStorage } =
255
+ await runSecretStorageBootstrapScenario({
256
+ generated: createGeneratedRecoveryKey({
257
+ keyId: "REPAIRED",
258
+ name: "repaired",
259
+ bytes: [7, 7, 8, 9],
260
+ encodedPrivateKey: "encoded-repaired-key", // pragma: allowlist secret
261
+ }),
262
+ status: {
263
+ ready: true,
264
+ defaultKeyId: "LEGACY",
265
+ secretStorageKeyValidityMap: { LEGACY: true },
266
+ },
267
+ allowSecretStorageRecreateWithoutRecoveryKey: true,
268
+ firstBootstrapError: "Error decrypting secret m.cross_signing.master: bad MAC",
269
+ });
270
+
271
+ expect(createRecoveryKeyFromPassphrase).toHaveBeenCalledTimes(1);
272
+ expect(bootstrapSecretStorage).toHaveBeenCalledTimes(2);
273
+ expect(bootstrapSecretStorage).toHaveBeenLastCalledWith(
274
+ expect.objectContaining({
275
+ setupNewSecretStorage: true,
276
+ }),
277
+ );
278
+ });
279
+
280
+ it("stores an encoded recovery key and decodes its private key material", () => {
281
+ const recoveryKeyPath = createTempRecoveryKeyPath();
282
+ const store = new MatrixRecoveryKeyStore(recoveryKeyPath);
283
+ const encoded = encodeRecoveryKey(new Uint8Array(Array.from({ length: 32 }, (_, i) => i + 1)));
284
+ expect(encoded).toBeTypeOf("string");
285
+
286
+ const summary = store.storeEncodedRecoveryKey({
287
+ encodedPrivateKey: encoded as string,
288
+ keyId: "SSSSKEY",
289
+ });
290
+
291
+ expect(summary.keyId).toBe("SSSSKEY");
292
+ expect(summary.encodedPrivateKey).toBe(encoded);
293
+ const persisted = JSON.parse(fs.readFileSync(recoveryKeyPath, "utf8")) as {
294
+ privateKeyBase64?: string;
295
+ keyId?: string;
296
+ };
297
+ expect(persisted.keyId).toBe("SSSSKEY");
298
+ expect(
299
+ Buffer.from(persisted.privateKeyBase64 ?? "", "base64").equals(
300
+ Buffer.from(Array.from({ length: 32 }, (_, i) => i + 1)),
301
+ ),
302
+ ).toBe(true);
303
+ });
304
+
305
+ it("stages a recovery key for secret storage without persisting it until commit", async () => {
306
+ const recoveryKeyPath = createTempRecoveryKeyPath();
307
+ fs.rmSync(recoveryKeyPath, { force: true });
308
+ const store = new MatrixRecoveryKeyStore(recoveryKeyPath);
309
+ const encoded = encodeRecoveryKey(
310
+ new Uint8Array(Array.from({ length: 32 }, (_, i) => (i + 11) % 255)),
311
+ );
312
+ expect(encoded).toBeTypeOf("string");
313
+
314
+ store.stageEncodedRecoveryKey({
315
+ encodedPrivateKey: encoded as string,
316
+ keyId: "SSSSKEY",
317
+ });
318
+
319
+ expect(fs.existsSync(recoveryKeyPath)).toBe(false);
320
+ const callbacks = store.buildCryptoCallbacks();
321
+ const resolved = await callbacks.getSecretStorageKey?.(
322
+ { keys: { SSSSKEY: { name: "test" } } },
323
+ "m.cross_signing.master",
324
+ );
325
+ expect(resolved?.[0]).toBe("SSSSKEY");
326
+
327
+ store.commitStagedRecoveryKey({ keyId: "SSSSKEY" });
328
+
329
+ const persisted = JSON.parse(fs.readFileSync(recoveryKeyPath, "utf8")) as {
330
+ keyId?: string;
331
+ encodedPrivateKey?: string;
332
+ };
333
+ expect(persisted.keyId).toBe("SSSSKEY");
334
+ expect(persisted.encodedPrivateKey).toBe(encoded);
335
+ });
336
+
337
+ it("does not overwrite the stored recovery key while a staged key is only being validated", async () => {
338
+ const recoveryKeyPath = createTempRecoveryKeyPath();
339
+ const storedEncoded = encodeRecoveryKey(
340
+ new Uint8Array(Array.from({ length: 32 }, (_, i) => (i + 1) % 255)),
341
+ );
342
+ fs.writeFileSync(
343
+ recoveryKeyPath,
344
+ JSON.stringify({
345
+ version: 1,
346
+ createdAt: "2026-03-12T00:00:00.000Z",
347
+ keyId: "OLD",
348
+ encodedPrivateKey: storedEncoded,
349
+ privateKeyBase64: Buffer.from(
350
+ new Uint8Array(Array.from({ length: 32 }, (_, i) => (i + 1) % 255)),
351
+ ).toString("base64"),
352
+ }),
353
+ "utf8",
354
+ );
355
+
356
+ const store = new MatrixRecoveryKeyStore(recoveryKeyPath);
357
+ const stagedEncoded = encodeRecoveryKey(
358
+ new Uint8Array(Array.from({ length: 32 }, (_, i) => (i + 101) % 255)),
359
+ );
360
+ store.stageEncodedRecoveryKey({
361
+ encodedPrivateKey: stagedEncoded as string,
362
+ keyId: "NEW",
363
+ });
364
+
365
+ const crypto = {
366
+ on: vi.fn(),
367
+ bootstrapCrossSigning: vi.fn(async () => {}),
368
+ bootstrapSecretStorage: vi.fn(async () => {}),
369
+ createRecoveryKeyFromPassphrase: vi.fn(async () => {
370
+ throw new Error("should not be called");
371
+ }),
372
+ getSecretStorageStatus: vi.fn(async () => ({ ready: true, defaultKeyId: "NEW" })),
373
+ requestOwnUserVerification: vi.fn(async () => null),
374
+ } as unknown as MatrixCryptoBootstrapApi;
375
+
376
+ await store.bootstrapSecretStorageWithRecoveryKey(crypto);
377
+
378
+ const persisted = JSON.parse(fs.readFileSync(recoveryKeyPath, "utf8")) as {
379
+ keyId?: string;
380
+ encodedPrivateKey?: string;
381
+ };
382
+ expect(persisted.keyId).toBe("OLD");
383
+ expect(persisted.encodedPrivateKey).toBe(storedEncoded);
384
+ });
385
+ });