@openclaw/crabline 0.1.11 → 0.1.13

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 (214) hide show
  1. package/README.md +137 -23
  2. package/dist/src/bin/crabline.js +3 -1
  3. package/dist/src/bin/crabline.js.map +1 -1
  4. package/dist/src/cli/program.d.ts +5 -1
  5. package/dist/src/cli/program.js +281 -52
  6. package/dist/src/cli/program.js.map +1 -1
  7. package/dist/src/config/load.js +39 -3
  8. package/dist/src/config/load.js.map +1 -1
  9. package/dist/src/config/schema.d.ts +20 -0
  10. package/dist/src/config/schema.js +163 -35
  11. package/dist/src/config/schema.js.map +1 -1
  12. package/dist/src/core/errors.d.ts +1 -0
  13. package/dist/src/core/errors.js +6 -3
  14. package/dist/src/core/errors.js.map +1 -1
  15. package/dist/src/core/http-path.d.ts +1 -0
  16. package/dist/src/core/http-path.js +14 -0
  17. package/dist/src/core/http-path.js.map +1 -0
  18. package/dist/src/core/matcher.d.ts +3 -1
  19. package/dist/src/core/matcher.js +58 -2
  20. package/dist/src/core/matcher.js.map +1 -1
  21. package/dist/src/core/nonces.d.ts +2 -0
  22. package/dist/src/core/nonces.js +11 -1
  23. package/dist/src/core/nonces.js.map +1 -1
  24. package/dist/src/core/reporters.d.ts +5 -0
  25. package/dist/src/core/reporters.js +26 -2
  26. package/dist/src/core/reporters.js.map +1 -1
  27. package/dist/src/core/run.d.ts +3 -1
  28. package/dist/src/core/run.js +178 -66
  29. package/dist/src/core/run.js.map +1 -1
  30. package/dist/src/index.d.ts +12 -10
  31. package/dist/src/index.js +6 -5
  32. package/dist/src/index.js.map +1 -1
  33. package/dist/src/matrix-ids.d.ts +5 -0
  34. package/dist/src/matrix-ids.js +83 -0
  35. package/dist/src/matrix-ids.js.map +1 -0
  36. package/dist/src/openclaw/artifact-generation.d.ts +7 -6
  37. package/dist/src/openclaw/artifact-generation.js +385 -41
  38. package/dist/src/openclaw/artifact-generation.js.map +1 -1
  39. package/dist/src/openclaw/bridges/discord.d.ts +1 -0
  40. package/dist/src/openclaw/bridges/discord.js +149 -0
  41. package/dist/src/openclaw/bridges/discord.js.map +1 -0
  42. package/dist/src/openclaw/bridges/matrix.js +22 -5
  43. package/dist/src/openclaw/bridges/matrix.js.map +1 -1
  44. package/dist/src/openclaw/bridges/mattermost.js +14 -9
  45. package/dist/src/openclaw/bridges/mattermost.js.map +1 -1
  46. package/dist/src/openclaw/bridges/probe-response.d.ts +1 -0
  47. package/dist/src/openclaw/bridges/probe-response.js +5 -0
  48. package/dist/src/openclaw/bridges/probe-response.js.map +1 -0
  49. package/dist/src/openclaw/bridges/signal.js +16 -7
  50. package/dist/src/openclaw/bridges/signal.js.map +1 -1
  51. package/dist/src/openclaw/bridges/slack.js +117 -24
  52. package/dist/src/openclaw/bridges/slack.js.map +1 -1
  53. package/dist/src/openclaw/bridges/telegram.js +46 -20
  54. package/dist/src/openclaw/bridges/telegram.js.map +1 -1
  55. package/dist/src/openclaw/bridges/whatsapp.js +33 -17
  56. package/dist/src/openclaw/bridges/whatsapp.js.map +1 -1
  57. package/dist/src/openclaw/bridges/zalo.js +9 -4
  58. package/dist/src/openclaw/bridges/zalo.js.map +1 -1
  59. package/dist/src/openclaw/outbound-contract.js +3 -0
  60. package/dist/src/openclaw/outbound-contract.js.map +1 -1
  61. package/dist/src/openclaw/private-file.d.ts +38 -3
  62. package/dist/src/openclaw/private-file.js +2008 -87
  63. package/dist/src/openclaw/private-file.js.map +1 -1
  64. package/dist/src/openclaw/{smoke-lock.d.ts → provider-readiness-lock.d.ts} +14 -5
  65. package/dist/src/openclaw/provider-readiness-lock.js +1320 -0
  66. package/dist/src/openclaw/provider-readiness-lock.js.map +1 -0
  67. package/dist/src/openclaw/shared.d.ts +10 -17
  68. package/dist/src/openclaw/shared.js +53 -19
  69. package/dist/src/openclaw/shared.js.map +1 -1
  70. package/dist/src/openclaw.d.ts +3 -5
  71. package/dist/src/openclaw.js +245 -60
  72. package/dist/src/openclaw.js.map +1 -1
  73. package/dist/src/platform/process-owned-lock.d.ts +11 -0
  74. package/dist/src/platform/process-owned-lock.js +1698 -0
  75. package/dist/src/platform/process-owned-lock.js.map +1 -0
  76. package/dist/src/platform/recorder-directory.d.ts +1 -0
  77. package/dist/src/platform/recorder-directory.js +14 -0
  78. package/dist/src/platform/recorder-directory.js.map +1 -0
  79. package/dist/src/platform/windows-acl.d.ts +18 -0
  80. package/dist/src/platform/windows-acl.js +1367 -0
  81. package/dist/src/platform/windows-acl.js.map +1 -0
  82. package/dist/src/platform/windows-lock-root.d.ts +13 -0
  83. package/dist/src/platform/windows-lock-root.js +98 -0
  84. package/dist/src/platform/windows-lock-root.js.map +1 -0
  85. package/dist/src/providers/builtin/discord.d.ts +4 -0
  86. package/dist/src/providers/builtin/discord.js +52 -9
  87. package/dist/src/providers/builtin/discord.js.map +1 -1
  88. package/dist/src/providers/builtin/external-webhook-auth.d.ts +1 -0
  89. package/dist/src/providers/builtin/external-webhook-auth.js +38 -1
  90. package/dist/src/providers/builtin/external-webhook-auth.js.map +1 -1
  91. package/dist/src/providers/builtin/feishu.d.ts +1 -0
  92. package/dist/src/providers/builtin/feishu.js +96 -26
  93. package/dist/src/providers/builtin/feishu.js.map +1 -1
  94. package/dist/src/providers/builtin/googlechat.d.ts +1 -0
  95. package/dist/src/providers/builtin/googlechat.js +95 -26
  96. package/dist/src/providers/builtin/googlechat.js.map +1 -1
  97. package/dist/src/providers/builtin/imessage.js +60 -17
  98. package/dist/src/providers/builtin/imessage.js.map +1 -1
  99. package/dist/src/providers/builtin/loopback.js +88 -35
  100. package/dist/src/providers/builtin/loopback.js.map +1 -1
  101. package/dist/src/providers/builtin/matrix.d.ts +2 -2
  102. package/dist/src/providers/builtin/matrix.js +78 -21
  103. package/dist/src/providers/builtin/matrix.js.map +1 -1
  104. package/dist/src/providers/builtin/mattermost.d.ts +11 -7
  105. package/dist/src/providers/builtin/mattermost.js +138 -24
  106. package/dist/src/providers/builtin/mattermost.js.map +1 -1
  107. package/dist/src/providers/builtin/msteams.d.ts +1 -0
  108. package/dist/src/providers/builtin/msteams.js +118 -38
  109. package/dist/src/providers/builtin/msteams.js.map +1 -1
  110. package/dist/src/providers/builtin/native-local-mock.js +4 -4
  111. package/dist/src/providers/builtin/native-local-mock.js.map +1 -1
  112. package/dist/src/providers/builtin/script.js +1057 -146
  113. package/dist/src/providers/builtin/script.js.map +1 -1
  114. package/dist/src/providers/builtin/slack.d.ts +8 -1
  115. package/dist/src/providers/builtin/slack.js +327 -24
  116. package/dist/src/providers/builtin/slack.js.map +1 -1
  117. package/dist/src/providers/builtin/telegram.d.ts +1 -1
  118. package/dist/src/providers/builtin/telegram.js +55 -14
  119. package/dist/src/providers/builtin/telegram.js.map +1 -1
  120. package/dist/src/providers/builtin/whatsapp.d.ts +4 -3
  121. package/dist/src/providers/builtin/whatsapp.js +157 -29
  122. package/dist/src/providers/builtin/whatsapp.js.map +1 -1
  123. package/dist/src/providers/builtin/zalo.d.ts +5 -2
  124. package/dist/src/providers/builtin/zalo.js +31 -7
  125. package/dist/src/providers/builtin/zalo.js.map +1 -1
  126. package/dist/src/providers/catalog.d.ts +1 -1
  127. package/dist/src/providers/catalog.js +1 -1
  128. package/dist/src/providers/catalog.js.map +1 -1
  129. package/dist/src/providers/local-mock.d.ts +4 -0
  130. package/dist/src/providers/local-mock.js +162 -35
  131. package/dist/src/providers/local-mock.js.map +1 -1
  132. package/dist/src/providers/native-ids.d.ts +2 -0
  133. package/dist/src/providers/native-ids.js +3 -0
  134. package/dist/src/providers/native-ids.js.map +1 -1
  135. package/dist/src/providers/recorder.d.ts +14 -3
  136. package/dist/src/providers/recorder.js +521 -41
  137. package/dist/src/providers/recorder.js.map +1 -1
  138. package/dist/src/providers/registry.d.ts +1 -0
  139. package/dist/src/providers/registry.js +65 -20
  140. package/dist/src/providers/registry.js.map +1 -1
  141. package/dist/src/providers/signed-jwt.d.ts +6 -1
  142. package/dist/src/providers/signed-jwt.js +241 -50
  143. package/dist/src/providers/signed-jwt.js.map +1 -1
  144. package/dist/src/providers/slack-ids.d.ts +1 -0
  145. package/dist/src/providers/slack-ids.js +5 -0
  146. package/dist/src/providers/slack-ids.js.map +1 -1
  147. package/dist/src/providers/target-normalizers.d.ts +9 -1
  148. package/dist/src/providers/target-normalizers.js +177 -58
  149. package/dist/src/providers/target-normalizers.js.map +1 -1
  150. package/dist/src/providers/webhook-server.d.ts +3 -0
  151. package/dist/src/providers/webhook-server.js +268 -47
  152. package/dist/src/providers/webhook-server.js.map +1 -1
  153. package/dist/src/servers/discord.d.ts +40 -0
  154. package/dist/src/servers/discord.js +842 -0
  155. package/dist/src/servers/discord.js.map +1 -0
  156. package/dist/src/servers/http.d.ts +14 -2
  157. package/dist/src/servers/http.js +229 -58
  158. package/dist/src/servers/http.js.map +1 -1
  159. package/dist/src/servers/index.d.ts +10 -4
  160. package/dist/src/servers/index.js +4 -0
  161. package/dist/src/servers/index.js.map +1 -1
  162. package/dist/src/servers/matrix.d.ts +3 -0
  163. package/dist/src/servers/matrix.js +268 -110
  164. package/dist/src/servers/matrix.js.map +1 -1
  165. package/dist/src/servers/mattermost.d.ts +5 -0
  166. package/dist/src/servers/mattermost.js +389 -91
  167. package/dist/src/servers/mattermost.js.map +1 -1
  168. package/dist/src/servers/recorder.d.ts +11 -0
  169. package/dist/src/servers/recorder.js +1072 -69
  170. package/dist/src/servers/recorder.js.map +1 -1
  171. package/dist/src/servers/signal.d.ts +1 -0
  172. package/dist/src/servers/signal.js +279 -44
  173. package/dist/src/servers/signal.js.map +1 -1
  174. package/dist/src/servers/slack.d.ts +15 -0
  175. package/dist/src/servers/slack.js +270 -54
  176. package/dist/src/servers/slack.js.map +1 -1
  177. package/dist/src/servers/telegram-identity.d.ts +6 -0
  178. package/dist/src/servers/telegram-identity.js +27 -0
  179. package/dist/src/servers/telegram-identity.js.map +1 -0
  180. package/dist/src/servers/telegram.d.ts +17 -6
  181. package/dist/src/servers/telegram.js +1218 -107
  182. package/dist/src/servers/telegram.js.map +1 -1
  183. package/dist/src/servers/webhook-target.d.ts +12 -4
  184. package/dist/src/servers/webhook-target.js +226 -28
  185. package/dist/src/servers/webhook-target.js.map +1 -1
  186. package/dist/src/servers/whatsapp-baileys-websocket.d.ts +60 -1
  187. package/dist/src/servers/whatsapp-baileys-websocket.js +782 -112
  188. package/dist/src/servers/whatsapp-baileys-websocket.js.map +1 -1
  189. package/dist/src/servers/whatsapp-jid.d.ts +1 -0
  190. package/dist/src/servers/whatsapp-jid.js +9 -5
  191. package/dist/src/servers/whatsapp-jid.js.map +1 -1
  192. package/dist/src/servers/whatsapp-wire/binary-node.d.ts +1 -0
  193. package/dist/src/servers/whatsapp-wire/binary-node.js +37 -13
  194. package/dist/src/servers/whatsapp-wire/binary-node.js.map +1 -1
  195. package/dist/src/servers/whatsapp-wire/crypto.d.ts +3 -0
  196. package/dist/src/servers/whatsapp-wire/crypto.js +23 -8
  197. package/dist/src/servers/whatsapp-wire/crypto.js.map +1 -1
  198. package/dist/src/servers/whatsapp-wire/handshake.d.ts +9 -3
  199. package/dist/src/servers/whatsapp-wire/handshake.js +173 -12
  200. package/dist/src/servers/whatsapp-wire/handshake.js.map +1 -1
  201. package/dist/src/servers/whatsapp-wire/signal.js +3 -3
  202. package/dist/src/servers/whatsapp-wire/signal.js.map +1 -1
  203. package/dist/src/servers/whatsapp.d.ts +7 -1
  204. package/dist/src/servers/whatsapp.js +149 -25
  205. package/dist/src/servers/whatsapp.js.map +1 -1
  206. package/dist/src/servers/zalo.d.ts +1 -0
  207. package/dist/src/servers/zalo.js +193 -82
  208. package/dist/src/servers/zalo.js.map +1 -1
  209. package/docs/channel-setup.md +251 -64
  210. package/fixtures/examples/crabline.example.yaml +3 -2
  211. package/package.json +18 -10
  212. package/dist/src/openclaw/smoke-lock.js +0 -1004
  213. package/dist/src/openclaw/smoke-lock.js.map +0 -1
  214. package/fixtures/examples/openclaw-bridge.yaml +0 -125
