@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.
- package/README.md +137 -23
- package/dist/src/bin/crabline.js +3 -1
- package/dist/src/bin/crabline.js.map +1 -1
- package/dist/src/cli/program.d.ts +5 -1
- package/dist/src/cli/program.js +281 -52
- package/dist/src/cli/program.js.map +1 -1
- package/dist/src/config/load.js +39 -3
- package/dist/src/config/load.js.map +1 -1
- package/dist/src/config/schema.d.ts +20 -0
- package/dist/src/config/schema.js +163 -35
- package/dist/src/config/schema.js.map +1 -1
- package/dist/src/core/errors.d.ts +1 -0
- package/dist/src/core/errors.js +6 -3
- package/dist/src/core/errors.js.map +1 -1
- package/dist/src/core/http-path.d.ts +1 -0
- package/dist/src/core/http-path.js +14 -0
- package/dist/src/core/http-path.js.map +1 -0
- package/dist/src/core/matcher.d.ts +3 -1
- package/dist/src/core/matcher.js +58 -2
- package/dist/src/core/matcher.js.map +1 -1
- package/dist/src/core/nonces.d.ts +2 -0
- package/dist/src/core/nonces.js +11 -1
- package/dist/src/core/nonces.js.map +1 -1
- package/dist/src/core/reporters.d.ts +5 -0
- package/dist/src/core/reporters.js +26 -2
- package/dist/src/core/reporters.js.map +1 -1
- package/dist/src/core/run.d.ts +3 -1
- package/dist/src/core/run.js +178 -66
- package/dist/src/core/run.js.map +1 -1
- package/dist/src/index.d.ts +12 -10
- package/dist/src/index.js +6 -5
- package/dist/src/index.js.map +1 -1
- package/dist/src/matrix-ids.d.ts +5 -0
- package/dist/src/matrix-ids.js +83 -0
- package/dist/src/matrix-ids.js.map +1 -0
- package/dist/src/openclaw/artifact-generation.d.ts +7 -6
- package/dist/src/openclaw/artifact-generation.js +385 -41
- package/dist/src/openclaw/artifact-generation.js.map +1 -1
- package/dist/src/openclaw/bridges/discord.d.ts +1 -0
- package/dist/src/openclaw/bridges/discord.js +149 -0
- package/dist/src/openclaw/bridges/discord.js.map +1 -0
- package/dist/src/openclaw/bridges/matrix.js +22 -5
- package/dist/src/openclaw/bridges/matrix.js.map +1 -1
- package/dist/src/openclaw/bridges/mattermost.js +14 -9
- package/dist/src/openclaw/bridges/mattermost.js.map +1 -1
- package/dist/src/openclaw/bridges/probe-response.d.ts +1 -0
- package/dist/src/openclaw/bridges/probe-response.js +5 -0
- package/dist/src/openclaw/bridges/probe-response.js.map +1 -0
- package/dist/src/openclaw/bridges/signal.js +16 -7
- package/dist/src/openclaw/bridges/signal.js.map +1 -1
- package/dist/src/openclaw/bridges/slack.js +117 -24
- package/dist/src/openclaw/bridges/slack.js.map +1 -1
- package/dist/src/openclaw/bridges/telegram.js +46 -20
- package/dist/src/openclaw/bridges/telegram.js.map +1 -1
- package/dist/src/openclaw/bridges/whatsapp.js +33 -17
- package/dist/src/openclaw/bridges/whatsapp.js.map +1 -1
- package/dist/src/openclaw/bridges/zalo.js +9 -4
- package/dist/src/openclaw/bridges/zalo.js.map +1 -1
- package/dist/src/openclaw/outbound-contract.js +3 -0
- package/dist/src/openclaw/outbound-contract.js.map +1 -1
- package/dist/src/openclaw/private-file.d.ts +38 -3
- package/dist/src/openclaw/private-file.js +2008 -87
- package/dist/src/openclaw/private-file.js.map +1 -1
- package/dist/src/openclaw/{smoke-lock.d.ts → provider-readiness-lock.d.ts} +14 -5
- package/dist/src/openclaw/provider-readiness-lock.js +1320 -0
- package/dist/src/openclaw/provider-readiness-lock.js.map +1 -0
- package/dist/src/openclaw/shared.d.ts +10 -17
- package/dist/src/openclaw/shared.js +53 -19
- package/dist/src/openclaw/shared.js.map +1 -1
- package/dist/src/openclaw.d.ts +3 -5
- package/dist/src/openclaw.js +245 -60
- package/dist/src/openclaw.js.map +1 -1
- package/dist/src/platform/process-owned-lock.d.ts +11 -0
- package/dist/src/platform/process-owned-lock.js +1698 -0
- package/dist/src/platform/process-owned-lock.js.map +1 -0
- package/dist/src/platform/recorder-directory.d.ts +1 -0
- package/dist/src/platform/recorder-directory.js +14 -0
- package/dist/src/platform/recorder-directory.js.map +1 -0
- package/dist/src/platform/windows-acl.d.ts +18 -0
- package/dist/src/platform/windows-acl.js +1367 -0
- package/dist/src/platform/windows-acl.js.map +1 -0
- package/dist/src/platform/windows-lock-root.d.ts +13 -0
- package/dist/src/platform/windows-lock-root.js +98 -0
- package/dist/src/platform/windows-lock-root.js.map +1 -0
- package/dist/src/providers/builtin/discord.d.ts +4 -0
- package/dist/src/providers/builtin/discord.js +52 -9
- package/dist/src/providers/builtin/discord.js.map +1 -1
- package/dist/src/providers/builtin/external-webhook-auth.d.ts +1 -0
- package/dist/src/providers/builtin/external-webhook-auth.js +38 -1
- package/dist/src/providers/builtin/external-webhook-auth.js.map +1 -1
- package/dist/src/providers/builtin/feishu.d.ts +1 -0
- package/dist/src/providers/builtin/feishu.js +96 -26
- package/dist/src/providers/builtin/feishu.js.map +1 -1
- package/dist/src/providers/builtin/googlechat.d.ts +1 -0
- package/dist/src/providers/builtin/googlechat.js +95 -26
- package/dist/src/providers/builtin/googlechat.js.map +1 -1
- package/dist/src/providers/builtin/imessage.js +60 -17
- package/dist/src/providers/builtin/imessage.js.map +1 -1
- package/dist/src/providers/builtin/loopback.js +88 -35
- package/dist/src/providers/builtin/loopback.js.map +1 -1
- package/dist/src/providers/builtin/matrix.d.ts +2 -2
- package/dist/src/providers/builtin/matrix.js +78 -21
- package/dist/src/providers/builtin/matrix.js.map +1 -1
- package/dist/src/providers/builtin/mattermost.d.ts +11 -7
- package/dist/src/providers/builtin/mattermost.js +138 -24
- package/dist/src/providers/builtin/mattermost.js.map +1 -1
- package/dist/src/providers/builtin/msteams.d.ts +1 -0
- package/dist/src/providers/builtin/msteams.js +118 -38
- package/dist/src/providers/builtin/msteams.js.map +1 -1
- package/dist/src/providers/builtin/native-local-mock.js +4 -4
- package/dist/src/providers/builtin/native-local-mock.js.map +1 -1
- package/dist/src/providers/builtin/script.js +1057 -146
- package/dist/src/providers/builtin/script.js.map +1 -1
- package/dist/src/providers/builtin/slack.d.ts +8 -1
- package/dist/src/providers/builtin/slack.js +327 -24
- package/dist/src/providers/builtin/slack.js.map +1 -1
- package/dist/src/providers/builtin/telegram.d.ts +1 -1
- package/dist/src/providers/builtin/telegram.js +55 -14
- package/dist/src/providers/builtin/telegram.js.map +1 -1
- package/dist/src/providers/builtin/whatsapp.d.ts +4 -3
- package/dist/src/providers/builtin/whatsapp.js +157 -29
- package/dist/src/providers/builtin/whatsapp.js.map +1 -1
- package/dist/src/providers/builtin/zalo.d.ts +5 -2
- package/dist/src/providers/builtin/zalo.js +31 -7
- package/dist/src/providers/builtin/zalo.js.map +1 -1
- package/dist/src/providers/catalog.d.ts +1 -1
- package/dist/src/providers/catalog.js +1 -1
- package/dist/src/providers/catalog.js.map +1 -1
- package/dist/src/providers/local-mock.d.ts +4 -0
- package/dist/src/providers/local-mock.js +162 -35
- package/dist/src/providers/local-mock.js.map +1 -1
- package/dist/src/providers/native-ids.d.ts +2 -0
- package/dist/src/providers/native-ids.js +3 -0
- package/dist/src/providers/native-ids.js.map +1 -1
- package/dist/src/providers/recorder.d.ts +14 -3
- package/dist/src/providers/recorder.js +521 -41
- package/dist/src/providers/recorder.js.map +1 -1
- package/dist/src/providers/registry.d.ts +1 -0
- package/dist/src/providers/registry.js +65 -20
- package/dist/src/providers/registry.js.map +1 -1
- package/dist/src/providers/signed-jwt.d.ts +6 -1
- package/dist/src/providers/signed-jwt.js +241 -50
- package/dist/src/providers/signed-jwt.js.map +1 -1
- package/dist/src/providers/slack-ids.d.ts +1 -0
- package/dist/src/providers/slack-ids.js +5 -0
- package/dist/src/providers/slack-ids.js.map +1 -1
- package/dist/src/providers/target-normalizers.d.ts +9 -1
- package/dist/src/providers/target-normalizers.js +177 -58
- package/dist/src/providers/target-normalizers.js.map +1 -1
- package/dist/src/providers/webhook-server.d.ts +3 -0
- package/dist/src/providers/webhook-server.js +268 -47
- package/dist/src/providers/webhook-server.js.map +1 -1
- package/dist/src/servers/discord.d.ts +40 -0
- package/dist/src/servers/discord.js +842 -0
- package/dist/src/servers/discord.js.map +1 -0
- package/dist/src/servers/http.d.ts +14 -2
- package/dist/src/servers/http.js +229 -58
- package/dist/src/servers/http.js.map +1 -1
- package/dist/src/servers/index.d.ts +10 -4
- package/dist/src/servers/index.js +4 -0
- package/dist/src/servers/index.js.map +1 -1
- package/dist/src/servers/matrix.d.ts +3 -0
- package/dist/src/servers/matrix.js +268 -110
- package/dist/src/servers/matrix.js.map +1 -1
- package/dist/src/servers/mattermost.d.ts +5 -0
- package/dist/src/servers/mattermost.js +389 -91
- package/dist/src/servers/mattermost.js.map +1 -1
- package/dist/src/servers/recorder.d.ts +11 -0
- package/dist/src/servers/recorder.js +1072 -69
- package/dist/src/servers/recorder.js.map +1 -1
- package/dist/src/servers/signal.d.ts +1 -0
- package/dist/src/servers/signal.js +279 -44
- package/dist/src/servers/signal.js.map +1 -1
- package/dist/src/servers/slack.d.ts +15 -0
- package/dist/src/servers/slack.js +270 -54
- package/dist/src/servers/slack.js.map +1 -1
- package/dist/src/servers/telegram-identity.d.ts +6 -0
- package/dist/src/servers/telegram-identity.js +27 -0
- package/dist/src/servers/telegram-identity.js.map +1 -0
- package/dist/src/servers/telegram.d.ts +17 -6
- package/dist/src/servers/telegram.js +1218 -107
- package/dist/src/servers/telegram.js.map +1 -1
- package/dist/src/servers/webhook-target.d.ts +12 -4
- package/dist/src/servers/webhook-target.js +226 -28
- package/dist/src/servers/webhook-target.js.map +1 -1
- package/dist/src/servers/whatsapp-baileys-websocket.d.ts +60 -1
- package/dist/src/servers/whatsapp-baileys-websocket.js +782 -112
- package/dist/src/servers/whatsapp-baileys-websocket.js.map +1 -1
- package/dist/src/servers/whatsapp-jid.d.ts +1 -0
- package/dist/src/servers/whatsapp-jid.js +9 -5
- package/dist/src/servers/whatsapp-jid.js.map +1 -1
- package/dist/src/servers/whatsapp-wire/binary-node.d.ts +1 -0
- package/dist/src/servers/whatsapp-wire/binary-node.js +37 -13
- package/dist/src/servers/whatsapp-wire/binary-node.js.map +1 -1
- package/dist/src/servers/whatsapp-wire/crypto.d.ts +3 -0
- package/dist/src/servers/whatsapp-wire/crypto.js +23 -8
- package/dist/src/servers/whatsapp-wire/crypto.js.map +1 -1
- package/dist/src/servers/whatsapp-wire/handshake.d.ts +9 -3
- package/dist/src/servers/whatsapp-wire/handshake.js +173 -12
- package/dist/src/servers/whatsapp-wire/handshake.js.map +1 -1
- package/dist/src/servers/whatsapp-wire/signal.js +3 -3
- package/dist/src/servers/whatsapp-wire/signal.js.map +1 -1
- package/dist/src/servers/whatsapp.d.ts +7 -1
- package/dist/src/servers/whatsapp.js +149 -25
- package/dist/src/servers/whatsapp.js.map +1 -1
- package/dist/src/servers/zalo.d.ts +1 -0
- package/dist/src/servers/zalo.js +193 -82
- package/dist/src/servers/zalo.js.map +1 -1
- package/docs/channel-setup.md +251 -64
- package/fixtures/examples/crabline.example.yaml +3 -2
- package/package.json +18 -10
- package/dist/src/openclaw/smoke-lock.js +0 -1004
- package/dist/src/openclaw/smoke-lock.js.map +0 -1
- 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 {
|
|
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
|
|
20
|
-
return
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
|
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 =
|
|
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
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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 =
|
|
357
|
-
if (!roomId || !sender ||
|
|
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
|
-
!
|
|
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
|
|
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
|
|
457
|
-
|
|
458
|
-
.find((profile) => profile !== undefined);
|
|
459
|
-
return member
|
|
604
|
+
const profile = params.state.profiles.get(userId);
|
|
605
|
+
return profile
|
|
460
606
|
? jsonResponse({
|
|
461
|
-
...(
|
|
462
|
-
...(
|
|
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
|
|
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 ??
|
|
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
|
},
|