@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,428 @@
1
+ import MarkdownIt from "markdown-it";
2
+ import {
3
+ isAutoLinkedFileRef,
4
+ normalizeLowercaseStringOrEmpty,
5
+ } from "openclaw/plugin-sdk/text-runtime";
6
+ import type { MatrixClient } from "./sdk.js";
7
+ import { isMatrixQualifiedUserId } from "./target-ids.js";
8
+
9
+ const md = new MarkdownIt({
10
+ html: false,
11
+ linkify: true,
12
+ breaks: true,
13
+ typographer: false,
14
+ });
15
+
16
+ md.enable("strikethrough");
17
+
18
+ const { escapeHtml } = md.utils;
19
+
20
+ export type MatrixMentions = {
21
+ room?: boolean;
22
+ user_ids?: string[];
23
+ };
24
+
25
+ type MarkdownToken = ReturnType<typeof md.parse>[number];
26
+ type MarkdownInlineToken = NonNullable<MarkdownToken["children"]>[number];
27
+ type MatrixMentionCandidate = {
28
+ raw: string;
29
+ start: number;
30
+ end: number;
31
+ kind: "room" | "user";
32
+ userId?: string;
33
+ };
34
+
35
+ const ESCAPED_MENTION_SENTINEL = "\uE000";
36
+ const MENTION_PATTERN = /@[A-Za-z0-9._=+\-/:[\]]+/g;
37
+ const MATRIX_MENTION_USER_ID_PATTERN =
38
+ /^@[A-Za-z0-9._=+\-/]+:(?:[A-Za-z0-9.-]+|\[[0-9A-Fa-f:.]+\])(?::\d+)?$/;
39
+ const TRIMMABLE_MENTION_SUFFIX = /[),.!?:;\]]/;
40
+
41
+ function shouldSuppressAutoLink(
42
+ tokens: Parameters<NonNullable<typeof md.renderer.rules.link_open>>[0],
43
+ idx: number,
44
+ ): boolean {
45
+ const token = tokens[idx];
46
+ if (token?.type !== "link_open" || token.info !== "auto") {
47
+ return false;
48
+ }
49
+ const href = token.attrGet("href") ?? "";
50
+ const label = tokens[idx + 1]?.type === "text" ? (tokens[idx + 1]?.content ?? "") : "";
51
+ return Boolean(href && label && isAutoLinkedFileRef(href, label));
52
+ }
53
+
54
+ md.renderer.rules.image = (tokens, idx) => escapeHtml(tokens[idx]?.content ?? "");
55
+
56
+ md.renderer.rules.html_block = (tokens, idx) => escapeHtml(tokens[idx]?.content ?? "");
57
+ md.renderer.rules.html_inline = (tokens, idx) => escapeHtml(tokens[idx]?.content ?? "");
58
+ md.renderer.rules.link_open = (tokens, idx, _options, _env, self) =>
59
+ shouldSuppressAutoLink(tokens, idx) ? "" : self.renderToken(tokens, idx, _options);
60
+ md.renderer.rules.link_close = (tokens, idx, _options, _env, self) => {
61
+ const openIdx = idx - 2;
62
+ if (openIdx >= 0 && shouldSuppressAutoLink(tokens, openIdx)) {
63
+ return "";
64
+ }
65
+ return self.renderToken(tokens, idx, _options);
66
+ };
67
+
68
+ function maskEscapedMentions(markdown: string): string {
69
+ let masked = "";
70
+ let idx = 0;
71
+ let codeFenceLength = 0;
72
+
73
+ while (idx < markdown.length) {
74
+ if (markdown[idx] === "`" && !isMarkdownEscaped(markdown, idx)) {
75
+ let runLength = 1;
76
+ while (markdown[idx + runLength] === "`") {
77
+ runLength += 1;
78
+ }
79
+ if (codeFenceLength === 0) {
80
+ codeFenceLength = runLength;
81
+ } else if (runLength === codeFenceLength) {
82
+ codeFenceLength = 0;
83
+ }
84
+ masked += markdown.slice(idx, idx + runLength);
85
+ idx += runLength;
86
+ continue;
87
+ }
88
+ if (codeFenceLength === 0 && markdown[idx] === "\\" && markdown[idx + 1] === "@") {
89
+ masked += ESCAPED_MENTION_SENTINEL;
90
+ idx += 2;
91
+ continue;
92
+ }
93
+ masked += markdown[idx] ?? "";
94
+ idx += 1;
95
+ }
96
+
97
+ return masked;
98
+ }
99
+
100
+ function isMarkdownEscaped(markdown: string, idx: number): boolean {
101
+ let slashCount = 0;
102
+ let cursor = idx - 1;
103
+ while (cursor >= 0 && markdown[cursor] === "\\") {
104
+ slashCount += 1;
105
+ cursor -= 1;
106
+ }
107
+ return slashCount % 2 === 1;
108
+ }
109
+
110
+ function restoreEscapedMentions(text: string): string {
111
+ return text.replaceAll(ESCAPED_MENTION_SENTINEL, "@");
112
+ }
113
+
114
+ function restoreEscapedMentionsInCode(text: string): string {
115
+ return text.replaceAll(ESCAPED_MENTION_SENTINEL, "\\@");
116
+ }
117
+
118
+ function restoreEscapedMentionsInBlockTokens(tokens: MarkdownToken[]): void {
119
+ for (const token of tokens) {
120
+ if ((token.type === "fence" || token.type === "code_block") && token.content) {
121
+ token.content = restoreEscapedMentionsInCode(token.content);
122
+ }
123
+ }
124
+ }
125
+
126
+ function isMentionStartBoundary(charBefore: string | undefined): boolean {
127
+ return !charBefore || !/[A-Za-z0-9_]/.test(charBefore);
128
+ }
129
+
130
+ function trimMentionSuffix(raw: string, end: number): { raw: string; end: number } | null {
131
+ while (raw.length > 1 && TRIMMABLE_MENTION_SUFFIX.test(raw.at(-1) ?? "")) {
132
+ if (raw.at(-1) === "]" && /\[[0-9A-Fa-f:.]+\](?::\d+)?$/i.test(raw)) {
133
+ break;
134
+ }
135
+ raw = raw.slice(0, -1);
136
+ end -= 1;
137
+ }
138
+ if (!raw.startsWith("@") || raw === "@") {
139
+ return null;
140
+ }
141
+ return { raw, end };
142
+ }
143
+
144
+ function isMatrixMentionUserId(raw: string): boolean {
145
+ return isMatrixQualifiedUserId(raw) && MATRIX_MENTION_USER_ID_PATTERN.test(raw);
146
+ }
147
+
148
+ function buildMentionCandidate(raw: string, start: number): MatrixMentionCandidate | null {
149
+ const normalized = trimMentionSuffix(raw, start + raw.length);
150
+ if (!normalized) {
151
+ return null;
152
+ }
153
+ const kind = normalizeLowercaseStringOrEmpty(normalized.raw) === "@room" ? "room" : "user";
154
+ const base: MatrixMentionCandidate = {
155
+ raw: normalized.raw,
156
+ start,
157
+ end: normalized.end,
158
+ kind,
159
+ };
160
+ if (kind === "room") {
161
+ return base;
162
+ }
163
+ const userCandidate = isMatrixMentionUserId(normalized.raw)
164
+ ? { ...base, userId: normalized.raw }
165
+ : null;
166
+ if (!userCandidate) {
167
+ return null;
168
+ }
169
+ return userCandidate;
170
+ }
171
+
172
+ function collectMentionCandidates(text: string): MatrixMentionCandidate[] {
173
+ const mentions: MatrixMentionCandidate[] = [];
174
+ for (const match of text.matchAll(MENTION_PATTERN)) {
175
+ const raw = match[0];
176
+ const start = match.index ?? -1;
177
+ if (start < 0 || !raw) {
178
+ continue;
179
+ }
180
+ if (!isMentionStartBoundary(text[start - 1])) {
181
+ continue;
182
+ }
183
+ const candidate = buildMentionCandidate(raw, start);
184
+ if (!candidate) {
185
+ continue;
186
+ }
187
+ mentions.push(candidate);
188
+ }
189
+ return mentions;
190
+ }
191
+
192
+ function createToken(
193
+ sample: MarkdownInlineToken,
194
+ type: string,
195
+ tag: string,
196
+ nesting: number,
197
+ ): MarkdownInlineToken {
198
+ const TokenCtor = sample.constructor as new (
199
+ type: string,
200
+ tag: string,
201
+ nesting: number,
202
+ ) => MarkdownInlineToken;
203
+ return new TokenCtor(type, tag, nesting);
204
+ }
205
+
206
+ function createTextToken(sample: MarkdownInlineToken, content: string): MarkdownInlineToken {
207
+ const token = createToken(sample, "text", "", 0);
208
+ token.content = content;
209
+ return token;
210
+ }
211
+
212
+ function createMentionLinkTokens(params: {
213
+ sample: MarkdownInlineToken;
214
+ href: string;
215
+ label: string;
216
+ }): MarkdownInlineToken[] {
217
+ const open = createToken(params.sample, "link_open", "a", 1);
218
+ open.attrSet("href", params.href);
219
+ const text = createTextToken(params.sample, params.label);
220
+ const close = createToken(params.sample, "link_close", "a", -1);
221
+ return [open, text, close];
222
+ }
223
+
224
+ function resolveMentionUserId(match: MatrixMentionCandidate): string | null {
225
+ if (match.kind !== "user") {
226
+ return null;
227
+ }
228
+ return match.userId ?? null;
229
+ }
230
+
231
+ async function resolveMatrixSelfUserId(client: MatrixClient): Promise<string | null> {
232
+ const getUserId = (client as { getUserId?: () => Promise<string> | string }).getUserId;
233
+ if (typeof getUserId !== "function") {
234
+ return null;
235
+ }
236
+ return await Promise.resolve(getUserId.call(client)).catch(() => null);
237
+ }
238
+
239
+ function mutateInlineTokensWithMentions(params: {
240
+ children: MarkdownInlineToken[];
241
+ userIds: string[];
242
+ seenUserIds: Set<string>;
243
+ selfUserId: string | null;
244
+ }): { children: MarkdownInlineToken[]; roomMentioned: boolean } {
245
+ const nextChildren: MarkdownInlineToken[] = [];
246
+ let roomMentioned = false;
247
+ let insideLinkDepth = 0;
248
+ for (const child of params.children) {
249
+ if (child.type === "link_open") {
250
+ insideLinkDepth += 1;
251
+ nextChildren.push(child);
252
+ continue;
253
+ }
254
+ if (child.type === "link_close") {
255
+ insideLinkDepth = Math.max(0, insideLinkDepth - 1);
256
+ nextChildren.push(child);
257
+ continue;
258
+ }
259
+ if (child.type !== "text" || !child.content) {
260
+ nextChildren.push(child);
261
+ continue;
262
+ }
263
+
264
+ const visibleContent = restoreEscapedMentions(child.content);
265
+ if (insideLinkDepth > 0) {
266
+ nextChildren.push(createTextToken(child, visibleContent));
267
+ continue;
268
+ }
269
+ const matches = collectMentionCandidates(child.content);
270
+ if (matches.length === 0) {
271
+ nextChildren.push(createTextToken(child, visibleContent));
272
+ continue;
273
+ }
274
+
275
+ let cursor = 0;
276
+ for (const match of matches) {
277
+ if (match.start > cursor) {
278
+ nextChildren.push(
279
+ createTextToken(child, restoreEscapedMentions(child.content.slice(cursor, match.start))),
280
+ );
281
+ }
282
+ cursor = match.end;
283
+ if (match.kind === "room") {
284
+ roomMentioned = true;
285
+ nextChildren.push(createTextToken(child, match.raw));
286
+ continue;
287
+ }
288
+
289
+ const resolvedUserId = resolveMentionUserId(match);
290
+ if (!resolvedUserId || resolvedUserId === params.selfUserId) {
291
+ nextChildren.push(createTextToken(child, match.raw));
292
+ continue;
293
+ }
294
+ if (!params.seenUserIds.has(resolvedUserId)) {
295
+ params.seenUserIds.add(resolvedUserId);
296
+ params.userIds.push(resolvedUserId);
297
+ }
298
+ nextChildren.push(
299
+ ...createMentionLinkTokens({
300
+ sample: child,
301
+ href: `https://matrix.to/#/${encodeURIComponent(resolvedUserId)}`,
302
+ label: match.raw,
303
+ }),
304
+ );
305
+ }
306
+ if (cursor < child.content.length) {
307
+ nextChildren.push(
308
+ createTextToken(child, restoreEscapedMentions(child.content.slice(cursor))),
309
+ );
310
+ }
311
+ }
312
+ return { children: nextChildren, roomMentioned };
313
+ }
314
+
315
+ // Compact loose lists by hiding a list item's single wrapper paragraph,
316
+ // mirroring what markdown-it already does for tight lists. Without this
317
+ // Element renders <p> margins inside <li>, splitting numbers from content.
318
+ //
319
+ // Keep multi-paragraph items visible so separate paragraphs do not collapse
320
+ // together inside the same list item.
321
+ function compactLooseListTokens(tokens: MarkdownToken[]): void {
322
+ const listItemStack: Array<{
323
+ level: number;
324
+ immediateParagraphOpenIndexes: number[];
325
+ immediateParagraphCloseIndexes: number[];
326
+ }> = [];
327
+
328
+ for (const [index, token] of tokens.entries()) {
329
+ if (token.type === "list_item_open") {
330
+ listItemStack.push({
331
+ level: token.level,
332
+ immediateParagraphOpenIndexes: [],
333
+ immediateParagraphCloseIndexes: [],
334
+ });
335
+ continue;
336
+ }
337
+
338
+ if (token.type === "list_item_close") {
339
+ const item = listItemStack.pop();
340
+ if (
341
+ item &&
342
+ item.immediateParagraphOpenIndexes.length === 1 &&
343
+ item.immediateParagraphCloseIndexes.length === 1
344
+ ) {
345
+ tokens[item.immediateParagraphOpenIndexes[0]].hidden = true;
346
+ tokens[item.immediateParagraphCloseIndexes[0]].hidden = true;
347
+ }
348
+ continue;
349
+ }
350
+
351
+ const currentItem = listItemStack.at(-1);
352
+ if (!currentItem || token.level !== currentItem.level + 1) {
353
+ continue;
354
+ }
355
+
356
+ if (token.type === "paragraph_open") {
357
+ currentItem.immediateParagraphOpenIndexes.push(index);
358
+ } else if (token.type === "paragraph_close") {
359
+ currentItem.immediateParagraphCloseIndexes.push(index);
360
+ }
361
+ }
362
+ }
363
+
364
+ export function markdownToMatrixHtml(markdown: string): string {
365
+ const tokens = md.parse(markdown ?? "", {});
366
+ compactLooseListTokens(tokens);
367
+ return md.renderer.render(tokens, md.options, {}).trimEnd();
368
+ }
369
+
370
+ async function resolveMarkdownMentionState(params: {
371
+ markdown: string;
372
+ client: MatrixClient;
373
+ }): Promise<{ tokens: MarkdownToken[]; mentions: MatrixMentions }> {
374
+ const markdown = maskEscapedMentions(params.markdown ?? "");
375
+ const tokens = md.parse(markdown, {});
376
+ restoreEscapedMentionsInBlockTokens(tokens);
377
+ const selfUserId = await resolveMatrixSelfUserId(params.client);
378
+ const userIds: string[] = [];
379
+ const seenUserIds = new Set<string>();
380
+ let roomMentioned = false;
381
+
382
+ for (const token of tokens) {
383
+ if (!token.children?.length) {
384
+ continue;
385
+ }
386
+ const mutated = mutateInlineTokensWithMentions({
387
+ children: token.children,
388
+ userIds,
389
+ seenUserIds,
390
+ selfUserId,
391
+ });
392
+ token.children = mutated.children;
393
+ roomMentioned ||= mutated.roomMentioned;
394
+ }
395
+
396
+ const mentions: MatrixMentions = {};
397
+ if (userIds.length > 0) {
398
+ mentions.user_ids = userIds;
399
+ }
400
+ if (roomMentioned) {
401
+ mentions.room = true;
402
+ }
403
+ return {
404
+ tokens,
405
+ mentions,
406
+ };
407
+ }
408
+
409
+ export async function resolveMatrixMentionsInMarkdown(params: {
410
+ markdown: string;
411
+ client: MatrixClient;
412
+ }): Promise<MatrixMentions> {
413
+ const state = await resolveMarkdownMentionState(params);
414
+ return state.mentions;
415
+ }
416
+
417
+ export async function renderMarkdownToMatrixHtmlWithMentions(params: {
418
+ markdown: string;
419
+ client: MatrixClient;
420
+ }): Promise<{ html?: string; mentions: MatrixMentions }> {
421
+ const state = await resolveMarkdownMentionState(params);
422
+ compactLooseListTokens(state.tokens);
423
+ const html = md.renderer.render(state.tokens, md.options, {}).trimEnd();
424
+ return {
425
+ html: html || undefined,
426
+ mentions: state.mentions,
427
+ };
428
+ }
@@ -0,0 +1,95 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import { createRequire } from "node:module";
4
+ import path from "node:path";
5
+ import { ensureMatrixCryptoRuntime } from "./deps.js";
6
+
7
+ export type MatrixLegacyCryptoInspectionResult = {
8
+ deviceId: string | null;
9
+ roomKeyCounts: {
10
+ total: number;
11
+ backedUp: number;
12
+ } | null;
13
+ backupVersion: string | null;
14
+ decryptionKeyBase64: string | null;
15
+ };
16
+
17
+ function resolveLegacyMachineStorePath(params: {
18
+ cryptoRootDir: string;
19
+ deviceId: string;
20
+ }): string | null {
21
+ const hashedDir = path.join(
22
+ params.cryptoRootDir,
23
+ crypto.createHash("sha256").update(params.deviceId).digest("hex"),
24
+ );
25
+ if (fs.existsSync(path.join(hashedDir, "matrix-sdk-crypto.sqlite3"))) {
26
+ return hashedDir;
27
+ }
28
+ if (fs.existsSync(path.join(params.cryptoRootDir, "matrix-sdk-crypto.sqlite3"))) {
29
+ return params.cryptoRootDir;
30
+ }
31
+ const match = fs
32
+ .readdirSync(params.cryptoRootDir, { withFileTypes: true })
33
+ .find(
34
+ (entry) =>
35
+ entry.isDirectory() &&
36
+ fs.existsSync(path.join(params.cryptoRootDir, entry.name, "matrix-sdk-crypto.sqlite3")),
37
+ );
38
+ return match ? path.join(params.cryptoRootDir, match.name) : null;
39
+ }
40
+
41
+ export async function inspectLegacyMatrixCryptoStore(params: {
42
+ cryptoRootDir: string;
43
+ userId: string;
44
+ deviceId: string;
45
+ log?: (message: string) => void;
46
+ }): Promise<MatrixLegacyCryptoInspectionResult> {
47
+ const machineStorePath = resolveLegacyMachineStorePath(params);
48
+ if (!machineStorePath) {
49
+ throw new Error(`Matrix legacy crypto store not found for device ${params.deviceId}`);
50
+ }
51
+
52
+ const requireFn = createRequire(import.meta.url);
53
+ await ensureMatrixCryptoRuntime({
54
+ requireFn,
55
+ resolveFn: requireFn.resolve.bind(requireFn),
56
+ log: params.log,
57
+ });
58
+
59
+ const { DeviceId, OlmMachine, StoreType, UserId } = requireFn(
60
+ "@matrix-org/matrix-sdk-crypto-nodejs",
61
+ ) as typeof import("@matrix-org/matrix-sdk-crypto-nodejs");
62
+ const machine = await OlmMachine.initialize(
63
+ new UserId(params.userId),
64
+ new DeviceId(params.deviceId),
65
+ machineStorePath,
66
+ "",
67
+ StoreType.Sqlite,
68
+ );
69
+
70
+ try {
71
+ const [backupKeys, roomKeyCounts] = await Promise.all([
72
+ machine.getBackupKeys(),
73
+ machine.roomKeyCounts(),
74
+ ]);
75
+ return {
76
+ deviceId: params.deviceId,
77
+ roomKeyCounts: roomKeyCounts
78
+ ? {
79
+ total: typeof roomKeyCounts.total === "number" ? roomKeyCounts.total : 0,
80
+ backedUp: typeof roomKeyCounts.backedUp === "number" ? roomKeyCounts.backedUp : 0,
81
+ }
82
+ : null,
83
+ backupVersion:
84
+ typeof backupKeys?.backupVersion === "string" && backupKeys.backupVersion.trim()
85
+ ? backupKeys.backupVersion
86
+ : null,
87
+ decryptionKeyBase64:
88
+ typeof backupKeys?.decryptionKeyBase64 === "string" && backupKeys.decryptionKeyBase64.trim()
89
+ ? backupKeys.decryptionKeyBase64
90
+ : null,
91
+ };
92
+ } finally {
93
+ machine.close();
94
+ }
95
+ }
@@ -0,0 +1,20 @@
1
+ export const MATRIX_MEDIA_SIZE_LIMIT_ERROR_MESSAGE = "Matrix media exceeds configured size limit";
2
+
3
+ export class MatrixMediaSizeLimitError extends Error {
4
+ readonly code = "MATRIX_MEDIA_SIZE_LIMIT" as const;
5
+
6
+ constructor(message = MATRIX_MEDIA_SIZE_LIMIT_ERROR_MESSAGE, options?: ErrorOptions) {
7
+ super(message, options);
8
+ this.name = "MatrixMediaSizeLimitError";
9
+ }
10
+ }
11
+
12
+ export function isMatrixMediaSizeLimitError(err: unknown): err is MatrixMediaSizeLimitError {
13
+ if (err instanceof MatrixMediaSizeLimitError) {
14
+ return true;
15
+ }
16
+ if (!(err instanceof Error) || err.cause === undefined) {
17
+ return false;
18
+ }
19
+ return isMatrixMediaSizeLimitError(err.cause);
20
+ }
@@ -0,0 +1,169 @@
1
+ import path from "node:path";
2
+ import type {
3
+ MatrixMessageAttachmentKind,
4
+ MatrixMessageAttachmentSummary,
5
+ MatrixMessageSummary,
6
+ } from "./actions/types.js";
7
+
8
+ const MATRIX_MEDIA_KINDS: Record<string, MatrixMessageAttachmentKind> = {
9
+ "m.audio": "audio",
10
+ "m.file": "file",
11
+ "m.image": "image",
12
+ "m.sticker": "sticker",
13
+ "m.video": "video",
14
+ };
15
+
16
+ function resolveMatrixMediaKind(msgtype: string | undefined): MatrixMessageAttachmentKind | null {
17
+ return MATRIX_MEDIA_KINDS[msgtype ?? ""] ?? null;
18
+ }
19
+
20
+ function resolveMatrixMediaLabel(
21
+ kind: MatrixMessageAttachmentKind | undefined,
22
+ fallback = "media",
23
+ ): string {
24
+ return `${kind ?? fallback} attachment`;
25
+ }
26
+
27
+ function formatMatrixAttachmentMarker(params: {
28
+ kind?: MatrixMessageAttachmentKind;
29
+ tooLarge?: boolean;
30
+ unavailable?: boolean;
31
+ }): string {
32
+ const label = resolveMatrixMediaLabel(params.kind);
33
+ if (params.tooLarge) {
34
+ return `[matrix ${label} too large]`;
35
+ }
36
+ return params.unavailable ? `[matrix ${label} unavailable]` : `[matrix ${label}]`;
37
+ }
38
+
39
+ export function isLikelyBareFilename(text: string): boolean {
40
+ const trimmed = text.trim();
41
+ if (!trimmed || trimmed.includes("\n") || /\s/.test(trimmed)) {
42
+ return false;
43
+ }
44
+ if (path.basename(trimmed) !== trimmed) {
45
+ return false;
46
+ }
47
+ return path.extname(trimmed).length > 1;
48
+ }
49
+
50
+ function resolveCaptionOrFilename(params: { body?: string; filename?: string }): {
51
+ caption?: string;
52
+ filename?: string;
53
+ } {
54
+ const body = params.body?.trim() ?? "";
55
+ const filename = params.filename?.trim() ?? "";
56
+ if (filename) {
57
+ if (!body || body === filename) {
58
+ return { filename };
59
+ }
60
+ return { caption: body, filename };
61
+ }
62
+ if (!body) {
63
+ return {};
64
+ }
65
+ if (isLikelyBareFilename(body)) {
66
+ return { filename: body };
67
+ }
68
+ return { caption: body };
69
+ }
70
+
71
+ export function resolveMatrixMessageAttachment(params: {
72
+ body?: string;
73
+ filename?: string;
74
+ msgtype?: string;
75
+ }): MatrixMessageAttachmentSummary | undefined {
76
+ const kind = resolveMatrixMediaKind(params.msgtype);
77
+ if (!kind) {
78
+ return undefined;
79
+ }
80
+ const resolved = resolveCaptionOrFilename(params);
81
+ return {
82
+ kind,
83
+ caption: resolved.caption,
84
+ filename: resolved.filename,
85
+ };
86
+ }
87
+
88
+ export function resolveMatrixMessageBody(params: {
89
+ body?: string;
90
+ filename?: string;
91
+ msgtype?: string;
92
+ }): string | undefined {
93
+ const attachment = resolveMatrixMessageAttachment(params);
94
+ if (!attachment) {
95
+ const body = params.body?.trim() ?? "";
96
+ return body || undefined;
97
+ }
98
+ return attachment.caption;
99
+ }
100
+
101
+ export function formatMatrixAttachmentText(params: {
102
+ attachment?: MatrixMessageAttachmentSummary;
103
+ tooLarge?: boolean;
104
+ unavailable?: boolean;
105
+ }): string | undefined {
106
+ if (!params.attachment) {
107
+ return undefined;
108
+ }
109
+ return formatMatrixAttachmentMarker({
110
+ kind: params.attachment.kind,
111
+ tooLarge: params.tooLarge,
112
+ unavailable: params.unavailable,
113
+ });
114
+ }
115
+
116
+ export function formatMatrixMessageText(params: {
117
+ body?: string;
118
+ attachment?: MatrixMessageAttachmentSummary;
119
+ tooLarge?: boolean;
120
+ unavailable?: boolean;
121
+ }): string | undefined {
122
+ const body = params.body?.trim() ?? "";
123
+ const marker = formatMatrixAttachmentText({
124
+ attachment: params.attachment,
125
+ tooLarge: params.tooLarge,
126
+ unavailable: params.unavailable,
127
+ });
128
+ if (!marker) {
129
+ return body || undefined;
130
+ }
131
+ if (!body) {
132
+ return marker;
133
+ }
134
+ return `${body}\n\n${marker}`;
135
+ }
136
+
137
+ export function formatMatrixMessageSummaryText(
138
+ summary: Pick<MatrixMessageSummary, "body" | "attachment">,
139
+ ): string | undefined {
140
+ return formatMatrixMessageText(summary);
141
+ }
142
+
143
+ export function formatMatrixMediaUnavailableText(params: {
144
+ body?: string;
145
+ filename?: string;
146
+ msgtype?: string;
147
+ }): string {
148
+ return (
149
+ formatMatrixMessageText({
150
+ body: resolveMatrixMessageBody(params),
151
+ attachment: resolveMatrixMessageAttachment(params),
152
+ unavailable: true,
153
+ }) ?? ""
154
+ );
155
+ }
156
+
157
+ export function formatMatrixMediaTooLargeText(params: {
158
+ body?: string;
159
+ filename?: string;
160
+ msgtype?: string;
161
+ }): string {
162
+ return (
163
+ formatMatrixMessageText({
164
+ body: resolveMatrixMessageBody(params),
165
+ attachment: resolveMatrixMessageAttachment(params),
166
+ tooLarge: true,
167
+ }) ?? ""
168
+ );
169
+ }