@@ -1,96 +1,25 @@
1
1
  import { createHash, randomBytes } from "node:crypto";
2
- import { isIP } from "node:net";
3
2
  import path from "node:path";
4
- import { adminAuthError, hasAdminToken, InvalidJsonBodyError, isJsonObject, jsonResponse, parseUnknownRequestBody, queryRecord, readInteger, readTrimmedString, RequestBodyTooLargeError, startHttpJsonServer, } from "./http.js";
3
+ import { isHistoricalMatrixUserId, isMatrixEventId, isMatrixRoomId, isMatrixUserId, } from "../matrix-ids.js";
4
+ import { adminAuthError, constantTimeTokenEqual, DEFAULT_MAX_RESPONSE_BODY_BYTES, hasAdminToken, InvalidJsonBodyError, isJsonObject, jsonResponse, parseUnknownRequestBody, queryRecord, readInteger, readTrimmedString, RequestBodyTooLargeError, startHttpJsonServer, } from "./http.js";
5
5
  import { recordCommittedServerEvent, recordServerEvent, } from "./recorder.js";
6
6
  const MAX_MATRIX_FILTER_BYTES = 1024 * 1024;
7
7
  const MAX_MATRIX_FILTERS = 100;
8
- const MAX_MATRIX_IDENTIFIER_BYTES = 255;
9
8
  const MAX_MATRIX_TIMELINE_EVENTS = 1_000;
