@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,139 @@
1
+ import fs from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { readJsonFileWithFallback, writeJsonFileAtomically } from "openclaw/plugin-sdk/json-store";
5
+ import { getMatrixRuntime } from "../../runtime.js";
6
+ import { resolveMatrixStoragePaths } from "../client/storage.js";
7
+ import type { MatrixAuth } from "../client/types.js";
8
+ import type { MatrixClient } from "../sdk.js";
9
+
10
+ type MatrixLegacyCryptoMigrationState = {
11
+ version: 1;
12
+ accountId: string;
13
+ roomKeyCounts: {
14
+ total: number;
15
+ backedUp: number;
16
+ } | null;
17
+ restoreStatus: "pending" | "completed" | "manual-action-required";
18
+ restoredAt?: string;
19
+ importedCount?: number;
20
+ totalCount?: number;
21
+ lastError?: string | null;
22
+ };
23
+
24
+ export type MatrixLegacyCryptoRestoreResult =
25
+ | { kind: "skipped" }
26
+ | {
27
+ kind: "restored";
28
+ imported: number;
29
+ total: number;
30
+ localOnlyKeys: number;
31
+ }
32
+ | {
33
+ kind: "failed";
34
+ error: string;
35
+ localOnlyKeys: number;
36
+ };
37
+
38
+ function isMigrationState(value: unknown): value is MatrixLegacyCryptoMigrationState {
39
+ return (
40
+ Boolean(value) && typeof value === "object" && (value as { version?: unknown }).version === 1
41
+ );
42
+ }
43
+
44
+ async function resolvePendingMigrationStatePath(params: {
45
+ stateDir: string;
46
+ auth: Pick<MatrixAuth, "homeserver" | "userId" | "accessToken" | "accountId" | "deviceId">;
47
+ }): Promise<{
48
+ statePath: string;
49
+ value: MatrixLegacyCryptoMigrationState | null;
50
+ }> {
51
+ const { rootDir } = resolveMatrixStoragePaths({
52
+ homeserver: params.auth.homeserver,
53
+ userId: params.auth.userId,
54
+ accessToken: params.auth.accessToken,
55
+ accountId: params.auth.accountId,
56
+ deviceId: params.auth.deviceId,
57
+ stateDir: params.stateDir,
58
+ });
59
+ const directStatePath = path.join(rootDir, "legacy-crypto-migration.json");
60
+ const { value: directValue } =
61
+ await readJsonFileWithFallback<MatrixLegacyCryptoMigrationState | null>(directStatePath, null);
62
+ if (isMigrationState(directValue) && directValue.restoreStatus === "pending") {
63
+ return { statePath: directStatePath, value: directValue };
64
+ }
65
+
66
+ const accountStorageDir = path.dirname(rootDir);
67
+ let siblingEntries: string[] = [];
68
+ try {
69
+ siblingEntries = (await fs.readdir(accountStorageDir, { withFileTypes: true }))
70
+ .filter((entry) => entry.isDirectory())
71
+ .map((entry) => entry.name)
72
+ .filter((entry) => path.join(accountStorageDir, entry) !== rootDir)
73
+ .toSorted((left, right) => left.localeCompare(right));
74
+ } catch {
75
+ return { statePath: directStatePath, value: directValue };
76
+ }
77
+
78
+ for (const sibling of siblingEntries) {
79
+ const siblingStatePath = path.join(accountStorageDir, sibling, "legacy-crypto-migration.json");
80
+ const { value } = await readJsonFileWithFallback<MatrixLegacyCryptoMigrationState | null>(
81
+ siblingStatePath,
82
+ null,
83
+ );
84
+ if (isMigrationState(value) && value.restoreStatus === "pending") {
85
+ return { statePath: siblingStatePath, value };
86
+ }
87
+ }
88
+ return { statePath: directStatePath, value: directValue };
89
+ }
90
+
91
+ export async function maybeRestoreLegacyMatrixBackup(params: {
92
+ client: Pick<MatrixClient, "restoreRoomKeyBackup">;
93
+ auth: Pick<MatrixAuth, "homeserver" | "userId" | "accessToken" | "accountId" | "deviceId">;
94
+ env?: NodeJS.ProcessEnv;
95
+ stateDir?: string;
96
+ }): Promise<MatrixLegacyCryptoRestoreResult> {
97
+ const env = params.env ?? process.env;
98
+ const stateDir = params.stateDir ?? getMatrixRuntime().state.resolveStateDir(env, os.homedir);
99
+ const { statePath, value } = await resolvePendingMigrationStatePath({
100
+ stateDir,
101
+ auth: params.auth,
102
+ });
103
+ if (!isMigrationState(value) || value.restoreStatus !== "pending") {
104
+ return { kind: "skipped" };
105
+ }
106
+
107
+ const restore = await params.client.restoreRoomKeyBackup();
108
+ const localOnlyKeys =
109
+ value.roomKeyCounts && value.roomKeyCounts.total > value.roomKeyCounts.backedUp
110
+ ? value.roomKeyCounts.total - value.roomKeyCounts.backedUp
111
+ : 0;
112
+
113
+ if (restore.success) {
114
+ await writeJsonFileAtomically(statePath, {
115
+ ...value,
116
+ restoreStatus: "completed",
117
+ restoredAt: restore.restoredAt ?? new Date().toISOString(),
118
+ importedCount: restore.imported,
119
+ totalCount: restore.total,
120
+ lastError: null,
121
+ } satisfies MatrixLegacyCryptoMigrationState);
122
+ return {
123
+ kind: "restored",
124
+ imported: restore.imported,
125
+ total: restore.total,
126
+ localOnlyKeys,
127
+ };
128
+ }
129
+
130
+ await writeJsonFileAtomically(statePath, {
131
+ ...value,
132
+ lastError: restore.error ?? "unknown",
133
+ } satisfies MatrixLegacyCryptoMigrationState);
134
+ return {
135
+ kind: "failed",
136
+ error: restore.error ?? "unknown",
137
+ localOnlyKeys,
138
+ };
139
+ }
@@ -0,0 +1,100 @@
1
+ import {
2
+ normalizeLowercaseStringOrEmpty,
3
+ normalizeOptionalString,
4
+ } from "openclaw/plugin-sdk/text-runtime";
5
+ import type { LocationMessageEventContent } from "../sdk.js";
6
+ import { formatLocationText, toLocationContext, type NormalizedLocation } from "./runtime-api.js";
7
+ import { EventType } from "./types.js";
8
+
9
+ export type MatrixLocationPayload = {
10
+ text: string;
11
+ context: ReturnType<typeof toLocationContext>;
12
+ };
13
+
14
+ type GeoUriParams = {
15
+ latitude: number;
16
+ longitude: number;
17
+ accuracy?: number;
18
+ };
19
+
20
+ function parseGeoUri(value: string): GeoUriParams | null {
21
+ const trimmed = value.trim();
22
+ if (!trimmed) {
23
+ return null;
24
+ }
25
+ if (!normalizeLowercaseStringOrEmpty(trimmed).startsWith("geo:")) {
26
+ return null;
27
+ }
28
+ const payload = trimmed.slice(4);
29
+ const [coordsPart, ...paramParts] = payload.split(";");
30
+ const coords = coordsPart.split(",");
31
+ if (coords.length < 2) {
32
+ return null;
33
+ }
34
+ const latitude = Number.parseFloat(coords[0] ?? "");
35
+ const longitude = Number.parseFloat(coords[1] ?? "");
36
+ if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) {
37
+ return null;
38
+ }
39
+
40
+ const params = new Map<string, string>();
41
+ for (const part of paramParts) {
42
+ const segment = part.trim();
43
+ if (!segment) {
44
+ continue;
45
+ }
46
+ const eqIndex = segment.indexOf("=");
47
+ const rawKey = eqIndex === -1 ? segment : segment.slice(0, eqIndex);
48
+ const rawValue = eqIndex === -1 ? "" : segment.slice(eqIndex + 1);
49
+ const key = normalizeLowercaseStringOrEmpty(rawKey);
50
+ if (!key) {
51
+ continue;
52
+ }
53
+ const valuePart = rawValue.trim();
54
+ params.set(key, valuePart ? decodeURIComponent(valuePart) : "");
55
+ }
56
+
57
+ const accuracyRaw = params.get("u");
58
+ const accuracy = accuracyRaw ? Number.parseFloat(accuracyRaw) : undefined;
59
+
60
+ return {
61
+ latitude,
62
+ longitude,
63
+ accuracy: Number.isFinite(accuracy) ? accuracy : undefined,
64
+ };
65
+ }
66
+
67
+ export function resolveMatrixLocation(params: {
68
+ eventType: string;
69
+ content: LocationMessageEventContent;
70
+ }): MatrixLocationPayload | null {
71
+ const { eventType, content } = params;
72
+ const isLocation =
73
+ eventType === EventType.Location ||
74
+ (eventType === EventType.RoomMessage && content.msgtype === EventType.Location);
75
+ if (!isLocation) {
76
+ return null;
77
+ }
78
+ const geoUri = normalizeOptionalString(content.geo_uri) ?? "";
79
+ if (!geoUri) {
80
+ return null;
81
+ }
82
+ const parsed = parseGeoUri(geoUri);
83
+ if (!parsed) {
84
+ return null;
85
+ }
86
+ const caption = normalizeOptionalString(content.body) ?? "";
87
+ const location: NormalizedLocation = {
88
+ latitude: parsed.latitude,
89
+ longitude: parsed.longitude,
90
+ accuracy: parsed.accuracy,
91
+ caption: caption || undefined,
92
+ source: "pin",
93
+ isLive: false,
94
+ };
95
+
96
+ return {
97
+ text: formatLocationText(location),
98
+ context: toLocationContext(location),
99
+ };
100
+ }
@@ -0,0 +1,159 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+ import type { PluginRuntime } from "../../../runtime-api.js";
3
+ import { setMatrixRuntime } from "../../runtime.js";
4
+ import { MatrixMediaSizeLimitError } from "../media-errors.js";
5
+ import { downloadMatrixMedia } from "./media.js";
6
+
7
+ function createEncryptedClient() {
8
+ const decryptMedia = vi.fn().mockResolvedValue(Buffer.from("decrypted"));
9
+
10
+ return {
11
+ client: {
12
+ crypto: { decryptMedia },
13
+ mxcToHttp: vi.fn().mockReturnValue("https://example/mxc"),
14
+ } as unknown as import("../sdk.js").MatrixClient,
15
+ decryptMedia,
16
+ };
17
+ }
18
+
19
+ function createEncryptedFile() {
20
+ return {
21
+ url: "mxc://example/file",
22
+ key: {
23
+ kty: "oct",
24
+ key_ops: ["encrypt", "decrypt"],
25
+ alg: "A256CTR",
26
+ k: "secret",
27
+ ext: true,
28
+ },
29
+ iv: "iv",
30
+ hashes: { sha256: "hash" },
31
+ v: "v2",
32
+ };
33
+ }
34
+
35
+ describe("downloadMatrixMedia", () => {
36
+ const saveMediaBuffer = vi.fn().mockResolvedValue({
37
+ path: "/tmp/media",
38
+ contentType: "image/png",
39
+ });
40
+
41
+ const runtimeStub = {
42
+ channel: {
43
+ media: {
44
+ saveMediaBuffer: (...args: unknown[]) => saveMediaBuffer(...args),
45
+ },
46
+ },
47
+ } as unknown as PluginRuntime;
48
+
49
+ beforeEach(() => {
50
+ vi.clearAllMocks();
51
+ setMatrixRuntime(runtimeStub);
52
+ });
53
+
54
+ it("decrypts encrypted media when file payloads are present", async () => {
55
+ const { client, decryptMedia } = createEncryptedClient();
56
+ const file = createEncryptedFile();
57
+
58
+ const result = await downloadMatrixMedia({
59
+ client,
60
+ mxcUrl: "mxc://example/file",
61
+ contentType: "image/png",
62
+ maxBytes: 1024,
63
+ file,
64
+ });
65
+
66
+ expect(decryptMedia).toHaveBeenCalledWith(file, {
67
+ maxBytes: 1024,
68
+ readIdleTimeoutMs: 30_000,
69
+ });
70
+ expect(saveMediaBuffer).toHaveBeenCalledWith(
71
+ Buffer.from("decrypted"),
72
+ "image/png",
73
+ "inbound",
74
+ 1024,
75
+ undefined,
76
+ );
77
+ expect(result?.path).toBe("/tmp/media");
78
+ });
79
+
80
+ it("forwards originalFilename to saveMediaBuffer when provided", async () => {
81
+ const { client } = createEncryptedClient();
82
+ const file = createEncryptedFile();
83
+
84
+ await downloadMatrixMedia({
85
+ client,
86
+ mxcUrl: "mxc://example/file",
87
+ contentType: "image/png",
88
+ maxBytes: 1024,
89
+ file,
90
+ originalFilename: "Screenshot 2026-03-27.png",
91
+ });
92
+
93
+ expect(saveMediaBuffer).toHaveBeenCalledWith(
94
+ Buffer.from("decrypted"),
95
+ "image/png",
96
+ "inbound",
97
+ 1024,
98
+ "Screenshot 2026-03-27.png",
99
+ );
100
+ });
101
+
102
+ it("rejects encrypted media that exceeds maxBytes before decrypting", async () => {
103
+ const { client, decryptMedia } = createEncryptedClient();
104
+ const file = createEncryptedFile();
105
+
106
+ await expect(
107
+ downloadMatrixMedia({
108
+ client,
109
+ mxcUrl: "mxc://example/file",
110
+ contentType: "image/png",
111
+ sizeBytes: 2048,
112
+ maxBytes: 1024,
113
+ file,
114
+ }),
115
+ ).rejects.toBeInstanceOf(MatrixMediaSizeLimitError);
116
+
117
+ expect(decryptMedia).not.toHaveBeenCalled();
118
+ expect(saveMediaBuffer).not.toHaveBeenCalled();
119
+ });
120
+
121
+ it("preserves typed size-limit errors from plain media downloads", async () => {
122
+ const tooLargeError = new MatrixMediaSizeLimitError(
123
+ "Matrix media exceeds configured size limit (8192 bytes > 4096 bytes)",
124
+ );
125
+ const downloadContent = vi.fn().mockRejectedValue(tooLargeError);
126
+ const client = {
127
+ downloadContent,
128
+ } as unknown as import("../sdk.js").MatrixClient;
129
+
130
+ await expect(
131
+ downloadMatrixMedia({
132
+ client,
133
+ mxcUrl: "mxc://example/file",
134
+ contentType: "image/png",
135
+ maxBytes: 4096,
136
+ }),
137
+ ).rejects.toBe(tooLargeError);
138
+ });
139
+
140
+ it("passes byte limits through plain media downloads", async () => {
141
+ const downloadContent = vi.fn().mockResolvedValue(Buffer.from("plain"));
142
+
143
+ const client = {
144
+ downloadContent,
145
+ } as unknown as import("../sdk.js").MatrixClient;
146
+
147
+ await downloadMatrixMedia({
148
+ client,
149
+ mxcUrl: "mxc://example/file",
150
+ contentType: "image/png",
151
+ maxBytes: 4096,
152
+ });
153
+
154
+ expect(downloadContent).toHaveBeenCalledWith("mxc://example/file", {
155
+ maxBytes: 4096,
156
+ readIdleTimeoutMs: 30_000,
157
+ });
158
+ });
159
+ });
@@ -0,0 +1,119 @@
1
+ import { getMatrixRuntime } from "../../runtime.js";
2
+ import { MatrixMediaSizeLimitError, isMatrixMediaSizeLimitError } from "../media-errors.js";
3
+ import type { MatrixClient } from "../sdk.js";
4
+
5
+ // Type for encrypted file info
6
+ type EncryptedFile = {
7
+ url: string;
8
+ key: {
9
+ kty: string;
10
+ key_ops: string[];
11
+ alg: string;
12
+ k: string;
13
+ ext: boolean;
14
+ };
15
+ iv: string;
16
+ hashes: Record<string, string>;
17
+ v: string;
18
+ };
19
+
20
+ const MATRIX_MEDIA_DOWNLOAD_IDLE_TIMEOUT_MS = 30_000;
21
+
22
+ async function fetchMatrixMediaBuffer(params: {
23
+ client: MatrixClient;
24
+ mxcUrl: string;
25
+ maxBytes: number;
26
+ }): Promise<{ buffer: Buffer } | null> {
27
+ try {
28
+ const buffer = await params.client.downloadContent(params.mxcUrl, {
29
+ maxBytes: params.maxBytes,
30
+ readIdleTimeoutMs: MATRIX_MEDIA_DOWNLOAD_IDLE_TIMEOUT_MS,
31
+ });
32
+ return { buffer };
33
+ } catch (err) {
34
+ if (isMatrixMediaSizeLimitError(err)) {
35
+ throw err;
36
+ }
37
+ throw new Error(`Matrix media download failed: ${String(err)}`, { cause: err });
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Download and decrypt encrypted media from a Matrix room.
43
+ * Uses the Matrix crypto adapter's decryptMedia helper.
44
+ */
45
+ async function fetchEncryptedMediaBuffer(params: {
46
+ client: MatrixClient;
47
+ file: EncryptedFile;
48
+ maxBytes: number;
49
+ }): Promise<{ buffer: Buffer } | null> {
50
+ if (!params.client.crypto) {
51
+ throw new Error("Cannot decrypt media: crypto not enabled");
52
+ }
53
+
54
+ const decrypted = await params.client.crypto.decryptMedia(
55
+ params.file as Parameters<typeof params.client.crypto.decryptMedia>[0],
56
+ {
57
+ maxBytes: params.maxBytes,
58
+ readIdleTimeoutMs: MATRIX_MEDIA_DOWNLOAD_IDLE_TIMEOUT_MS,
59
+ },
60
+ );
61
+
62
+ if (decrypted.byteLength > params.maxBytes) {
63
+ throw new MatrixMediaSizeLimitError();
64
+ }
65
+
66
+ return { buffer: decrypted };
67
+ }
68
+
69
+ export async function downloadMatrixMedia(params: {
70
+ client: MatrixClient;
71
+ mxcUrl: string;
72
+ contentType?: string;
73
+ sizeBytes?: number;
74
+ maxBytes: number;
75
+ file?: EncryptedFile;
76
+ originalFilename?: string;
77
+ }): Promise<{
78
+ path: string;
79
+ contentType?: string;
80
+ placeholder: string;
81
+ } | null> {
82
+ let fetched: { buffer: Buffer; headerType?: string } | null;
83
+ if (typeof params.sizeBytes === "number" && params.sizeBytes > params.maxBytes) {
84
+ throw new MatrixMediaSizeLimitError();
85
+ }
86
+
87
+ if (params.file) {
88
+ // Encrypted media
89
+ fetched = await fetchEncryptedMediaBuffer({
90
+ client: params.client,
91
+ file: params.file,
92
+ maxBytes: params.maxBytes,
93
+ });
94
+ } else {
95
+ // Unencrypted media
96
+ fetched = await fetchMatrixMediaBuffer({
97
+ client: params.client,
98
+ mxcUrl: params.mxcUrl,
99
+ maxBytes: params.maxBytes,
100
+ });
101
+ }
102
+
103
+ if (!fetched) {
104
+ return null;
105
+ }
106
+ const headerType = params.contentType ?? undefined;
107
+ const saved = await getMatrixRuntime().channel.media.saveMediaBuffer(
108
+ fetched.buffer,
109
+ headerType,
110
+ "inbound",
111
+ params.maxBytes,
112
+ params.originalFilename,
113
+ );
114
+ return {
115
+ path: saved.path,
116
+ contentType: saved.contentType,
117
+ placeholder: "[matrix media]",
118
+ };
119
+ }