codeharbor 0.1.14 → 0.1.16
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/.env.example +8 -0
- package/README.md +20 -2
- package/dist/cli.js +339 -26
- package/package.json +1 -1
package/.env.example
CHANGED
|
@@ -38,6 +38,7 @@ MATRIX_TYPING_TIMEOUT_MS=10000
|
|
|
38
38
|
SESSION_ACTIVE_WINDOW_MINUTES=20
|
|
39
39
|
|
|
40
40
|
# Group trigger defaults.
|
|
41
|
+
GROUP_DIRECT_MODE_ENABLED=false
|
|
41
42
|
GROUP_TRIGGER_ALLOW_MENTION=true
|
|
42
43
|
GROUP_TRIGGER_ALLOW_REPLY=true
|
|
43
44
|
GROUP_TRIGGER_ALLOW_ACTIVE_WINDOW=true
|
|
@@ -74,6 +75,13 @@ ADMIN_PORT=8787
|
|
|
74
75
|
# Strongly recommended for any non-localhost access.
|
|
75
76
|
# Required when exposing admin via reverse proxy/tunnel/public domain.
|
|
76
77
|
ADMIN_TOKEN=
|
|
78
|
+
# Optional multi-token RBAC (JSON array).
|
|
79
|
+
# Each item: {"token":"...","role":"admin|viewer","actor":"ops-name"}
|
|
80
|
+
# Example:
|
|
81
|
+
# ADMIN_TOKENS_JSON=[{"token":"admin-secret","role":"admin","actor":"ops-admin"},{"token":"viewer-secret","role":"viewer","actor":"ops-audit"}]
|
|
82
|
+
# Rotate helper:
|
|
83
|
+
# ./scripts/rotate-admin-token.sh --target rbac --role admin --actor ops-admin
|
|
84
|
+
ADMIN_TOKENS_JSON=
|
|
77
85
|
# Optional IP allowlist (comma-separated, for example: 127.0.0.1,192.168.1.10).
|
|
78
86
|
ADMIN_IP_ALLOWLIST=
|
|
79
87
|
# Optional browser origin allowlist for CORS (comma-separated).
|
package/README.md
CHANGED
|
@@ -311,7 +311,7 @@ Optional overrides:
|
|
|
311
311
|
codeharbor admin serve --host 127.0.0.1 --port 8787
|
|
312
312
|
```
|
|
313
313
|
|
|
314
|
-
If you bind Admin to a non-loopback host and `ADMIN_TOKEN`
|
|
314
|
+
If you bind Admin to a non-loopback host and both `ADMIN_TOKEN` and `ADMIN_TOKENS_JSON` are empty, startup is rejected by default.
|
|
315
315
|
Explicit bypass exists but is not recommended:
|
|
316
316
|
|
|
317
317
|
```bash
|
|
@@ -327,6 +327,7 @@ Open these UI routes in browser:
|
|
|
327
327
|
|
|
328
328
|
Main endpoints:
|
|
329
329
|
|
|
330
|
+
- `GET /api/admin/auth/status`
|
|
330
331
|
- `GET /api/admin/config/global`
|
|
331
332
|
- `PUT /api/admin/config/global`
|
|
332
333
|
- `GET /api/admin/config/rooms`
|
|
@@ -336,7 +337,7 @@ Main endpoints:
|
|
|
336
337
|
- `GET /api/admin/health`
|
|
337
338
|
- `GET /api/admin/audit?limit=50`
|
|
338
339
|
|
|
339
|
-
When `ADMIN_TOKEN` is set, requests must include:
|
|
340
|
+
When `ADMIN_TOKEN` or `ADMIN_TOKENS_JSON` is set, requests must include:
|
|
340
341
|
|
|
341
342
|
```http
|
|
342
343
|
Authorization: Bearer <ADMIN_TOKEN>
|
|
@@ -345,9 +346,24 @@ Authorization: Bearer <ADMIN_TOKEN>
|
|
|
345
346
|
Access control options:
|
|
346
347
|
|
|
347
348
|
- `ADMIN_TOKEN`: require bearer token for `/api/admin/*`
|
|
349
|
+
- `ADMIN_TOKENS_JSON`: optional multi-token RBAC list (supports `admin` and `viewer` roles)
|
|
348
350
|
- `ADMIN_IP_ALLOWLIST`: optional comma-separated client IP whitelist (for example `127.0.0.1,192.168.1.10`)
|
|
349
351
|
- `ADMIN_ALLOWED_ORIGINS`: optional CORS origin allowlist for browser-based cross-origin admin access
|
|
350
352
|
|
|
353
|
+
RBAC behavior:
|
|
354
|
+
|
|
355
|
+
- `viewer` tokens can call read endpoints (`GET /api/admin/*`)
|
|
356
|
+
- `admin` tokens can call read + write endpoints (`PUT/POST/DELETE /api/admin/*`)
|
|
357
|
+
- for `ADMIN_TOKENS_JSON`, audit actor is derived from token identity (`actor` field), not `x-admin-actor`
|
|
358
|
+
- Admin UI shows current permission status (role/source) after saving auth
|
|
359
|
+
|
|
360
|
+
Rotate tokens quickly (repository script):
|
|
361
|
+
|
|
362
|
+
```bash
|
|
363
|
+
./scripts/rotate-admin-token.sh --target rbac --role admin --actor ops-admin
|
|
364
|
+
./scripts/rotate-admin-token.sh --target rbac --role viewer --actor ops-audit
|
|
365
|
+
```
|
|
366
|
+
|
|
351
367
|
Note: `PUT /api/admin/config/global` writes to `.env` and marks changes as restart-required.
|
|
352
368
|
|
|
353
369
|
### Admin UI Quick Walkthrough
|
|
@@ -386,12 +402,14 @@ If any check fails, it prints actionable fix commands (for example `codeharbor i
|
|
|
386
402
|
- Direct Message (DM)
|
|
387
403
|
- all text messages are processed by default (no prefix required)
|
|
388
404
|
- Group Room
|
|
405
|
+
- when `GROUP_DIRECT_MODE_ENABLED=true`, all non-empty messages are processed directly (no prefix/mention/reply required)
|
|
389
406
|
- processed when **any allowed trigger** matches:
|
|
390
407
|
- message mentions bot user id
|
|
391
408
|
- message replies to a bot message
|
|
392
409
|
- sender has an active conversation window
|
|
393
410
|
- optional explicit prefix match (`MATRIX_COMMAND_PREFIX`)
|
|
394
411
|
- Trigger Policy
|
|
412
|
+
- `GROUP_DIRECT_MODE_ENABLED` controls whether groups bypass trigger matching entirely
|
|
395
413
|
- global defaults via `GROUP_TRIGGER_ALLOW_*`
|
|
396
414
|
- per-room overrides via `ROOM_TRIGGER_POLICY_JSON`
|
|
397
415
|
- Active Conversation Window
|
package/dist/cli.js
CHANGED
|
@@ -646,6 +646,7 @@ var AdminServer = class {
|
|
|
646
646
|
host;
|
|
647
647
|
port;
|
|
648
648
|
adminToken;
|
|
649
|
+
adminTokens;
|
|
649
650
|
adminIpAllowlist;
|
|
650
651
|
adminAllowedOrigins;
|
|
651
652
|
cwd;
|
|
@@ -662,6 +663,7 @@ var AdminServer = class {
|
|
|
662
663
|
this.host = options.host;
|
|
663
664
|
this.port = options.port;
|
|
664
665
|
this.adminToken = options.adminToken;
|
|
666
|
+
this.adminTokens = buildAdminTokenMap(options.adminTokens ?? []);
|
|
665
667
|
this.adminIpAllowlist = normalizeAllowlist(options.adminIpAllowlist ?? []);
|
|
666
668
|
this.adminAllowedOrigins = normalizeOriginAllowlist(options.adminAllowedOrigins ?? []);
|
|
667
669
|
this.cwd = options.cwd ?? process.cwd();
|
|
@@ -753,10 +755,32 @@ var AdminServer = class {
|
|
|
753
755
|
this.sendHtml(res, renderAdminConsoleHtml());
|
|
754
756
|
return;
|
|
755
757
|
}
|
|
756
|
-
|
|
758
|
+
const requiredRole = requiredAdminRoleForRequest(req.method, url.pathname);
|
|
759
|
+
const authIdentity = requiredRole ? this.resolveAdminIdentity(req) : null;
|
|
760
|
+
if (requiredRole && !authIdentity) {
|
|
757
761
|
this.sendJson(res, 401, {
|
|
758
762
|
ok: false,
|
|
759
|
-
error: "Unauthorized. Provide Authorization: Bearer <ADMIN_TOKEN
|
|
763
|
+
error: "Unauthorized. Provide Authorization: Bearer <ADMIN_TOKEN> (or token from ADMIN_TOKENS_JSON)."
|
|
764
|
+
});
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
if (requiredRole && authIdentity && !hasRequiredAdminRole(authIdentity.role, requiredRole)) {
|
|
768
|
+
this.sendJson(res, 403, {
|
|
769
|
+
ok: false,
|
|
770
|
+
error: "Forbidden. This endpoint requires admin write permission."
|
|
771
|
+
});
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
if (req.method === "GET" && url.pathname === "/api/admin/auth/status") {
|
|
775
|
+
this.sendJson(res, 200, {
|
|
776
|
+
ok: true,
|
|
777
|
+
data: {
|
|
778
|
+
authenticated: Boolean(authIdentity),
|
|
779
|
+
role: authIdentity?.role ?? null,
|
|
780
|
+
source: authIdentity?.source ?? "none",
|
|
781
|
+
actor: resolveIdentityActor(authIdentity),
|
|
782
|
+
canWrite: authIdentity ? hasRequiredAdminRole(authIdentity.role, "admin") : false
|
|
783
|
+
}
|
|
760
784
|
});
|
|
761
785
|
return;
|
|
762
786
|
}
|
|
@@ -770,7 +794,7 @@ var AdminServer = class {
|
|
|
770
794
|
}
|
|
771
795
|
if (req.method === "PUT" && url.pathname === "/api/admin/config/global") {
|
|
772
796
|
const body = await readJsonBody(req);
|
|
773
|
-
const actor =
|
|
797
|
+
const actor = resolveAuditActor(req, authIdentity);
|
|
774
798
|
const result = this.updateGlobalConfig(body, actor);
|
|
775
799
|
this.sendJson(res, 200, {
|
|
776
800
|
ok: true,
|
|
@@ -798,13 +822,13 @@ var AdminServer = class {
|
|
|
798
822
|
}
|
|
799
823
|
if (req.method === "PUT") {
|
|
800
824
|
const body = await readJsonBody(req);
|
|
801
|
-
const actor =
|
|
825
|
+
const actor = resolveAuditActor(req, authIdentity);
|
|
802
826
|
const room = this.updateRoomConfig(roomId, body, actor);
|
|
803
827
|
this.sendJson(res, 200, { ok: true, data: room });
|
|
804
828
|
return;
|
|
805
829
|
}
|
|
806
830
|
if (req.method === "DELETE") {
|
|
807
|
-
const actor =
|
|
831
|
+
const actor = resolveAuditActor(req, authIdentity);
|
|
808
832
|
this.configService.deleteRoomSettings(roomId, actor);
|
|
809
833
|
this.sendJson(res, 200, { ok: true, roomId });
|
|
810
834
|
return;
|
|
@@ -834,7 +858,7 @@ var AdminServer = class {
|
|
|
834
858
|
if (req.method === "POST" && url.pathname === "/api/admin/service/restart") {
|
|
835
859
|
const body = asObject(await readJsonBody(req), "service restart payload");
|
|
836
860
|
const restartAdmin = normalizeBoolean(body.withAdmin, false);
|
|
837
|
-
const actor =
|
|
861
|
+
const actor = resolveAuditActor(req, authIdentity);
|
|
838
862
|
try {
|
|
839
863
|
const result = await this.restartServices(restartAdmin);
|
|
840
864
|
this.stateStore.appendConfigRevision(
|
|
@@ -999,6 +1023,12 @@ var AdminServer = class {
|
|
|
999
1023
|
envUpdates.SESSION_ACTIVE_WINDOW_MINUTES = String(value);
|
|
1000
1024
|
updatedKeys.push("sessionActiveWindowMinutes");
|
|
1001
1025
|
}
|
|
1026
|
+
if ("groupDirectModeEnabled" in body) {
|
|
1027
|
+
const value = normalizeBoolean(body.groupDirectModeEnabled, this.config.groupDirectModeEnabled);
|
|
1028
|
+
this.config.groupDirectModeEnabled = value;
|
|
1029
|
+
envUpdates.GROUP_DIRECT_MODE_ENABLED = String(value);
|
|
1030
|
+
updatedKeys.push("groupDirectModeEnabled");
|
|
1031
|
+
}
|
|
1002
1032
|
if ("cliCompat" in body) {
|
|
1003
1033
|
const compat = asObject(body.cliCompat, "cliCompat");
|
|
1004
1034
|
if ("enabled" in compat) {
|
|
@@ -1092,14 +1122,34 @@ var AdminServer = class {
|
|
|
1092
1122
|
summary: normalizeOptionalString(body.summary)
|
|
1093
1123
|
});
|
|
1094
1124
|
}
|
|
1095
|
-
|
|
1096
|
-
if (!this.adminToken) {
|
|
1097
|
-
return
|
|
1125
|
+
resolveAdminIdentity(req) {
|
|
1126
|
+
if (!this.adminToken && this.adminTokens.size === 0) {
|
|
1127
|
+
return {
|
|
1128
|
+
role: "admin",
|
|
1129
|
+
actor: null,
|
|
1130
|
+
source: "open"
|
|
1131
|
+
};
|
|
1098
1132
|
}
|
|
1099
|
-
const
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1133
|
+
const token = readAdminToken(req);
|
|
1134
|
+
if (!token) {
|
|
1135
|
+
return null;
|
|
1136
|
+
}
|
|
1137
|
+
if (this.adminToken && token === this.adminToken) {
|
|
1138
|
+
return {
|
|
1139
|
+
role: "admin",
|
|
1140
|
+
actor: null,
|
|
1141
|
+
source: "legacy"
|
|
1142
|
+
};
|
|
1143
|
+
}
|
|
1144
|
+
const mappedIdentity = this.adminTokens.get(token);
|
|
1145
|
+
if (!mappedIdentity) {
|
|
1146
|
+
return null;
|
|
1147
|
+
}
|
|
1148
|
+
return {
|
|
1149
|
+
role: mappedIdentity.role,
|
|
1150
|
+
actor: mappedIdentity.actor,
|
|
1151
|
+
source: "scoped"
|
|
1152
|
+
};
|
|
1103
1153
|
}
|
|
1104
1154
|
isClientAllowed(req) {
|
|
1105
1155
|
if (this.adminIpAllowlist.length === 0) {
|
|
@@ -1174,6 +1224,7 @@ function buildGlobalConfigSnapshot(config) {
|
|
|
1174
1224
|
matrixCommandPrefix: config.matrixCommandPrefix,
|
|
1175
1225
|
codexWorkdir: config.codexWorkdir,
|
|
1176
1226
|
rateLimiter: { ...config.rateLimiter },
|
|
1227
|
+
groupDirectModeEnabled: config.groupDirectModeEnabled,
|
|
1177
1228
|
defaultGroupTriggerPolicy: { ...config.defaultGroupTriggerPolicy },
|
|
1178
1229
|
matrixProgressUpdates: config.matrixProgressUpdates,
|
|
1179
1230
|
matrixProgressMinIntervalMs: config.matrixProgressMinIntervalMs,
|
|
@@ -1391,10 +1442,61 @@ function normalizeHeaderValue(value) {
|
|
|
1391
1442
|
}
|
|
1392
1443
|
return value.trim();
|
|
1393
1444
|
}
|
|
1394
|
-
function
|
|
1445
|
+
function readAdminToken(req) {
|
|
1446
|
+
const authorization = normalizeHeaderValue(req.headers.authorization);
|
|
1447
|
+
if (authorization) {
|
|
1448
|
+
const match = /^bearer\s+(.+)$/i.exec(authorization);
|
|
1449
|
+
const token = match?.[1]?.trim() ?? "";
|
|
1450
|
+
if (token) {
|
|
1451
|
+
return token;
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
const fromHeader = normalizeHeaderValue(req.headers["x-admin-token"]);
|
|
1455
|
+
return fromHeader || null;
|
|
1456
|
+
}
|
|
1457
|
+
function resolveIdentityActor(identity) {
|
|
1458
|
+
if (!identity || identity.source !== "scoped") {
|
|
1459
|
+
return null;
|
|
1460
|
+
}
|
|
1461
|
+
if (identity.actor) {
|
|
1462
|
+
return identity.actor;
|
|
1463
|
+
}
|
|
1464
|
+
return identity.role === "admin" ? "admin-token" : "viewer-token";
|
|
1465
|
+
}
|
|
1466
|
+
function resolveAuditActor(req, identity) {
|
|
1467
|
+
const scopedActor = resolveIdentityActor(identity);
|
|
1468
|
+
if (scopedActor) {
|
|
1469
|
+
return scopedActor;
|
|
1470
|
+
}
|
|
1395
1471
|
const actor = normalizeHeaderValue(req.headers["x-admin-actor"]);
|
|
1396
1472
|
return actor || null;
|
|
1397
1473
|
}
|
|
1474
|
+
function requiredAdminRoleForRequest(method, pathname) {
|
|
1475
|
+
if (!pathname.startsWith("/api/admin/")) {
|
|
1476
|
+
return null;
|
|
1477
|
+
}
|
|
1478
|
+
const normalizedMethod = (method ?? "GET").toUpperCase();
|
|
1479
|
+
if (normalizedMethod === "GET" || normalizedMethod === "HEAD") {
|
|
1480
|
+
return "viewer";
|
|
1481
|
+
}
|
|
1482
|
+
return "admin";
|
|
1483
|
+
}
|
|
1484
|
+
function hasRequiredAdminRole(role, requiredRole) {
|
|
1485
|
+
if (requiredRole === "viewer") {
|
|
1486
|
+
return role === "viewer" || role === "admin";
|
|
1487
|
+
}
|
|
1488
|
+
return role === "admin";
|
|
1489
|
+
}
|
|
1490
|
+
function buildAdminTokenMap(tokens) {
|
|
1491
|
+
const mapped = /* @__PURE__ */ new Map();
|
|
1492
|
+
for (const token of tokens) {
|
|
1493
|
+
mapped.set(token.token, {
|
|
1494
|
+
role: token.role,
|
|
1495
|
+
actor: token.actor
|
|
1496
|
+
});
|
|
1497
|
+
}
|
|
1498
|
+
return mapped;
|
|
1499
|
+
}
|
|
1398
1500
|
function formatError(error) {
|
|
1399
1501
|
if (error instanceof Error) {
|
|
1400
1502
|
return error.message;
|
|
@@ -1642,6 +1744,7 @@ var ADMIN_CONSOLE_HTML = `<!doctype html>
|
|
|
1642
1744
|
<button id="auth-clear-btn" type="button" class="secondary">Clear Auth</button>
|
|
1643
1745
|
</div>
|
|
1644
1746
|
<div id="notice" class="notice">Ready.</div>
|
|
1747
|
+
<p id="auth-role" class="muted">Permission: unknown</p>
|
|
1645
1748
|
</section>
|
|
1646
1749
|
|
|
1647
1750
|
<section class="panel" data-view="settings-global">
|
|
@@ -1697,6 +1800,7 @@ var ADMIN_CONSOLE_HTML = `<!doctype html>
|
|
|
1697
1800
|
<input id="global-concurrency-room" type="number" min="0" />
|
|
1698
1801
|
</label>
|
|
1699
1802
|
|
|
1803
|
+
<label class="checkbox"><input id="global-direct-mode" type="checkbox" /><span>Group direct mode (no trigger required)</span></label>
|
|
1700
1804
|
<label class="checkbox"><input id="global-trigger-mention" type="checkbox" /><span>Trigger: mention</span></label>
|
|
1701
1805
|
<label class="checkbox"><input id="global-trigger-reply" type="checkbox" /><span>Trigger: reply</span></label>
|
|
1702
1806
|
<label class="checkbox"><input id="global-trigger-window" type="checkbox" /><span>Trigger: active window</span></label>
|
|
@@ -1841,6 +1945,7 @@ var ADMIN_CONSOLE_HTML = `<!doctype html>
|
|
|
1841
1945
|
var tokenInput = document.getElementById("auth-token");
|
|
1842
1946
|
var actorInput = document.getElementById("auth-actor");
|
|
1843
1947
|
var noticeNode = document.getElementById("notice");
|
|
1948
|
+
var authRoleNode = document.getElementById("auth-role");
|
|
1844
1949
|
var roomListBody = document.getElementById("room-list-body");
|
|
1845
1950
|
var healthBody = document.getElementById("health-body");
|
|
1846
1951
|
var auditBody = document.getElementById("audit-body");
|
|
@@ -1852,6 +1957,7 @@ var ADMIN_CONSOLE_HTML = `<!doctype html>
|
|
|
1852
1957
|
localStorage.setItem(storageTokenKey, tokenInput.value.trim());
|
|
1853
1958
|
localStorage.setItem(storageActorKey, actorInput.value.trim());
|
|
1854
1959
|
showNotice("ok", "Auth settings saved to localStorage.");
|
|
1960
|
+
void refreshAuthStatus();
|
|
1855
1961
|
});
|
|
1856
1962
|
|
|
1857
1963
|
document.getElementById("auth-clear-btn").addEventListener("click", function () {
|
|
@@ -1860,6 +1966,7 @@ var ADMIN_CONSOLE_HTML = `<!doctype html>
|
|
|
1860
1966
|
localStorage.removeItem(storageTokenKey);
|
|
1861
1967
|
localStorage.removeItem(storageActorKey);
|
|
1862
1968
|
showNotice("warn", "Auth settings cleared.");
|
|
1969
|
+
void refreshAuthStatus();
|
|
1863
1970
|
});
|
|
1864
1971
|
|
|
1865
1972
|
document.getElementById("global-save-btn").addEventListener("click", saveGlobal);
|
|
@@ -1884,6 +1991,7 @@ var ADMIN_CONSOLE_HTML = `<!doctype html>
|
|
|
1884
1991
|
} else {
|
|
1885
1992
|
handleRoute();
|
|
1886
1993
|
}
|
|
1994
|
+
void refreshAuthStatus();
|
|
1887
1995
|
|
|
1888
1996
|
function getCurrentView() {
|
|
1889
1997
|
return routeToView[window.location.hash] || "settings-global";
|
|
@@ -1974,6 +2082,29 @@ var ADMIN_CONSOLE_HTML = `<!doctype html>
|
|
|
1974
2082
|
noticeNode.textContent = message;
|
|
1975
2083
|
}
|
|
1976
2084
|
|
|
2085
|
+
async function refreshAuthStatus() {
|
|
2086
|
+
try {
|
|
2087
|
+
var response = await apiRequest("/api/admin/auth/status", "GET");
|
|
2088
|
+
var data = response.data || {};
|
|
2089
|
+
if (!data.role) {
|
|
2090
|
+
authRoleNode.textContent = "Permission: unauthenticated";
|
|
2091
|
+
return;
|
|
2092
|
+
}
|
|
2093
|
+
|
|
2094
|
+
var role = String(data.role).toUpperCase();
|
|
2095
|
+
var source = data.source ? " (" + String(data.source) + ")" : "";
|
|
2096
|
+
var actor = data.actor ? " as " + String(data.actor) : "";
|
|
2097
|
+
authRoleNode.textContent = "Permission: " + role + source + actor;
|
|
2098
|
+
} catch (error) {
|
|
2099
|
+
var message = error && error.message ? String(error.message) : "";
|
|
2100
|
+
if (/Unauthorized/i.test(message)) {
|
|
2101
|
+
authRoleNode.textContent = "Permission: unauthenticated";
|
|
2102
|
+
return;
|
|
2103
|
+
}
|
|
2104
|
+
authRoleNode.textContent = "Permission: unknown";
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
|
|
1977
2108
|
function renderEmptyRow(body, columns, text) {
|
|
1978
2109
|
body.innerHTML = "";
|
|
1979
2110
|
var row = document.createElement("tr");
|
|
@@ -2005,6 +2136,7 @@ var ADMIN_CONSOLE_HTML = `<!doctype html>
|
|
|
2005
2136
|
document.getElementById("global-concurrency-global").value = String(rateLimiter.maxConcurrentGlobal || 0);
|
|
2006
2137
|
document.getElementById("global-concurrency-user").value = String(rateLimiter.maxConcurrentPerUser || 0);
|
|
2007
2138
|
document.getElementById("global-concurrency-room").value = String(rateLimiter.maxConcurrentPerRoom || 0);
|
|
2139
|
+
document.getElementById("global-direct-mode").checked = Boolean(data.groupDirectModeEnabled);
|
|
2008
2140
|
|
|
2009
2141
|
document.getElementById("global-trigger-mention").checked = Boolean(trigger.allowMention);
|
|
2010
2142
|
document.getElementById("global-trigger-reply").checked = Boolean(trigger.allowReply);
|
|
@@ -2037,6 +2169,7 @@ var ADMIN_CONSOLE_HTML = `<!doctype html>
|
|
|
2037
2169
|
matrixProgressMinIntervalMs: asNumber("global-progress-interval", 2500),
|
|
2038
2170
|
matrixTypingTimeoutMs: asNumber("global-typing-timeout", 10000),
|
|
2039
2171
|
sessionActiveWindowMinutes: asNumber("global-active-window", 20),
|
|
2172
|
+
groupDirectModeEnabled: asBool("global-direct-mode"),
|
|
2040
2173
|
rateLimiter: {
|
|
2041
2174
|
windowMs: asNumber("global-rate-window", 60000),
|
|
2042
2175
|
maxRequestsPerUser: asNumber("global-rate-user", 20),
|
|
@@ -2460,6 +2593,7 @@ function findBreakIndex(candidate) {
|
|
|
2460
2593
|
}
|
|
2461
2594
|
|
|
2462
2595
|
// src/channels/matrix-channel.ts
|
|
2596
|
+
var LOCAL_TXN_PREFIX = "codeharbor-";
|
|
2463
2597
|
var MatrixChannel = class {
|
|
2464
2598
|
config;
|
|
2465
2599
|
logger;
|
|
@@ -2500,7 +2634,7 @@ var MatrixChannel = class {
|
|
|
2500
2634
|
}
|
|
2501
2635
|
const chunks = this.splitReplies ? splitText(text, this.chunkSize) : [text];
|
|
2502
2636
|
for (const chunk of chunks) {
|
|
2503
|
-
await this.
|
|
2637
|
+
await this.sendRichText(conversationId, chunk, "m.text");
|
|
2504
2638
|
}
|
|
2505
2639
|
}
|
|
2506
2640
|
async sendNotice(conversationId, text) {
|
|
@@ -2509,7 +2643,7 @@ var MatrixChannel = class {
|
|
|
2509
2643
|
}
|
|
2510
2644
|
const chunks = this.splitReplies ? splitText(text, this.chunkSize) : [text];
|
|
2511
2645
|
for (const chunk of chunks) {
|
|
2512
|
-
await this.
|
|
2646
|
+
await this.sendRichText(conversationId, chunk, "m.notice");
|
|
2513
2647
|
}
|
|
2514
2648
|
}
|
|
2515
2649
|
async upsertProgressNotice(conversationId, text, replaceEventId) {
|
|
@@ -2521,7 +2655,10 @@ var MatrixChannel = class {
|
|
|
2521
2655
|
throw new Error("Progress notice cannot be empty.");
|
|
2522
2656
|
}
|
|
2523
2657
|
if (!replaceEventId) {
|
|
2524
|
-
const response2 = await this.
|
|
2658
|
+
const response2 = await this.sendRawEvent(
|
|
2659
|
+
conversationId,
|
|
2660
|
+
buildMatrixRichMessageContent(normalized, "m.notice")
|
|
2661
|
+
);
|
|
2525
2662
|
return response2.event_id;
|
|
2526
2663
|
}
|
|
2527
2664
|
const content = {
|
|
@@ -2536,8 +2673,7 @@ var MatrixChannel = class {
|
|
|
2536
2673
|
event_id: replaceEventId
|
|
2537
2674
|
}
|
|
2538
2675
|
};
|
|
2539
|
-
const
|
|
2540
|
-
const response = await sendEditEvent(conversationId, import_matrix_js_sdk.EventType.RoomMessage, content);
|
|
2676
|
+
const response = await this.sendRawEvent(conversationId, content);
|
|
2541
2677
|
return response.event_id;
|
|
2542
2678
|
}
|
|
2543
2679
|
async setTyping(conversationId, isTyping, timeoutMs) {
|
|
@@ -2573,7 +2709,10 @@ var MatrixChannel = class {
|
|
|
2573
2709
|
return;
|
|
2574
2710
|
}
|
|
2575
2711
|
const senderId = event.getSender();
|
|
2576
|
-
if (!senderId
|
|
2712
|
+
if (!senderId) {
|
|
2713
|
+
return;
|
|
2714
|
+
}
|
|
2715
|
+
if (senderId === this.config.matrixUserId && isLikelyLocalEcho(event)) {
|
|
2577
2716
|
return;
|
|
2578
2717
|
}
|
|
2579
2718
|
const content = event.getContent();
|
|
@@ -2677,6 +2816,32 @@ var MatrixChannel = class {
|
|
|
2677
2816
|
await this.joinInvitedRoom(room.roomId);
|
|
2678
2817
|
}
|
|
2679
2818
|
}
|
|
2819
|
+
async sendRichText(conversationId, text, msgtype) {
|
|
2820
|
+
const payload = buildMatrixRichMessageContent(text, msgtype);
|
|
2821
|
+
await this.sendRawEvent(conversationId, payload);
|
|
2822
|
+
}
|
|
2823
|
+
async sendRawEvent(conversationId, content) {
|
|
2824
|
+
const txnId = `${LOCAL_TXN_PREFIX}${Date.now()}-${Math.floor(Math.random() * 1e6)}`;
|
|
2825
|
+
const response = await fetch(
|
|
2826
|
+
`${this.config.matrixHomeserver}/_matrix/client/v3/rooms/${encodeURIComponent(conversationId)}/send/m.room.message/${encodeURIComponent(txnId)}`,
|
|
2827
|
+
{
|
|
2828
|
+
method: "PUT",
|
|
2829
|
+
headers: {
|
|
2830
|
+
Authorization: `Bearer ${this.config.matrixAccessToken}`,
|
|
2831
|
+
"Content-Type": "application/json"
|
|
2832
|
+
},
|
|
2833
|
+
body: JSON.stringify(content)
|
|
2834
|
+
}
|
|
2835
|
+
);
|
|
2836
|
+
if (!response.ok) {
|
|
2837
|
+
throw new Error(`Matrix send failed (${response.status} ${response.statusText})`);
|
|
2838
|
+
}
|
|
2839
|
+
const payload = await response.json();
|
|
2840
|
+
if (!payload.event_id || typeof payload.event_id !== "string") {
|
|
2841
|
+
throw new Error("Matrix send failed (missing event_id)");
|
|
2842
|
+
}
|
|
2843
|
+
return { event_id: payload.event_id };
|
|
2844
|
+
}
|
|
2680
2845
|
async hydrateAttachments(attachments, eventId) {
|
|
2681
2846
|
if (!this.fetchMedia || attachments.length === 0) {
|
|
2682
2847
|
return attachments;
|
|
@@ -2747,6 +2912,17 @@ function buildRequestId(eventId) {
|
|
|
2747
2912
|
const suffix = Math.random().toString(36).slice(2, 8);
|
|
2748
2913
|
return `${eventId}:${suffix}`;
|
|
2749
2914
|
}
|
|
2915
|
+
function isLikelyLocalEcho(event) {
|
|
2916
|
+
const unsigned = event.getUnsigned();
|
|
2917
|
+
if (!unsigned || typeof unsigned !== "object") {
|
|
2918
|
+
return false;
|
|
2919
|
+
}
|
|
2920
|
+
const transactionId = unsigned.transaction_id;
|
|
2921
|
+
if (typeof transactionId !== "string" || !transactionId) {
|
|
2922
|
+
return false;
|
|
2923
|
+
}
|
|
2924
|
+
return transactionId.startsWith(LOCAL_TXN_PREFIX);
|
|
2925
|
+
}
|
|
2750
2926
|
function isDirectRoom(room) {
|
|
2751
2927
|
return room.getJoinedMemberCount() <= 2;
|
|
2752
2928
|
}
|
|
@@ -2838,6 +3014,61 @@ function resolveFileExtension(fileName, mimeType) {
|
|
|
2838
3014
|
}
|
|
2839
3015
|
return ".bin";
|
|
2840
3016
|
}
|
|
3017
|
+
function buildMatrixRichMessageContent(body, msgtype) {
|
|
3018
|
+
return {
|
|
3019
|
+
msgtype,
|
|
3020
|
+
body,
|
|
3021
|
+
format: "org.matrix.custom.html",
|
|
3022
|
+
formatted_body: renderMatrixHtml(body, msgtype)
|
|
3023
|
+
};
|
|
3024
|
+
}
|
|
3025
|
+
function renderMatrixHtml(body, msgtype) {
|
|
3026
|
+
const normalized = body.replace(/\r\n/g, "\n");
|
|
3027
|
+
const sections = [];
|
|
3028
|
+
const codeFencePattern = /```([^\n`]*)\n?([\s\S]*?)```/g;
|
|
3029
|
+
let cursor = 0;
|
|
3030
|
+
let match;
|
|
3031
|
+
while ((match = codeFencePattern.exec(normalized)) !== null) {
|
|
3032
|
+
const before = normalized.slice(cursor, match.index);
|
|
3033
|
+
const renderedBefore = renderTextSection(before);
|
|
3034
|
+
if (renderedBefore) {
|
|
3035
|
+
sections.push(renderedBefore);
|
|
3036
|
+
}
|
|
3037
|
+
const language = escapeHtml(match[1]?.trim() || "text");
|
|
3038
|
+
const code = escapeHtml(match[2].replace(/\n$/, ""));
|
|
3039
|
+
const label = language && language !== "text" ? `\u4EE3\u7801 (${language})` : "\u4EE3\u7801";
|
|
3040
|
+
sections.push(
|
|
3041
|
+
`<p><font color="#3558d1"><b>${label}</b></font></p><pre><code>${code}</code></pre>`
|
|
3042
|
+
);
|
|
3043
|
+
cursor = match.index + match[0].length;
|
|
3044
|
+
}
|
|
3045
|
+
const tail = normalized.slice(cursor);
|
|
3046
|
+
const renderedTail = renderTextSection(tail);
|
|
3047
|
+
if (renderedTail) {
|
|
3048
|
+
sections.push(renderedTail);
|
|
3049
|
+
}
|
|
3050
|
+
if (sections.length === 0) {
|
|
3051
|
+
sections.push("<p>(\u7A7A\u6D88\u606F)</p>");
|
|
3052
|
+
}
|
|
3053
|
+
const badge = msgtype === "m.notice" ? `<p><font color="#8a5a00"><b>\u{1F4E3} CodeHarbor \u63D0\u793A</b></font></p>` : `<p><font color="#1f7a5a"><b>\u{1F916} AI \u56DE\u590D</b></font></p>`;
|
|
3054
|
+
return `<div>${badge}${sections.join("")}</div>`;
|
|
3055
|
+
}
|
|
3056
|
+
function renderTextSection(raw) {
|
|
3057
|
+
if (!raw.trim()) {
|
|
3058
|
+
return "";
|
|
3059
|
+
}
|
|
3060
|
+
const normalized = raw.replace(/\r\n/g, "\n").trim();
|
|
3061
|
+
const paragraphs = normalized.split(/\n{2,}/);
|
|
3062
|
+
const rendered = paragraphs.map((paragraph) => {
|
|
3063
|
+
const escaped = escapeHtml(paragraph);
|
|
3064
|
+
const inlineCode = escaped.replace(/`([^`\n]+)`/g, "<code>$1</code>");
|
|
3065
|
+
return `<p>${inlineCode.replace(/\n/g, "<br/>")}</p>`;
|
|
3066
|
+
}).join("");
|
|
3067
|
+
return rendered;
|
|
3068
|
+
}
|
|
3069
|
+
function escapeHtml(value) {
|
|
3070
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
3071
|
+
}
|
|
2841
3072
|
|
|
2842
3073
|
// src/config-service.ts
|
|
2843
3074
|
var import_node_fs5 = __toESM(require("fs"));
|
|
@@ -4040,6 +4271,7 @@ var Orchestrator = class {
|
|
|
4040
4271
|
commandPrefix;
|
|
4041
4272
|
matrixUserId;
|
|
4042
4273
|
sessionActiveWindowMs;
|
|
4274
|
+
groupDirectModeEnabled;
|
|
4043
4275
|
defaultGroupTriggerPolicy;
|
|
4044
4276
|
roomTriggerPolicies;
|
|
4045
4277
|
configService;
|
|
@@ -4077,6 +4309,7 @@ var Orchestrator = class {
|
|
|
4077
4309
|
this.matrixUserId = options?.matrixUserId ?? "";
|
|
4078
4310
|
const sessionActiveWindowMinutes = options?.sessionActiveWindowMinutes ?? 20;
|
|
4079
4311
|
this.sessionActiveWindowMs = Math.max(1, sessionActiveWindowMinutes) * 6e4;
|
|
4312
|
+
this.groupDirectModeEnabled = options?.groupDirectModeEnabled ?? false;
|
|
4080
4313
|
this.defaultGroupTriggerPolicy = options?.defaultGroupTriggerPolicy ?? {
|
|
4081
4314
|
allowMention: true,
|
|
4082
4315
|
allowReply: true,
|
|
@@ -4411,7 +4644,7 @@ var Orchestrator = class {
|
|
|
4411
4644
|
const prefixTriggered = prefixAllowed && this.commandPrefix.length > 0;
|
|
4412
4645
|
const prefixedText = prefixTriggered ? extractCommandText(incomingTrimmed, this.commandPrefix) : null;
|
|
4413
4646
|
const activeSession = message.isDirectMessage || groupPolicy?.allowActiveWindow ? this.stateStore.isSessionActive(sessionKey) : false;
|
|
4414
|
-
const conversationalTrigger = message.isDirectMessage || Boolean(groupPolicy?.allowMention) && message.mentionsBot || Boolean(groupPolicy?.allowReply) && message.repliesToBot || activeSession;
|
|
4647
|
+
const conversationalTrigger = message.isDirectMessage || this.groupDirectModeEnabled || Boolean(groupPolicy?.allowMention) && message.mentionsBot || Boolean(groupPolicy?.allowReply) && message.repliesToBot || activeSession;
|
|
4415
4648
|
if (!conversationalTrigger && prefixedText === null) {
|
|
4416
4649
|
return { kind: "ignore" };
|
|
4417
4650
|
}
|
|
@@ -4449,7 +4682,7 @@ var Orchestrator = class {
|
|
|
4449
4682
|
}
|
|
4450
4683
|
const status = this.stateStore.getSessionStatus(sessionKey);
|
|
4451
4684
|
const roomConfig = this.resolveRoomRuntimeConfig(message.conversationId);
|
|
4452
|
-
const scope = message.isDirectMessage ? "\u79C1\u804A\uFF08\u514D\u524D\u7F00\uFF09" : "\u7FA4\u804A\uFF08\u6309\u623F\u95F4\u89E6\u53D1\u7B56\u7565\uFF09";
|
|
4685
|
+
const scope = message.isDirectMessage ? "\u79C1\u804A\uFF08\u514D\u524D\u7F00\uFF09" : this.groupDirectModeEnabled ? "\u7FA4\u804A\uFF08\u9ED8\u8BA4\u76F4\u901A\uFF09" : "\u7FA4\u804A\uFF08\u6309\u623F\u95F4\u89E6\u53D1\u7B56\u7565\uFF09";
|
|
4453
4686
|
const activeUntil = status.activeUntil ?? "\u672A\u6FC0\u6D3B";
|
|
4454
4687
|
const metrics = this.metrics.snapshot(this.runningExecutions.size);
|
|
4455
4688
|
const limiter = this.rateLimiter.snapshot();
|
|
@@ -5525,6 +5758,7 @@ var CodeHarborApp = class {
|
|
|
5525
5758
|
commandPrefix: config.matrixCommandPrefix,
|
|
5526
5759
|
matrixUserId: config.matrixUserId,
|
|
5527
5760
|
sessionActiveWindowMinutes: config.sessionActiveWindowMinutes,
|
|
5761
|
+
groupDirectModeEnabled: config.groupDirectModeEnabled,
|
|
5528
5762
|
defaultGroupTriggerPolicy: config.defaultGroupTriggerPolicy,
|
|
5529
5763
|
roomTriggerPolicies: config.roomTriggerPolicies,
|
|
5530
5764
|
rateLimiterOptions: config.rateLimiter,
|
|
@@ -5573,6 +5807,7 @@ var CodeHarborAdminApp = class {
|
|
|
5573
5807
|
host: options?.host ?? config.adminBindHost,
|
|
5574
5808
|
port: options?.port ?? config.adminPort,
|
|
5575
5809
|
adminToken: config.adminToken,
|
|
5810
|
+
adminTokens: config.adminTokens,
|
|
5576
5811
|
adminIpAllowlist: config.adminIpAllowlist,
|
|
5577
5812
|
adminAllowedOrigins: config.adminAllowedOrigins
|
|
5578
5813
|
});
|
|
@@ -5583,7 +5818,7 @@ var CodeHarborAdminApp = class {
|
|
|
5583
5818
|
this.logger.info("CodeHarbor admin server started", {
|
|
5584
5819
|
host: address?.host ?? this.config.adminBindHost,
|
|
5585
5820
|
port: address?.port ?? this.config.adminPort,
|
|
5586
|
-
tokenProtected: Boolean(this.config.adminToken)
|
|
5821
|
+
tokenProtected: Boolean(this.config.adminToken) || this.config.adminTokens.length > 0
|
|
5587
5822
|
});
|
|
5588
5823
|
}
|
|
5589
5824
|
async stop() {
|
|
@@ -5666,6 +5901,7 @@ var configSchema = import_zod.z.object({
|
|
|
5666
5901
|
MATRIX_PROGRESS_MIN_INTERVAL_MS: import_zod.z.string().default("2500").transform((v) => Number.parseInt(v, 10)).pipe(import_zod.z.number().int().positive()),
|
|
5667
5902
|
MATRIX_TYPING_TIMEOUT_MS: import_zod.z.string().default("10000").transform((v) => Number.parseInt(v, 10)).pipe(import_zod.z.number().int().positive()),
|
|
5668
5903
|
SESSION_ACTIVE_WINDOW_MINUTES: import_zod.z.string().default("20").transform((v) => Number.parseInt(v, 10)).pipe(import_zod.z.number().int().positive()),
|
|
5904
|
+
GROUP_DIRECT_MODE_ENABLED: import_zod.z.string().default("false").transform((v) => v.toLowerCase() === "true"),
|
|
5669
5905
|
GROUP_TRIGGER_ALLOW_MENTION: import_zod.z.string().default("true").transform((v) => v.toLowerCase() === "true"),
|
|
5670
5906
|
GROUP_TRIGGER_ALLOW_REPLY: import_zod.z.string().default("true").transform((v) => v.toLowerCase() === "true"),
|
|
5671
5907
|
GROUP_TRIGGER_ALLOW_ACTIVE_WINDOW: import_zod.z.string().default("true").transform((v) => v.toLowerCase() === "true"),
|
|
@@ -5688,6 +5924,7 @@ var configSchema = import_zod.z.object({
|
|
|
5688
5924
|
ADMIN_BIND_HOST: import_zod.z.string().default("127.0.0.1"),
|
|
5689
5925
|
ADMIN_PORT: import_zod.z.string().default("8787").transform((v) => Number.parseInt(v, 10)).pipe(import_zod.z.number().int().min(1).max(65535)),
|
|
5690
5926
|
ADMIN_TOKEN: import_zod.z.string().default(""),
|
|
5927
|
+
ADMIN_TOKENS_JSON: import_zod.z.string().default(""),
|
|
5691
5928
|
ADMIN_IP_ALLOWLIST: import_zod.z.string().default(""),
|
|
5692
5929
|
ADMIN_ALLOWED_ORIGINS: import_zod.z.string().default(""),
|
|
5693
5930
|
LOG_LEVEL: import_zod.z.enum(["debug", "info", "warn", "error"]).default("info")
|
|
@@ -5719,6 +5956,7 @@ var configSchema = import_zod.z.object({
|
|
|
5719
5956
|
matrixProgressMinIntervalMs: v.MATRIX_PROGRESS_MIN_INTERVAL_MS,
|
|
5720
5957
|
matrixTypingTimeoutMs: v.MATRIX_TYPING_TIMEOUT_MS,
|
|
5721
5958
|
sessionActiveWindowMinutes: v.SESSION_ACTIVE_WINDOW_MINUTES,
|
|
5959
|
+
groupDirectModeEnabled: v.GROUP_DIRECT_MODE_ENABLED,
|
|
5722
5960
|
defaultGroupTriggerPolicy: {
|
|
5723
5961
|
allowMention: v.GROUP_TRIGGER_ALLOW_MENTION,
|
|
5724
5962
|
allowReply: v.GROUP_TRIGGER_ALLOW_REPLY,
|
|
@@ -5747,6 +5985,7 @@ var configSchema = import_zod.z.object({
|
|
|
5747
5985
|
adminBindHost: v.ADMIN_BIND_HOST.trim() || "127.0.0.1",
|
|
5748
5986
|
adminPort: v.ADMIN_PORT,
|
|
5749
5987
|
adminToken: v.ADMIN_TOKEN.trim() || null,
|
|
5988
|
+
adminTokens: parseAdminTokens(v.ADMIN_TOKENS_JSON),
|
|
5750
5989
|
adminIpAllowlist: parseCsvList(v.ADMIN_IP_ALLOWLIST),
|
|
5751
5990
|
adminAllowedOrigins: parseCsvList(v.ADMIN_ALLOWED_ORIGINS),
|
|
5752
5991
|
logLevel: v.LOG_LEVEL
|
|
@@ -5837,6 +6076,53 @@ function parseExtraEnv(raw) {
|
|
|
5837
6076
|
function parseCsvList(raw) {
|
|
5838
6077
|
return raw.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0);
|
|
5839
6078
|
}
|
|
6079
|
+
function parseAdminTokens(raw) {
|
|
6080
|
+
const trimmed = raw.trim();
|
|
6081
|
+
if (!trimmed) {
|
|
6082
|
+
return [];
|
|
6083
|
+
}
|
|
6084
|
+
let parsed;
|
|
6085
|
+
try {
|
|
6086
|
+
parsed = JSON.parse(trimmed);
|
|
6087
|
+
} catch {
|
|
6088
|
+
throw new Error("ADMIN_TOKENS_JSON must be valid JSON.");
|
|
6089
|
+
}
|
|
6090
|
+
if (!Array.isArray(parsed)) {
|
|
6091
|
+
throw new Error("ADMIN_TOKENS_JSON must be a JSON array.");
|
|
6092
|
+
}
|
|
6093
|
+
const seenTokens = /* @__PURE__ */ new Set();
|
|
6094
|
+
return parsed.map((entry, index) => {
|
|
6095
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
6096
|
+
throw new Error(`ADMIN_TOKENS_JSON[${index}] must be an object.`);
|
|
6097
|
+
}
|
|
6098
|
+
const payload = entry;
|
|
6099
|
+
const tokenValue = payload.token;
|
|
6100
|
+
if (typeof tokenValue !== "string" || !tokenValue.trim()) {
|
|
6101
|
+
throw new Error(`ADMIN_TOKENS_JSON[${index}].token must be a non-empty string.`);
|
|
6102
|
+
}
|
|
6103
|
+
const token = tokenValue.trim();
|
|
6104
|
+
if (seenTokens.has(token)) {
|
|
6105
|
+
throw new Error(`ADMIN_TOKENS_JSON contains duplicated token at index ${index}.`);
|
|
6106
|
+
}
|
|
6107
|
+
seenTokens.add(token);
|
|
6108
|
+
let role = "admin";
|
|
6109
|
+
if (payload.role !== void 0) {
|
|
6110
|
+
if (payload.role !== "admin" && payload.role !== "viewer") {
|
|
6111
|
+
throw new Error(`ADMIN_TOKENS_JSON[${index}].role must be "admin" or "viewer".`);
|
|
6112
|
+
}
|
|
6113
|
+
role = payload.role;
|
|
6114
|
+
}
|
|
6115
|
+
if (payload.actor !== void 0 && payload.actor !== null && typeof payload.actor !== "string") {
|
|
6116
|
+
throw new Error(`ADMIN_TOKENS_JSON[${index}].actor must be a string when provided.`);
|
|
6117
|
+
}
|
|
6118
|
+
const actor = typeof payload.actor === "string" ? payload.actor.trim() || null : null;
|
|
6119
|
+
return {
|
|
6120
|
+
token,
|
|
6121
|
+
role,
|
|
6122
|
+
actor
|
|
6123
|
+
};
|
|
6124
|
+
});
|
|
6125
|
+
}
|
|
5840
6126
|
|
|
5841
6127
|
// src/config-snapshot.ts
|
|
5842
6128
|
var import_node_fs9 = __toESM(require("fs"));
|
|
@@ -5869,6 +6155,7 @@ var CONFIG_SNAPSHOT_ENV_KEYS = [
|
|
|
5869
6155
|
"MATRIX_PROGRESS_MIN_INTERVAL_MS",
|
|
5870
6156
|
"MATRIX_TYPING_TIMEOUT_MS",
|
|
5871
6157
|
"SESSION_ACTIVE_WINDOW_MINUTES",
|
|
6158
|
+
"GROUP_DIRECT_MODE_ENABLED",
|
|
5872
6159
|
"GROUP_TRIGGER_ALLOW_MENTION",
|
|
5873
6160
|
"GROUP_TRIGGER_ALLOW_REPLY",
|
|
5874
6161
|
"GROUP_TRIGGER_ALLOW_ACTIVE_WINDOW",
|
|
@@ -5891,6 +6178,7 @@ var CONFIG_SNAPSHOT_ENV_KEYS = [
|
|
|
5891
6178
|
"ADMIN_BIND_HOST",
|
|
5892
6179
|
"ADMIN_PORT",
|
|
5893
6180
|
"ADMIN_TOKEN",
|
|
6181
|
+
"ADMIN_TOKENS_JSON",
|
|
5894
6182
|
"ADMIN_IP_ALLOWLIST",
|
|
5895
6183
|
"ADMIN_ALLOWED_ORIGINS",
|
|
5896
6184
|
"LOG_LEVEL"
|
|
@@ -5933,6 +6221,7 @@ var envSnapshotSchema = import_zod2.z.object({
|
|
|
5933
6221
|
MATRIX_PROGRESS_MIN_INTERVAL_MS: integerStringSchema("MATRIX_PROGRESS_MIN_INTERVAL_MS", 1),
|
|
5934
6222
|
MATRIX_TYPING_TIMEOUT_MS: integerStringSchema("MATRIX_TYPING_TIMEOUT_MS", 1),
|
|
5935
6223
|
SESSION_ACTIVE_WINDOW_MINUTES: integerStringSchema("SESSION_ACTIVE_WINDOW_MINUTES", 1),
|
|
6224
|
+
GROUP_DIRECT_MODE_ENABLED: booleanStringSchema("GROUP_DIRECT_MODE_ENABLED").default("false"),
|
|
5936
6225
|
GROUP_TRIGGER_ALLOW_MENTION: booleanStringSchema("GROUP_TRIGGER_ALLOW_MENTION"),
|
|
5937
6226
|
GROUP_TRIGGER_ALLOW_REPLY: booleanStringSchema("GROUP_TRIGGER_ALLOW_REPLY"),
|
|
5938
6227
|
GROUP_TRIGGER_ALLOW_ACTIVE_WINDOW: booleanStringSchema("GROUP_TRIGGER_ALLOW_ACTIVE_WINDOW"),
|
|
@@ -5955,6 +6244,7 @@ var envSnapshotSchema = import_zod2.z.object({
|
|
|
5955
6244
|
ADMIN_BIND_HOST: import_zod2.z.string(),
|
|
5956
6245
|
ADMIN_PORT: integerStringSchema("ADMIN_PORT", 1, 65535),
|
|
5957
6246
|
ADMIN_TOKEN: import_zod2.z.string(),
|
|
6247
|
+
ADMIN_TOKENS_JSON: jsonArrayStringSchema("ADMIN_TOKENS_JSON", true).default(""),
|
|
5958
6248
|
ADMIN_IP_ALLOWLIST: import_zod2.z.string(),
|
|
5959
6249
|
ADMIN_ALLOWED_ORIGINS: import_zod2.z.string().default(""),
|
|
5960
6250
|
LOG_LEVEL: import_zod2.z.enum(LOG_LEVELS)
|
|
@@ -6121,6 +6411,7 @@ function buildSnapshotEnv(config) {
|
|
|
6121
6411
|
MATRIX_PROGRESS_MIN_INTERVAL_MS: String(config.matrixProgressMinIntervalMs),
|
|
6122
6412
|
MATRIX_TYPING_TIMEOUT_MS: String(config.matrixTypingTimeoutMs),
|
|
6123
6413
|
SESSION_ACTIVE_WINDOW_MINUTES: String(config.sessionActiveWindowMinutes),
|
|
6414
|
+
GROUP_DIRECT_MODE_ENABLED: String(config.groupDirectModeEnabled),
|
|
6124
6415
|
GROUP_TRIGGER_ALLOW_MENTION: String(config.defaultGroupTriggerPolicy.allowMention),
|
|
6125
6416
|
GROUP_TRIGGER_ALLOW_REPLY: String(config.defaultGroupTriggerPolicy.allowReply),
|
|
6126
6417
|
GROUP_TRIGGER_ALLOW_ACTIVE_WINDOW: String(config.defaultGroupTriggerPolicy.allowActiveWindow),
|
|
@@ -6143,6 +6434,7 @@ function buildSnapshotEnv(config) {
|
|
|
6143
6434
|
ADMIN_BIND_HOST: config.adminBindHost,
|
|
6144
6435
|
ADMIN_PORT: String(config.adminPort),
|
|
6145
6436
|
ADMIN_TOKEN: config.adminToken ?? "",
|
|
6437
|
+
ADMIN_TOKENS_JSON: serializeAdminTokens(config.adminTokens),
|
|
6146
6438
|
ADMIN_IP_ALLOWLIST: config.adminIpAllowlist.join(","),
|
|
6147
6439
|
ADMIN_ALLOWED_ORIGINS: config.adminAllowedOrigins.join(","),
|
|
6148
6440
|
LOG_LEVEL: config.logLevel
|
|
@@ -6232,6 +6524,9 @@ function parseIntStrict(raw) {
|
|
|
6232
6524
|
function serializeJsonObject(value) {
|
|
6233
6525
|
return Object.keys(value).length > 0 ? JSON.stringify(value) : "";
|
|
6234
6526
|
}
|
|
6527
|
+
function serializeAdminTokens(tokens) {
|
|
6528
|
+
return tokens.length > 0 ? JSON.stringify(tokens) : "";
|
|
6529
|
+
}
|
|
6235
6530
|
function booleanStringSchema(key) {
|
|
6236
6531
|
return import_zod2.z.string().refine((value) => BOOLEAN_STRING.test(value), {
|
|
6237
6532
|
message: `${key} must be a boolean string (true/false).`
|
|
@@ -6269,6 +6564,23 @@ function jsonObjectStringSchema(key, allowEmpty) {
|
|
|
6269
6564
|
message: `${key} must be an empty string or a JSON object string.`
|
|
6270
6565
|
});
|
|
6271
6566
|
}
|
|
6567
|
+
function jsonArrayStringSchema(key, allowEmpty) {
|
|
6568
|
+
return import_zod2.z.string().refine((value) => {
|
|
6569
|
+
const trimmed = value.trim();
|
|
6570
|
+
if (!trimmed) {
|
|
6571
|
+
return allowEmpty;
|
|
6572
|
+
}
|
|
6573
|
+
let parsed;
|
|
6574
|
+
try {
|
|
6575
|
+
parsed = JSON.parse(trimmed);
|
|
6576
|
+
} catch {
|
|
6577
|
+
return false;
|
|
6578
|
+
}
|
|
6579
|
+
return Array.isArray(parsed);
|
|
6580
|
+
}, {
|
|
6581
|
+
message: `${key} must be an empty string or a JSON array string.`
|
|
6582
|
+
});
|
|
6583
|
+
}
|
|
6272
6584
|
|
|
6273
6585
|
// src/preflight.ts
|
|
6274
6586
|
var import_node_child_process6 = require("child_process");
|
|
@@ -6461,11 +6773,12 @@ admin.command("serve").description("Start admin config API server").option("--ho
|
|
|
6461
6773
|
const host = options.host?.trim() || config.adminBindHost;
|
|
6462
6774
|
const port = options.port ? parsePortOption(options.port, config.adminPort) : config.adminPort;
|
|
6463
6775
|
const allowInsecureNoToken = options.allowInsecureNoToken ?? false;
|
|
6464
|
-
|
|
6776
|
+
const hasAdminAuth = Boolean(config.adminToken) || config.adminTokens.length > 0;
|
|
6777
|
+
if (!hasAdminAuth && !allowInsecureNoToken && isNonLoopbackHost(host)) {
|
|
6465
6778
|
process.stderr.write(
|
|
6466
6779
|
[
|
|
6467
|
-
"Refusing to start admin server on non-loopback host without
|
|
6468
|
-
"Fix: set ADMIN_TOKEN in .env, or explicitly pass --allow-insecure-no-token.",
|
|
6780
|
+
"Refusing to start admin server on non-loopback host without admin auth token.",
|
|
6781
|
+
"Fix: set ADMIN_TOKEN or ADMIN_TOKENS_JSON in .env, or explicitly pass --allow-insecure-no-token.",
|
|
6469
6782
|
""
|
|
6470
6783
|
].join("\n")
|
|
6471
6784
|
);
|