10
9
  const MAX_MATRIX_TRANSACTION_RESPONSES = 1_000;
11
10
  const MATRIX_TRANSACTION_RETENTION_MS = 10 * 60_000;
12
11
  const MAX_NODE_TIMER_DELAY_MS = 2_147_483_647;
12
+ const DEFAULT_MAX_MATRIX_COMMITTED_ROOMS = 1_000;
13
+ const DEFAULT_MAX_MATRIX_COMMITTED_USERS = 1_000;
14
+ const DEFAULT_MAX_MATRIX_SYNC_RESPONSE_BYTES = 4 * 1024 * 1024;
13
15
  class InvalidMatrixPathEncodingError extends Error {
14
16
  constructor() {
15
17
  super("Matrix request path contains invalid percent encoding.");
16
18
  this.name = "InvalidMatrixPathEncodingError";
17
19
  }
18
20
  }
19
- function matrixId(prefix, value, serverName) {
20
- return `${prefix}${createHash("sha256").update(value).digest("hex").slice(0, 16)}:${serverName}`;
21
- }
22
- function isMatrixIpv4Address(value) {
23
- const octets = value.split(".");
24
- return (octets.length === 4 && octets.every((octet) => /^\d{1,3}$/u.test(octet) && Number(octet) <= 255));
25
- }
26
- function isMatrixServerName(value) {
27
- const ipv6 = /^\[([^\]]+)\](?::(\d{1,5}))?$/u.exec(value);
28
- if (ipv6) {
29
- return isIP(ipv6[1]) === 6;
30
- }
31
- const hostAndPort = /^([^:]+?)(?::(\d{1,5}))?$/u.exec(value);
32
- if (!hostAndPort) {
33
- return false;
34
- }
35
- const hostname = hostAndPort[1];
36
- if (isMatrixIpv4Address(hostname)) {
37
- return true;
38
- }
39
- if (/^\d+\.\d+\.\d+\.\d+$/u.test(hostname)) {
40
- return false;
41
- }
42
- return hostname.length <= 255 && /^[A-Za-z0-9.-]+$/u.test(hostname);
43
- }
44
- function hasLoneSurrogate(value) {
45
- for (let index = 0; index < value.length; index += 1) {
46
- const code = value.charCodeAt(index);
47
- if (code >= 0xd800 && code <= 0xdbff) {
48
- const next = value.charCodeAt(index + 1);
49
- if (next < 0xdc00 || next > 0xdfff) {
50
- return true;
51
- }
52
- index += 1;
53
- }
54
- else if (code >= 0xdc00 && code <= 0xdfff) {
55
- return true;
56
- }
57
- }
58
- return false;
59
- }
60
- function isMatrixScopedIdentifier(value, sigil) {
61
- const separator = value.indexOf(":");
62
- const localpart = value.slice(1, separator);
63
- return (value.startsWith(sigil) &&
64
- Buffer.byteLength(value, "utf8") <= MAX_MATRIX_IDENTIFIER_BYTES &&
65
- separator >= (sigil === "@" ? 1 : 2) &&
66
- !localpart.includes("\0") &&
67
- !hasLoneSurrogate(localpart) &&
68
- isMatrixServerName(value.slice(separator + 1)));
69
- }
70
- function isMatrixHashIdentifier(value, sigil, allowLegacyBase64) {
71
- if (!value.startsWith(sigil) || Buffer.byteLength(value, "utf8") > MAX_MATRIX_IDENTIFIER_BYTES) {
72
- return false;
73
- }
74
- const opaqueId = value.slice(1);
75
- const encoding = allowLegacyBase64 && /^[A-Za-z0-9+/]{43}$/u.test(opaqueId)
76
- ? "base64"
77
- : /^[A-Za-z0-9_-]{43}$/u.test(opaqueId)
78
- ? "base64url"
79
- : undefined;
80
- if (!encoding) {
81
- return false;
82
- }
83
- const decoded = Buffer.from(opaqueId, encoding);
84
- return decoded.length === 32 && decoded.toString(encoding).replace(/=+$/u, "") === opaqueId;
85
- }
86
- function isMatrixRoomId(value) {
87
- return isMatrixScopedIdentifier(value, "!") || isMatrixHashIdentifier(value, "!", false);
88
- }
89
- function isMatrixEventId(value) {
90
- return isMatrixScopedIdentifier(value, "$") || isMatrixHashIdentifier(value, "$", true);
91
- }
92
- function isMatrixUserId(value) {
93
- return isMatrixScopedIdentifier(value, "@");
21
+ function matrixRoomId(value, serverName) {
22
+ return `!${createHash("sha256").update(value).digest("hex").slice(0, 16)}:${serverName}`;
94
23
  }
95
24
  async function appendEvent(state, event, committed = false) {
96
25
  const params = { event, onEvent: state.onEvent, recorderPath: state.recorderPath };
@@ -98,7 +27,8 @@ async function appendEvent(state, event, committed = false) {
98
27
  }
99
28
  function authorized(request, token) {
100
29
  const match = /^Bearer\s+(\S+)$/iu.exec(request.headers.authorization ?? "");
101
- return match?.[1] === token;
30
+ const providedToken = match?.[1];
31
+ return providedToken ? constantTimeTokenEqual(providedToken, token) : false;
102
32
  }
103
33
  function matrixError(errcode, error, status) {
104
34
  return jsonResponse({ errcode, error }, status);
@@ -110,6 +40,15 @@ function matrixResourceLimitError(error) {
110
40
  error,
111
41
  }, 503);
112
42
  }
43
+ function resolvePositiveLimit(value, name, fallback) {
44
+ if (value === undefined) {
45
+ return fallback;
46
+ }
47
+ if (!Number.isSafeInteger(value) || value < 1) {
48
+ throw new Error(`${name} must be a positive safe integer.`);
49
+ }
50
+ return value;
51
+ }
113
52
  function decodeMatrixPathSegment(value) {
114
53
  try {
115
54
  return decodeURIComponent(value);
@@ -119,7 +58,11 @@ function decodeMatrixPathSegment(value) {
119
58
  }
120
59
  }
121
60
  function eventId(state) {
122
- return matrixId("$", `event-${state.nextEvent++}`, state.serverName);
61
+ return `$${createHash("sha256")
62
+ .update(state.serverName)
63
+ .update("\0")
64
+ .update(`event-${state.nextEvent++}`)
65
+ .digest("base64url")}`;
123
66
  }
124
67
  function notifySyncWaiters(state) {
125
68
  for (const resolve of state.syncWaiters) {
@@ -225,6 +168,46 @@ function publishTypingState(state, room) {
225
168
  });
226
169
  notifySyncWaiters(state);
227
170
  }
171
+ function publishDirectRoom(state, userId, roomId) {
172
+ const roomIds = state.directRooms.get(userId) ?? new Set();
173
+ if (roomIds.has(roomId)) {
174
+ return;
175
+ }
176
+ roomIds.add(roomId);
177
+ state.directRooms.set(userId, roomIds);
178
+ state.directRoomsSequence = state.nextSequence++;
179
+ }
180
+ function serializeSyncAccountData(state, since, maxBytes) {
181
+ const sequence = state.directRoomsSequence;
182
+ if (sequence === undefined || (since !== undefined && sequence <= since)) {
183
+ return '{"events":[]}';
184
+ }
185
+ const prefix = '{"events":[{"content":{';
186
+ const suffix = '},"type":"m.direct"}]}';
187
+ const parts = [];
188
+ let bytes = Buffer.byteLength(prefix, "utf8") + Buffer.byteLength(suffix, "utf8");
189
+ for (const [userId, roomIds] of state.directRooms) {
190
+ const roomParts = [];
191
+ let roomBytes = 2;
192
+ for (const roomId of roomIds) {
193
+ const separator = roomParts.length === 0 ? "" : ",";
194
+ const roomPart = `${separator}${JSON.stringify(roomId)}`;
195
+ roomBytes += Buffer.byteLength(roomPart, "utf8");
196
+ if (bytes + roomBytes > maxBytes) {
197
+ return undefined;
198
+ }
199
+ roomParts.push(roomPart);
200
+ }
201
+ const separator = parts.length === 0 ? "" : ",";
202
+ const part = `${separator}${JSON.stringify(userId)}:[${roomParts.join("")}]`;
203
+ bytes += Buffer.byteLength(part, "utf8");
204
+ if (bytes > maxBytes) {
205
+ return undefined;
206
+ }
207
+ parts.push(part);
208
+ }
209
+ return `${prefix}${parts.join("")}${suffix}`;
210
+ }
228
211
  function rememberTransaction(state, key, response) {
229
212
  state.transactions.set(key, response);
230
213
  }
@@ -250,9 +233,26 @@ function transactionResponse(state, key) {
250
233
  }
251
234
  function readTimelineLimit(filter) {
252
235
  const room = filter?.room;
253
- const timeline = isJsonObject(room) ? room.timeline : undefined;
254
- const limit = isJsonObject(timeline) ? readInteger(timeline.limit) : undefined;
255
- return limit === undefined ? undefined : Math.max(0, limit);
236
+ if (room === undefined) {
237
+ return undefined;
238
+ }
239
+ if (!isJsonObject(room)) {
240
+ return matrixError("M_INVALID_PARAM", "Invalid room filter", 400);
241
+ }
242
+ const timeline = room.timeline;
243
+ if (timeline === undefined) {
244
+ return undefined;
245
+ }
246
+ if (!isJsonObject(timeline)) {
247
+ return matrixError("M_INVALID_PARAM", "Invalid timeline filter", 400);
248
+ }
249
+ if (!Object.hasOwn(timeline, "limit")) {
250
+ return undefined;
251
+ }
252
+ const limit = timeline.limit;
253
+ return typeof limit === "number" && Number.isSafeInteger(limit) && limit > 0
254
+ ? limit
255
+ : matrixError("M_INVALID_PARAM", "Invalid timeline limit", 400);
256
256
  }
257
257
  function resolveSyncFilter(url, state) {
258
258
  const value = url.searchParams.get("filter");
@@ -271,11 +271,7 @@ function resolveSyncFilter(url, state) {
271
271
  return matrixError("M_INVALID_PARAM", "Unknown filter", 400);
272
272
  }
273
273
  }
274
- function syncRoom(room, since, timelineLimit) {
275
- const available = room.timeline.filter((entry) => since === undefined || entry.sequence > since);
276
- const timeline = timelineLimit === undefined
277
- ? available
278
- : available.slice(Math.max(0, available.length - timelineLimit));
274
+ function createSyncRoom(room, since, available, timeline) {
279
275
  const firstSequence = timeline[0]?.sequence;
280
276
  const historyWasTrimmed = room.lastDroppedTimelineSequence !== undefined &&
281
277
  (since === undefined || since < room.lastDroppedTimelineSequence);
@@ -310,6 +306,82 @@ function syncRoom(room, since, timelineLimit) {
310
306
  unread_notifications: { highlight_count: 0, notification_count: 0 },
311
307
  };
312
308
  }
309
+ function jsonItemsByteLength(items, maxBytes) {
310
+ let bytes = 0;
311
+ for (const item of items) {
312
+ const serialized = JSON.stringify(item);
313
+ if (serialized === undefined) {
314
+ return undefined;
315
+ }
316
+ bytes += Buffer.byteLength(serialized, "utf8");
317
+ if (bytes > maxBytes) {
318
+ return undefined;
319
+ }
320
+ }
321
+ return bytes;
322
+ }
323
+ function boundedSyncRoom(room, since, timelineLimit, maxBytes, maxTimelineEvents = Number.POSITIVE_INFINITY) {
324
+ const available = room.timeline.filter((entry) => since === undefined || entry.sequence > since);
325
+ const requestedTimeline = timelineLimit === undefined
326
+ ? available
327
+ : available.slice(Math.max(0, available.length - timelineLimit));
328
+ const ephemeral = room.ephemeral
329
+ .filter((entry) => since === undefined || entry.sequence > since)
330
+ .map((entry) => entry.event);
331
+ const ephemeralBytes = jsonItemsByteLength(ephemeral, maxBytes);
332
+ if (ephemeralBytes === undefined) {
333
+ return undefined;
334
+ }
335
+ const timeline = [];
336
+ let timelineBytes = 0;
337
+ for (let index = requestedTimeline.length - 1; index >= 0; index -= 1) {
338
+ if (timeline.length >= maxTimelineEvents) {
339
+ break;
340
+ }
341
+ const entry = requestedTimeline[index];
342
+ const serializedEvent = JSON.stringify(entry.event);
343
+ if (serializedEvent === undefined) {
344
+ return undefined;
345
+ }
346
+ const eventBytes = Buffer.byteLength(serializedEvent, "utf8");
347
+ if (timelineBytes + ephemeralBytes + eventBytes > maxBytes) {
348
+ break;
349
+ }
350
+ timeline.unshift(entry);
351
+ timelineBytes += eventBytes;
352
+ }
353
+ if (requestedTimeline.length > 0 && timelineLimit !== 0 && timeline.length === 0) {
354
+ return undefined;
355
+ }
356
+ while (true) {
357
+ const body = createSyncRoom(room, since, available, timeline);
358
+ const stateBytes = jsonItemsByteLength(body.state.events, maxBytes);
359
+ if (stateBytes !== undefined && stateBytes + ephemeralBytes + timelineBytes <= maxBytes) {
360
+ const serialized = JSON.stringify(body);
361
+ if (Buffer.byteLength(serialized, "utf8") <= maxBytes) {
362
+ return serialized;
363
+ }
364
+ }
365
+ if (timeline.length === 1 && requestedTimeline.length > 0 && timelineLimit !== 0) {
366
+ return undefined;
367
+ }
368
+ const dropped = timeline.shift();
369
+ if (!dropped) {
370
+ return undefined;
371
+ }
372
+ const serializedDropped = JSON.stringify(dropped.event);
373
+ if (serializedDropped === undefined) {
374
+ return undefined;
375
+ }
376
+ timelineBytes -= Buffer.byteLength(serializedDropped, "utf8");
377
+ }
378
+ }
379
+ function roomHasSyncUpdates(room, since) {
380
+ return (since === undefined ||
381
+ room.createdSequence > since ||
382
+ room.timeline.some((entry) => entry.sequence > since) ||
383
+ room.ephemeral.some((entry) => entry.sequence > since));
384
+ }
313
385
  function parseSyncToken(value) {
314
386
  if (value === null) {
315
387
  return undefined;
@@ -331,20 +403,62 @@ async function handleSync(url, state) {
331
403
  return filter;
332
404
  }
333
405
  const timelineLimit = readTimelineLimit(filter);
406
+ if (timelineLimit instanceof Response) {
407
+ return timelineLimit;
408
+ }
334
409
  const timeout = Math.min(readInteger(url.searchParams.get("timeout")) ?? 0, 1_000);
335
- const hasNewEvents = [...state.rooms.values()].some((room) => [...room.timeline, ...room.ephemeral].some((entry) => since === undefined || entry.sequence > since));
410
+ const hasNewEvents = (state.directRoomsSequence !== undefined &&
411
+ (since === undefined || state.directRoomsSequence > since)) ||
412
+ [...state.rooms.values()].some((room) => roomHasSyncUpdates(room, since));
336
413
  if (since !== undefined && !hasNewEvents && timeout > 0) {
337
414
  await waitForSyncEvent(state, timeout);
338
415
  }
339
- const join = Object.fromEntries([...state.rooms.values()].map((room) => [room.id, syncRoom(room, since, timelineLimit)]));
340
- return jsonResponse({
341
- account_data: { events: [] },
342
- device_lists: { changed: [], left: [] },
343
- device_one_time_keys_count: {},
344
- next_batch: `s${state.nextSequence - 1}`,
345
- presence: { events: [] },
346
- rooms: { invite: {}, join, knock: {}, leave: {} },
347
- to_device: { events: [] },
416
+ const accountData = serializeSyncAccountData(state, since, state.maxSyncResponseBytes);
417
+ if (!accountData) {
418
+ return matrixResourceLimitError("Sync response exceeds the configured byte limit");
419
+ }
420
+ const prefix = `{"account_data":${accountData},"device_lists":{"changed":[],"left":[]},"device_one_time_keys_count":{},"next_batch":${JSON.stringify(`s${state.nextSequence - 1}`)},"presence":{"events":[]},"rooms":{"invite":{},"join":{`;
421
+ const suffix = '},"knock":{},"leave":{}},"to_device":{"events":[]}}';
422
+ let responseBytes = Buffer.byteLength(prefix, "utf8") + Buffer.byteLength(suffix, "utf8");
423
+ const rooms = [...state.rooms.values()].filter((room) => roomHasSyncUpdates(room, since));
424
+ const minimumEntries = [];
425
+ for (const [index, room] of rooms.entries()) {
426
+ const separator = index === 0 ? "" : ",";
427
+ const roomKey = JSON.stringify(room.id);
428
+ const framingBytes = Buffer.byteLength(`${separator}${roomKey}:`, "utf8");
429
+ const roomBody = boundedSyncRoom(room, since, timelineLimit, state.maxSyncResponseBytes, 1);
430
+ if (!roomBody) {
431
+ return matrixResourceLimitError("Sync response exceeds the configured byte limit");
432
+ }
433
+ minimumEntries.push({
434
+ framingBytes,
435
+ minimumBytes: framingBytes + Buffer.byteLength(roomBody, "utf8"),
436
+ room,
437
+ roomKey,
438
+ separator,
439
+ });
440
+ }
441
+ let reservedBytes = minimumEntries.reduce((total, entry) => total + entry.minimumBytes, 0);
442
+ if (responseBytes + reservedBytes > state.maxSyncResponseBytes) {
443
+ return matrixResourceLimitError("Sync response exceeds the configured byte limit");
444
+ }
445
+ const parts = [];
446
+ for (const entry of minimumEntries) {
447
+ reservedBytes -= entry.minimumBytes;
448
+ const roomBody = boundedSyncRoom(entry.room, since, timelineLimit, state.maxSyncResponseBytes - responseBytes - entry.framingBytes - reservedBytes);
449
+ if (!roomBody) {
450
+ return matrixResourceLimitError("Sync response exceeds the configured byte limit");
451
+ }
452
+ parts.push(`${entry.separator}${entry.roomKey}:${roomBody}`);
453
+ responseBytes += entry.framingBytes + Buffer.byteLength(roomBody, "utf8");
454
+ }
455
+ const body = `${prefix}${parts.join("")}${suffix}`;
456
+ if (Buffer.byteLength(body, "utf8") > state.maxSyncResponseBytes) {
457
+ return matrixResourceLimitError("Sync response exceeds the configured byte limit");
458
+ }
459
+ return new Response(body, {
460
+ headers: { "content-type": "application/json" },
461
+ status: 200,
348
462
  });
349
463
  }
350
464
  function findRoom(state, encodedRoomId) {
@@ -353,18 +467,33 @@ function findRoom(state, encodedRoomId) {
353
467
  async function handleAdminInbound(params) {
354
468
  const roomId = readTrimmedString(params.body.roomId);
355
469
  const sender = readTrimmedString(params.body.sender ?? params.body.senderId);
356
- const text = readTrimmedString(params.body.text);
357
- if (!roomId || !sender || !text) {
470
+ const text = typeof params.body.text === "string" ? params.body.text : undefined;
471
+ if (!roomId || !sender || text === undefined || text.length === 0) {
358
472
  return jsonResponse({ error: "roomId, senderId, and text are required", ok: false }, 400);
359
473
  }
360
474
  const threadId = readTrimmedString(params.body.threadId);
361
475
  if (!isMatrixRoomId(roomId) ||
362
- !isMatrixUserId(sender) ||
476
+ !isHistoricalMatrixUserId(sender) ||
363
477
  (threadId !== undefined && !isMatrixEventId(threadId))) {
364
478
  return jsonResponse({ error: "Invalid Matrix identifier", ok: false }, 400);
365
479
  }
366
480
  const direct = params.body.direct === true;
367
- const room = params.state.rooms.get(roomId) ??
481
+ const existingRoom = params.state.rooms.get(roomId);
482
+ if (!existingRoom && params.state.rooms.size >= params.state.maxCommittedRooms) {
483
+ return jsonResponse({ error: "Committed Matrix rooms limit reached", ok: false }, 503);
484
+ }
485
+ if (!params.state.profiles.has(sender) &&
486
+ params.state.profiles.size >= params.state.maxCommittedUsers) {
487
+ return jsonResponse({ error: "Committed Matrix users limit reached", ok: false }, 503);
488
+ }
489
+ const roomHasSender = existingRoom
490
+ ? existingRoom.users.has(sender)
491
+ : sender === params.state.botUserId;
492
+ const roomUserCount = existingRoom?.users.size ?? 1;
493
+ if (!roomHasSender && roomUserCount >= params.state.maxCommittedUsers) {
494
+ return jsonResponse({ error: "Committed Matrix users limit reached", ok: false }, 503);
495
+ }
496
+ const room = existingRoom ??
368
497
  createRoom({
369
498
  botUserId: params.state.botUserId,
370
499
  createdSequence: params.state.nextSequence++,
@@ -375,7 +504,19 @@ async function handleAdminInbound(params) {
375
504
  state: params.state,
376
505
  });
377
506
  params.state.rooms.set(roomId, room);
507
+ if (direct) {
508
+ publishDirectRoom(params.state, sender, roomId);
509
+ }
378
510
  const senderName = readTrimmedString(params.body.senderName);
511
+ if (senderName) {
512
+ params.state.profiles.set(sender, {
513
+ ...params.state.profiles.get(sender),
514
+ display_name: senderName,
515
+ });
516
+ }
517
+ else if (!params.state.profiles.has(sender)) {
518
+ params.state.profiles.set(sender, {});
519
+ }
379
520
  const existingProfile = room.users.get(sender);
380
521
  if (existingProfile === undefined) {
381
522
  room.users.set(sender, senderName ? { display_name: senderName } : {});
@@ -415,6 +556,13 @@ async function handleAdminInbound(params) {
415
556
  sequence: params.state.nextSequence++,
416
557
  });
417
558
  }
559
+ const roomAvatarUrl = room.users.get(sender)?.avatar_url;
560
+ if (roomAvatarUrl) {
561
+ params.state.profiles.set(sender, {
562
+ ...params.state.profiles.get(sender),
563
+ avatar_url: roomAvatarUrl,
564
+ });
565
+ }
418
566
  const content = { body: text, msgtype: "m.text" };
419
567
  if (threadId) {
420
568
  content["m.relates_to"] = {
@@ -453,13 +601,11 @@ async function handleMatrixApi(params) {
453
601
  let match = /^\/profile\/([^/]+)$/u.exec(relativePath);
454
602
  if (params.method === "GET" && match) {
455
603
  const userId = decodeMatrixPathSegment(match[1]);
456
- const member = [...params.state.rooms.values()]
457
- .map((room) => room.users.get(userId))
458
- .find((profile) => profile !== undefined);
459
- return member
604
+ const profile = params.state.profiles.get(userId);
605
+ return profile
460
606
  ? jsonResponse({
461
- ...(member.avatar_url ? { avatar_url: member.avatar_url } : {}),
462
- ...(member.display_name ? { displayname: member.display_name } : {}),
607
+ ...(profile.avatar_url ? { avatar_url: profile.avatar_url } : {}),
608
+ ...(profile.display_name ? { displayname: profile.display_name } : {}),
463
609
  })
464
610
  : matrixError("M_NOT_FOUND", "Unknown user", 404);
465
611
  }
@@ -654,24 +800,34 @@ async function handleMatrixApi(params) {
654
800
  export async function startMatrixServer(params = {}) {
655
801
  const host = params.host ?? "127.0.0.1";
656
802
  const serverName = params.serverName ?? "matrix.test";
803
+ const botUserId = params.botUserId ?? `@openclaw:${serverName}`;
804
+ if (!isMatrixUserId(botUserId)) {
805
+ throw new Error("botUserId must be a canonical Matrix user ID.");
806
+ }
657
807
  const state = {
658
808
  accessToken: params.accessToken ?? `syt_crabline_${randomBytes(12).toString("hex")}`,
659
809
  adminToken: params.adminToken ?? randomBytes(24).toString("hex"),
660
- botUserId: params.botUserId ?? `@openclaw:${serverName}`,
810
+ botUserId,
661
811
  deviceId: params.deviceId ?? "CRABLINE",
812
+ directRooms: new Map(),
813
+ directRoomsSequence: undefined,
662
814
  filterBytes: 0,
663
815
  filters: new Map(),
816
+ maxCommittedRooms: resolvePositiveLimit(params.maxCommittedRooms, "maxCommittedRooms", DEFAULT_MAX_MATRIX_COMMITTED_ROOMS),
817
+ maxCommittedUsers: resolvePositiveLimit(params.maxCommittedUsers, "maxCommittedUsers", DEFAULT_MAX_MATRIX_COMMITTED_USERS),
818
+ maxSyncResponseBytes: resolvePositiveLimit(params.maxSyncResponseBytes, "maxSyncResponseBytes", DEFAULT_MAX_MATRIX_SYNC_RESPONSE_BYTES),
664
819
  nextEvent: 1,
665
820
  nextFilter: 1,
666
821
  nextSequence: 1,
667
822
  onEvent: params.onEvent,
823
+ profiles: new Map([[botUserId, { display_name: "OpenClaw QA" }]]),
668
824
  recorderPath: params.recorderPath ?? path.resolve("artifacts/crabline/matrix.jsonl"),
669
825
  rooms: new Map(),
670
826
  serverName,
671
827
  syncWaiters: new Set(),
672
828
  transactions: new Map(),
673
829
  };
674
- const roomId = params.roomId ?? matrixId("!", "default-room", serverName);
830
+ const roomId = params.roomId ?? matrixRoomId("default-room", serverName);
675
831
  state.rooms.set(roomId, createRoom({
676
832
  botUserId: state.botUserId,
677
833
  createdSequence: 0,
@@ -694,6 +850,7 @@ export async function startMatrixServer(params = {}) {
694
850
  return matrixError("M_UNKNOWN", "Internal server error", 500);
695
851
  },
696
852
  host,
853
+ maxResponseBodyBytes: Math.max(DEFAULT_MAX_RESPONSE_BODY_BYTES, state.maxSyncResponseBytes),
697
854
  port: params.port ?? 0,
698
855
  serverName: "Matrix",
699
856
  async handle(request) {
@@ -761,6 +918,7 @@ export async function startMatrixServer(params = {}) {
761
918
  state,
762
919
  url,
763
920
  });
921
+ event.accepted ??= response.ok;
764
922
  await appendEvent(state, event, response.ok && ["DELETE", "POST", "PUT"].includes(method));
765
923
  return response;
766
924
  },