@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,627 @@
1
+ import type { MatrixClient } from "../sdk.js";
2
+ import { resolveMatrixMonitorAccessState } from "./access-state.js";
3
+ import type { MatrixRawEvent } from "./types.js";
4
+ import { EventType } from "./types.js";
5
+ import {
6
+ isMatrixVerificationEventType,
7
+ isMatrixVerificationRequestMsgType,
8
+ matrixVerificationConstants,
9
+ } from "./verification-utils.js";
10
+
11
+ const MAX_TRACKED_VERIFICATION_EVENTS = 1024;
12
+ const SAS_NOTICE_RETRY_DELAY_MS = 750;
13
+ const VERIFICATION_EVENT_STARTUP_GRACE_MS = 30_000;
14
+
15
+ type MatrixVerificationStage = "request" | "ready" | "start" | "cancel" | "done" | "other";
16
+
17
+ type MatrixVerificationSummaryLike = {
18
+ id: string;
19
+ transactionId?: string;
20
+ roomId?: string;
21
+ otherUserId: string;
22
+ updatedAt?: string;
23
+ completed?: boolean;
24
+ pending?: boolean;
25
+ phase?: number;
26
+ phaseName?: string;
27
+ sas?: {
28
+ decimal?: [number, number, number];
29
+ emoji?: Array<[string, string]>;
30
+ };
31
+ };
32
+
33
+ type MatrixDirectRoomDeps = {
34
+ inspectMatrixDirectRooms: typeof import("../direct-management.js").inspectMatrixDirectRooms;
35
+ isStrictDirectRoom: typeof import("../direct-room.js").isStrictDirectRoom;
36
+ };
37
+
38
+ let matrixDirectRoomDepsPromise: Promise<MatrixDirectRoomDeps> | undefined;
39
+
40
+ async function loadMatrixDirectRoomDeps(): Promise<MatrixDirectRoomDeps> {
41
+ matrixDirectRoomDepsPromise ??= Promise.all([
42
+ import("../direct-management.js"),
43
+ import("../direct-room.js"),
44
+ ]).then(([directManagementModule, directRoomModule]) => ({
45
+ inspectMatrixDirectRooms: directManagementModule.inspectMatrixDirectRooms,
46
+ isStrictDirectRoom: directRoomModule.isStrictDirectRoom,
47
+ }));
48
+ return await matrixDirectRoomDepsPromise;
49
+ }
50
+
51
+ function trimMaybeString(input: unknown): string | null {
52
+ if (typeof input !== "string") {
53
+ return null;
54
+ }
55
+ const trimmed = input.trim();
56
+ return trimmed.length > 0 ? trimmed : null;
57
+ }
58
+
59
+ function readVerificationSignal(event: MatrixRawEvent): {
60
+ stage: MatrixVerificationStage;
61
+ flowId: string | null;
62
+ } | null {
63
+ const type = trimMaybeString(event?.type) ?? "";
64
+ const content = event?.content ?? {};
65
+ const msgtype = trimMaybeString((content as { msgtype?: unknown }).msgtype) ?? "";
66
+ const relatedEventId = trimMaybeString(
67
+ (content as { "m.relates_to"?: { event_id?: unknown } })["m.relates_to"]?.event_id,
68
+ );
69
+ const transactionId = trimMaybeString((content as { transaction_id?: unknown }).transaction_id);
70
+ if (type === EventType.RoomMessage && isMatrixVerificationRequestMsgType(msgtype)) {
71
+ return {
72
+ stage: "request",
73
+ flowId: trimMaybeString(event.event_id) ?? transactionId ?? relatedEventId,
74
+ };
75
+ }
76
+ if (!isMatrixVerificationEventType(type)) {
77
+ return null;
78
+ }
79
+ const flowId = transactionId ?? relatedEventId ?? trimMaybeString(event.event_id);
80
+ if (type === `${matrixVerificationConstants.eventPrefix}request`) {
81
+ return { stage: "request", flowId };
82
+ }
83
+ if (type === `${matrixVerificationConstants.eventPrefix}ready`) {
84
+ return { stage: "ready", flowId };
85
+ }
86
+ if (type === "m.key.verification.start") {
87
+ return { stage: "start", flowId };
88
+ }
89
+ if (type === "m.key.verification.cancel") {
90
+ return { stage: "cancel", flowId };
91
+ }
92
+ if (type === "m.key.verification.done") {
93
+ return { stage: "done", flowId };
94
+ }
95
+ return { stage: "other", flowId };
96
+ }
97
+
98
+ function formatVerificationStageNotice(params: {
99
+ stage: MatrixVerificationStage;
100
+ senderId: string;
101
+ event: MatrixRawEvent;
102
+ }): string | null {
103
+ const { stage, senderId, event } = params;
104
+ const content = event.content as { code?: unknown; reason?: unknown };
105
+ switch (stage) {
106
+ case "request":
107
+ return `Matrix verification request received from ${senderId}. Open "Verify by emoji" in your Matrix client to continue.`;
108
+ case "ready":
109
+ return `Matrix verification is ready with ${senderId}. Choose "Verify by emoji" to reveal the emoji sequence.`;
110
+ case "start":
111
+ return `Matrix verification started with ${senderId}.`;
112
+ case "done":
113
+ return `Matrix verification completed with ${senderId}.`;
114
+ case "cancel": {
115
+ const code = trimMaybeString(content.code);
116
+ const reason = trimMaybeString(content.reason);
117
+ if (code && reason) {
118
+ return `Matrix verification cancelled by ${senderId} (${code}: ${reason}).`;
119
+ }
120
+ if (reason) {
121
+ return `Matrix verification cancelled by ${senderId} (${reason}).`;
122
+ }
123
+ return `Matrix verification cancelled by ${senderId}.`;
124
+ }
125
+ default:
126
+ return null;
127
+ }
128
+ }
129
+
130
+ function formatVerificationSasNotice(summary: MatrixVerificationSummaryLike): string | null {
131
+ const sas = summary.sas;
132
+ if (!sas) {
133
+ return null;
134
+ }
135
+ const emojiLine =
136
+ Array.isArray(sas.emoji) && sas.emoji.length > 0
137
+ ? `SAS emoji: ${sas.emoji
138
+ .map(
139
+ ([emoji, name]) => `${trimMaybeString(emoji) ?? "?"} ${trimMaybeString(name) ?? "?"}`,
140
+ )
141
+ .join(" | ")}`
142
+ : null;
143
+ const decimalLine =
144
+ Array.isArray(sas.decimal) && sas.decimal.length === 3
145
+ ? `SAS decimal: ${sas.decimal.join(" ")}`
146
+ : null;
147
+ if (!emojiLine && !decimalLine) {
148
+ return null;
149
+ }
150
+ const lines = [`Matrix verification SAS with ${summary.otherUserId}:`];
151
+ if (emojiLine) {
152
+ lines.push(emojiLine);
153
+ }
154
+ if (decimalLine) {
155
+ lines.push(decimalLine);
156
+ }
157
+ lines.push("If both sides match, choose 'They match' in your Matrix app.");
158
+ return lines.join("\n");
159
+ }
160
+
161
+ function resolveVerificationFlowCandidates(params: {
162
+ event: MatrixRawEvent;
163
+ flowId: string | null;
164
+ }): string[] {
165
+ const { event, flowId } = params;
166
+ const content = event.content as {
167
+ transaction_id?: unknown;
168
+ "m.relates_to"?: { event_id?: unknown };
169
+ };
170
+ const candidates = new Set<string>();
171
+ const add = (value: unknown) => {
172
+ const normalized = trimMaybeString(value);
173
+ if (normalized) {
174
+ candidates.add(normalized);
175
+ }
176
+ };
177
+ add(flowId);
178
+ add(event.event_id);
179
+ add(content.transaction_id);
180
+ add(content["m.relates_to"]?.event_id);
181
+ return Array.from(candidates);
182
+ }
183
+
184
+ function resolveSummaryRecency(summary: MatrixVerificationSummaryLike): number {
185
+ const ts = Date.parse(summary.updatedAt ?? "");
186
+ return Number.isFinite(ts) ? ts : 0;
187
+ }
188
+
189
+ function isActiveVerificationSummary(summary: MatrixVerificationSummaryLike): boolean {
190
+ if (summary.completed === true) {
191
+ return false;
192
+ }
193
+ if (summary.phaseName === "cancelled" || summary.phaseName === "done") {
194
+ return false;
195
+ }
196
+ if (typeof summary.phase === "number" && summary.phase >= 4) {
197
+ return false;
198
+ }
199
+ return true;
200
+ }
201
+
202
+ async function resolveVerificationSummaryForSignal(
203
+ client: MatrixClient,
204
+ params: {
205
+ roomId: string;
206
+ event: MatrixRawEvent;
207
+ senderId: string;
208
+ flowId: string | null;
209
+ },
210
+ ): Promise<MatrixVerificationSummaryLike | null> {
211
+ if (!client.crypto) {
212
+ return null;
213
+ }
214
+ await client.crypto
215
+ .ensureVerificationDmTracked({
216
+ roomId: params.roomId,
217
+ userId: params.senderId,
218
+ })
219
+ .catch(() => null);
220
+ const list = await client.crypto.listVerifications();
221
+ if (list.length === 0) {
222
+ return null;
223
+ }
224
+ const candidates = resolveVerificationFlowCandidates({
225
+ event: params.event,
226
+ flowId: params.flowId,
227
+ });
228
+ const byTransactionId = list.find((entry) =>
229
+ candidates.some((candidate) => entry.transactionId === candidate),
230
+ );
231
+ if (byTransactionId) {
232
+ return byTransactionId;
233
+ }
234
+
235
+ // Only fall back by user inside the active DM with that user. Otherwise a
236
+ // spoofed verification event in an unrelated room can leak the current SAS
237
+ // prompt into that room.
238
+ const { inspectMatrixDirectRooms, isStrictDirectRoom } = await loadMatrixDirectRoomDeps();
239
+ const inspection = await inspectMatrixDirectRooms({
240
+ client,
241
+ remoteUserId: params.senderId,
242
+ }).catch(() => null);
243
+ const activeRoomId = trimMaybeString(inspection?.activeRoomId);
244
+ if (activeRoomId) {
245
+ if (activeRoomId !== params.roomId) {
246
+ return null;
247
+ }
248
+ } else if (
249
+ !(await isStrictDirectRoom({
250
+ client,
251
+ roomId: params.roomId,
252
+ remoteUserId: params.senderId,
253
+ }))
254
+ ) {
255
+ // If we cannot determine a canonical active DM, preserve the older
256
+ // strict-room fallback so transient m.direct or joined-room read failures
257
+ // do not suppress SAS notices for the current DM.
258
+ return null;
259
+ }
260
+
261
+ // Fallback for DM flows where transaction IDs do not match room event IDs consistently.
262
+ const activeByUser = list
263
+ .filter((entry) => entry.otherUserId === params.senderId && isActiveVerificationSummary(entry))
264
+ .toSorted((a, b) => resolveSummaryRecency(b) - resolveSummaryRecency(a));
265
+ const activeInRoom = activeByUser.filter((entry) => {
266
+ const roomId = trimMaybeString(entry.roomId);
267
+ return roomId === params.roomId;
268
+ });
269
+ if (activeInRoom.length > 0) {
270
+ return activeInRoom[0] ?? null;
271
+ }
272
+ return activeByUser[0] ?? null;
273
+ }
274
+
275
+ async function resolveVerificationSasNoticeForSignal(
276
+ client: MatrixClient,
277
+ params: {
278
+ roomId: string;
279
+ event: MatrixRawEvent;
280
+ senderId: string;
281
+ flowId: string | null;
282
+ stage: MatrixVerificationStage;
283
+ },
284
+ ): Promise<{ summary: MatrixVerificationSummaryLike | null; sasNotice: string | null }> {
285
+ const summary = await resolveVerificationSummaryForSignal(client, params);
286
+ const immediateNotice =
287
+ summary && isActiveVerificationSummary(summary) ? formatVerificationSasNotice(summary) : null;
288
+ if (immediateNotice || (params.stage !== "ready" && params.stage !== "start")) {
289
+ return {
290
+ summary,
291
+ sasNotice: immediateNotice,
292
+ };
293
+ }
294
+
295
+ await new Promise((resolve) => setTimeout(resolve, SAS_NOTICE_RETRY_DELAY_MS));
296
+ const retriedSummary = await resolveVerificationSummaryForSignal(client, params);
297
+ return {
298
+ summary: retriedSummary,
299
+ sasNotice:
300
+ retriedSummary && isActiveVerificationSummary(retriedSummary)
301
+ ? formatVerificationSasNotice(retriedSummary)
302
+ : null,
303
+ };
304
+ }
305
+
306
+ function trackBounded(set: Set<string>, value: string): boolean {
307
+ if (!value || set.has(value)) {
308
+ return false;
309
+ }
310
+ set.add(value);
311
+ if (set.size > MAX_TRACKED_VERIFICATION_EVENTS) {
312
+ const oldest = set.values().next().value;
313
+ if (typeof oldest === "string") {
314
+ set.delete(oldest);
315
+ }
316
+ }
317
+ return true;
318
+ }
319
+
320
+ async function sendVerificationNotice(params: {
321
+ client: MatrixClient;
322
+ roomId: string;
323
+ body: string;
324
+ logVerboseMessage: (message: string) => void;
325
+ }): Promise<void> {
326
+ const roomId = trimMaybeString(params.roomId);
327
+ if (!roomId) {
328
+ return;
329
+ }
330
+ try {
331
+ await params.client.sendMessage(roomId, {
332
+ msgtype: "m.notice",
333
+ body: params.body,
334
+ });
335
+ } catch (err) {
336
+ params.logVerboseMessage(
337
+ `matrix: failed sending verification notice room=${roomId}: ${String(err)}`,
338
+ );
339
+ }
340
+ }
341
+
342
+ async function isVerificationNoticeAuthorized(params: {
343
+ senderId: string;
344
+ allowFrom: string[];
345
+ dmEnabled: boolean;
346
+ dmPolicy: "open" | "pairing" | "allowlist" | "disabled";
347
+ readStoreAllowFrom: () => Promise<string[]>;
348
+ logVerboseMessage: (message: string) => void;
349
+ }): Promise<boolean> {
350
+ // Verification notices are DM-only. If DM ingress is disabled, there is no
351
+ // policy-compatible path for posting these notices back into the room.
352
+ if (!params.dmEnabled || params.dmPolicy === "disabled") {
353
+ params.logVerboseMessage(
354
+ `matrix: blocked verification sender ${params.senderId} (dmPolicy=${params.dmPolicy}, dmEnabled=${String(params.dmEnabled)})`,
355
+ );
356
+ return false;
357
+ }
358
+ if (params.dmPolicy === "open") {
359
+ return true;
360
+ }
361
+ const storeAllowFrom = await params.readStoreAllowFrom();
362
+ const accessState = resolveMatrixMonitorAccessState({
363
+ allowFrom: params.allowFrom,
364
+ storeAllowFrom,
365
+ // Verification flows only exist in strict DMs, so room/group allowlists do
366
+ // not participate in the authorization decision here.
367
+ groupAllowFrom: [],
368
+ roomUsers: [],
369
+ senderId: params.senderId,
370
+ isRoom: false,
371
+ });
372
+ if (accessState.directAllowMatch.allowed) {
373
+ return true;
374
+ }
375
+ params.logVerboseMessage(
376
+ `matrix: blocked verification sender ${params.senderId} (dmPolicy=${params.dmPolicy})`,
377
+ );
378
+ return false;
379
+ }
380
+
381
+ export function createMatrixVerificationEventRouter(params: {
382
+ client: MatrixClient;
383
+ allowFrom: string[];
384
+ dmEnabled: boolean;
385
+ dmPolicy: "open" | "pairing" | "allowlist" | "disabled";
386
+ readStoreAllowFrom: () => Promise<string[]>;
387
+ logVerboseMessage: (message: string) => void;
388
+ }) {
389
+ const routerStartedAtMs = Date.now();
390
+ const routedVerificationEvents = new Set<string>();
391
+ const routedVerificationSasFingerprints = new Set<string>();
392
+ const routedVerificationStageNotices = new Set<string>();
393
+ const verificationFlowRooms = new Map<string, string>();
394
+ const verificationUserRooms = new Map<string, string>();
395
+
396
+ async function resolveActiveDirectRoomId(remoteUserId: string): Promise<string | null> {
397
+ const { inspectMatrixDirectRooms } = await loadMatrixDirectRoomDeps();
398
+ const inspection = await inspectMatrixDirectRooms({
399
+ client: params.client,
400
+ remoteUserId,
401
+ }).catch(() => null);
402
+ return trimMaybeString(inspection?.activeRoomId);
403
+ }
404
+
405
+ function shouldEmitVerificationEventNotice(event: MatrixRawEvent): boolean {
406
+ const eventTs =
407
+ typeof event.origin_server_ts === "number" && Number.isFinite(event.origin_server_ts)
408
+ ? event.origin_server_ts
409
+ : null;
410
+ if (eventTs === null) {
411
+ return true;
412
+ }
413
+ return eventTs >= routerStartedAtMs - VERIFICATION_EVENT_STARTUP_GRACE_MS;
414
+ }
415
+
416
+ function rememberVerificationRoom(roomId: string, event: MatrixRawEvent, flowId: string | null) {
417
+ for (const candidate of resolveVerificationFlowCandidates({ event, flowId })) {
418
+ verificationFlowRooms.set(candidate, roomId);
419
+ if (verificationFlowRooms.size > MAX_TRACKED_VERIFICATION_EVENTS) {
420
+ const oldest = verificationFlowRooms.keys().next().value;
421
+ if (typeof oldest === "string") {
422
+ verificationFlowRooms.delete(oldest);
423
+ }
424
+ }
425
+ }
426
+ }
427
+
428
+ function rememberVerificationUserRoom(remoteUserId: string, roomId: string): void {
429
+ const normalizedUserId = trimMaybeString(remoteUserId);
430
+ const normalizedRoomId = trimMaybeString(roomId);
431
+ if (!normalizedUserId || !normalizedRoomId) {
432
+ return;
433
+ }
434
+ verificationUserRooms.delete(normalizedUserId);
435
+ verificationUserRooms.set(normalizedUserId, normalizedRoomId);
436
+ if (verificationUserRooms.size > MAX_TRACKED_VERIFICATION_EVENTS) {
437
+ const oldest = verificationUserRooms.keys().next().value;
438
+ if (typeof oldest === "string") {
439
+ verificationUserRooms.delete(oldest);
440
+ }
441
+ }
442
+ }
443
+
444
+ async function resolveSummaryRoomId(
445
+ summary: MatrixVerificationSummaryLike,
446
+ ): Promise<string | null> {
447
+ const mappedRoomId =
448
+ trimMaybeString(summary.roomId) ??
449
+ trimMaybeString(
450
+ summary.transactionId ? verificationFlowRooms.get(summary.transactionId) : null,
451
+ ) ??
452
+ trimMaybeString(verificationFlowRooms.get(summary.id));
453
+ if (mappedRoomId) {
454
+ return mappedRoomId;
455
+ }
456
+
457
+ const remoteUserId = trimMaybeString(summary.otherUserId);
458
+ if (!remoteUserId) {
459
+ return null;
460
+ }
461
+ const recentRoomId = trimMaybeString(verificationUserRooms.get(remoteUserId));
462
+ const activeRoomId = await resolveActiveDirectRoomId(remoteUserId);
463
+ if (recentRoomId && activeRoomId && recentRoomId === activeRoomId) {
464
+ return recentRoomId;
465
+ }
466
+ if (activeRoomId) {
467
+ return activeRoomId;
468
+ }
469
+ if (
470
+ recentRoomId &&
471
+ (await (
472
+ await loadMatrixDirectRoomDeps()
473
+ ).isStrictDirectRoom({
474
+ client: params.client,
475
+ roomId: recentRoomId,
476
+ remoteUserId,
477
+ }))
478
+ ) {
479
+ return recentRoomId;
480
+ }
481
+ return null;
482
+ }
483
+
484
+ async function routeVerificationSummary(summary: MatrixVerificationSummaryLike): Promise<void> {
485
+ const roomId = await resolveSummaryRoomId(summary);
486
+ if (!roomId || !isActiveVerificationSummary(summary)) {
487
+ return;
488
+ }
489
+ if (
490
+ !(await (
491
+ await loadMatrixDirectRoomDeps()
492
+ ).isStrictDirectRoom({
493
+ client: params.client,
494
+ roomId,
495
+ remoteUserId: summary.otherUserId,
496
+ }))
497
+ ) {
498
+ params.logVerboseMessage(
499
+ `matrix: ignoring verification summary outside strict DM room=${roomId} sender=${summary.otherUserId}`,
500
+ );
501
+ return;
502
+ }
503
+ if (
504
+ !(await isVerificationNoticeAuthorized({
505
+ senderId: summary.otherUserId,
506
+ allowFrom: params.allowFrom,
507
+ dmEnabled: params.dmEnabled,
508
+ dmPolicy: params.dmPolicy,
509
+ readStoreAllowFrom: params.readStoreAllowFrom,
510
+ logVerboseMessage: params.logVerboseMessage,
511
+ }))
512
+ ) {
513
+ return;
514
+ }
515
+ const sasNotice = formatVerificationSasNotice(summary);
516
+ if (!sasNotice) {
517
+ return;
518
+ }
519
+ const sasFingerprint = `${summary.id}:${JSON.stringify(summary.sas)}`;
520
+ if (!trackBounded(routedVerificationSasFingerprints, sasFingerprint)) {
521
+ return;
522
+ }
523
+ await sendVerificationNotice({
524
+ client: params.client,
525
+ roomId,
526
+ body: sasNotice,
527
+ logVerboseMessage: params.logVerboseMessage,
528
+ });
529
+ }
530
+
531
+ function routeVerificationEvent(roomId: string, event: MatrixRawEvent): boolean {
532
+ const senderId = trimMaybeString(event?.sender);
533
+ if (!senderId) {
534
+ return false;
535
+ }
536
+ const signal = readVerificationSignal(event);
537
+ if (!signal) {
538
+ return false;
539
+ }
540
+ rememberVerificationRoom(roomId, event, signal.flowId);
541
+
542
+ void (async () => {
543
+ if (!shouldEmitVerificationEventNotice(event)) {
544
+ params.logVerboseMessage(
545
+ `matrix: ignoring historical verification event room=${roomId} id=${event.event_id ?? "unknown"} type=${event.type ?? "unknown"}`,
546
+ );
547
+ return;
548
+ }
549
+ const flowId = signal.flowId;
550
+ const sourceEventId = trimMaybeString(event?.event_id);
551
+ const sourceFingerprint = sourceEventId ?? `${senderId}:${event.type}:${flowId ?? "none"}`;
552
+ const shouldRouteInRoom = await (
553
+ await loadMatrixDirectRoomDeps()
554
+ ).isStrictDirectRoom({
555
+ client: params.client,
556
+ roomId,
557
+ remoteUserId: senderId,
558
+ });
559
+ if (!shouldRouteInRoom) {
560
+ params.logVerboseMessage(
561
+ `matrix: ignoring verification event outside strict DM room=${roomId} sender=${senderId}`,
562
+ );
563
+ return;
564
+ }
565
+ if (
566
+ !(await isVerificationNoticeAuthorized({
567
+ senderId,
568
+ allowFrom: params.allowFrom,
569
+ dmEnabled: params.dmEnabled,
570
+ dmPolicy: params.dmPolicy,
571
+ readStoreAllowFrom: params.readStoreAllowFrom,
572
+ logVerboseMessage: params.logVerboseMessage,
573
+ }))
574
+ ) {
575
+ return;
576
+ }
577
+ rememberVerificationUserRoom(senderId, roomId);
578
+ if (!trackBounded(routedVerificationEvents, sourceFingerprint)) {
579
+ return;
580
+ }
581
+
582
+ const stageNotice = formatVerificationStageNotice({ stage: signal.stage, senderId, event });
583
+ const { summary, sasNotice } = await resolveVerificationSasNoticeForSignal(params.client, {
584
+ roomId,
585
+ event,
586
+ senderId,
587
+ flowId,
588
+ stage: signal.stage,
589
+ }).catch(() => ({ summary: null, sasNotice: null }));
590
+
591
+ const notices: string[] = [];
592
+ if (stageNotice) {
593
+ const stageKey = `${roomId}:${senderId}:${flowId ?? sourceFingerprint}:${signal.stage}`;
594
+ if (trackBounded(routedVerificationStageNotices, stageKey)) {
595
+ notices.push(stageNotice);
596
+ }
597
+ }
598
+ if (summary && sasNotice) {
599
+ const sasFingerprint = `${summary.id}:${JSON.stringify(summary.sas)}`;
600
+ if (trackBounded(routedVerificationSasFingerprints, sasFingerprint)) {
601
+ notices.push(sasNotice);
602
+ }
603
+ }
604
+ if (notices.length === 0) {
605
+ return;
606
+ }
607
+
608
+ for (const body of notices) {
609
+ await sendVerificationNotice({
610
+ client: params.client,
611
+ roomId,
612
+ body,
613
+ logVerboseMessage: params.logVerboseMessage,
614
+ });
615
+ }
616
+ })().catch((err) => {
617
+ params.logVerboseMessage(`matrix: failed routing verification event: ${String(err)}`);
618
+ });
619
+
620
+ return true;
621
+ }
622
+
623
+ return {
624
+ routeVerificationEvent,
625
+ routeVerificationSummary,
626
+ };
627
+ }
@@ -0,0 +1,47 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ isMatrixVerificationEventType,
4
+ isMatrixVerificationNoticeBody,
5
+ isMatrixVerificationRequestMsgType,
6
+ isMatrixVerificationRoomMessage,
7
+ } from "./verification-utils.js";
8
+
9
+ describe("matrix verification message classifiers", () => {
10
+ it("recognizes verification event types", () => {
11
+ expect(isMatrixVerificationEventType("m.key.verification.start")).toBe(true);
12
+ expect(isMatrixVerificationEventType("m.room.message")).toBe(false);
13
+ });
14
+
15
+ it("recognizes verification request message type", () => {
16
+ expect(isMatrixVerificationRequestMsgType("m.key.verification.request")).toBe(true);
17
+ expect(isMatrixVerificationRequestMsgType("m.text")).toBe(false);
18
+ });
19
+
20
+ it("recognizes verification notice bodies", () => {
21
+ expect(
22
+ isMatrixVerificationNoticeBody("Matrix verification started with @alice:example.org."),
23
+ ).toBe(true);
24
+ expect(isMatrixVerificationNoticeBody("hello world")).toBe(false);
25
+ });
26
+
27
+ it("classifies verification room messages", () => {
28
+ expect(
29
+ isMatrixVerificationRoomMessage({
30
+ msgtype: "m.key.verification.request",
31
+ body: "verify request",
32
+ }),
33
+ ).toBe(true);
34
+ expect(
35
+ isMatrixVerificationRoomMessage({
36
+ msgtype: "m.notice",
37
+ body: "Matrix verification cancelled by @alice:example.org.",
38
+ }),
39
+ ).toBe(true);
40
+ expect(
41
+ isMatrixVerificationRoomMessage({
42
+ msgtype: "m.text",
43
+ body: "normal chat message",
44
+ }),
45
+ ).toBe(false);
46
+ });
47
+ });