clay-server 2.10.0 → 2.11.0-beta.10
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/bin/cli.js +157 -1
- package/lib/daemon.js +341 -2
- package/lib/dm.js +135 -0
- package/lib/os-users.js +301 -0
- package/lib/pages.js +36 -0
- package/lib/project.js +386 -67
- package/lib/public/app.js +675 -17
- package/lib/public/css/admin.css +99 -10
- package/lib/public/css/filebrowser.css +22 -0
- package/lib/public/css/icon-strip.css +162 -1
- package/lib/public/css/menus.css +23 -0
- package/lib/public/css/messages.css +245 -0
- package/lib/public/css/overlays.css +88 -0
- package/lib/public/css/server-settings.css +30 -2
- package/lib/public/css/sidebar.css +4 -0
- package/lib/public/index.html +140 -66
- package/lib/public/modules/admin.js +179 -12
- package/lib/public/modules/input.js +13 -2
- package/lib/public/modules/notifications.js +3 -1
- package/lib/public/modules/project-settings.js +154 -168
- package/lib/public/modules/server-settings.js +78 -189
- package/lib/public/modules/settings-defaults.js +243 -0
- package/lib/public/modules/sidebar.js +112 -6
- package/lib/public/modules/terminal.js +48 -10
- package/lib/public/modules/tools.js +214 -1
- package/lib/sdk-bridge.js +634 -6
- package/lib/sdk-worker.js +446 -0
- package/lib/server.js +335 -3
- package/lib/sessions.js +26 -0
- package/lib/terminal-manager.js +2 -2
- package/lib/terminal.js +20 -4
- package/lib/updater.js +38 -11
- package/lib/users.js +79 -0
- package/package.json +2 -2
package/lib/server.js
CHANGED
|
@@ -7,8 +7,10 @@ var { pinPageHtml, setupPageHtml, adminSetupPageHtml, multiUserLoginPageHtml, sm
|
|
|
7
7
|
var smtp = require("./smtp");
|
|
8
8
|
var { createProjectContext } = require("./project");
|
|
9
9
|
var users = require("./users");
|
|
10
|
+
var dm = require("./dm");
|
|
10
11
|
|
|
11
12
|
var { CONFIG_DIR } = require("./config");
|
|
13
|
+
var { provisionLinuxUser } = require("./os-users");
|
|
12
14
|
|
|
13
15
|
var https = require("https");
|
|
14
16
|
|
|
@@ -325,12 +327,16 @@ function createServer(opts) {
|
|
|
325
327
|
var portNum = opts.port || 2633;
|
|
326
328
|
var debug = opts.debug || false;
|
|
327
329
|
var dangerouslySkipPermissions = opts.dangerouslySkipPermissions || false;
|
|
330
|
+
var osUsers = opts.osUsers || false;
|
|
328
331
|
var lanHost = opts.lanHost || null;
|
|
329
332
|
var onAddProject = opts.onAddProject || null;
|
|
333
|
+
var onCreateProject = opts.onCreateProject || null;
|
|
334
|
+
var onCloneProject = opts.onCloneProject || null;
|
|
330
335
|
var onRemoveProject = opts.onRemoveProject || null;
|
|
331
336
|
var onReorderProjects = opts.onReorderProjects || null;
|
|
332
337
|
var onSetProjectTitle = opts.onSetProjectTitle || null;
|
|
333
338
|
var onSetProjectIcon = opts.onSetProjectIcon || null;
|
|
339
|
+
var onProjectOwnerChanged = opts.onProjectOwnerChanged || null;
|
|
334
340
|
var onGetServerDefaultEffort = opts.onGetServerDefaultEffort || null;
|
|
335
341
|
var onSetServerDefaultEffort = opts.onSetServerDefaultEffort || null;
|
|
336
342
|
var onGetProjectDefaultEffort = opts.onGetProjectDefaultEffort || null;
|
|
@@ -347,10 +353,13 @@ function createServer(opts) {
|
|
|
347
353
|
var onSetPin = opts.onSetPin || null;
|
|
348
354
|
var onSetKeepAwake = opts.onSetKeepAwake || null;
|
|
349
355
|
var onShutdown = opts.onShutdown || null;
|
|
356
|
+
var onSetUpdateChannel = opts.onSetUpdateChannel || null;
|
|
350
357
|
var onUpgradePin = opts.onUpgradePin || null;
|
|
351
358
|
var onSetProjectVisibility = opts.onSetProjectVisibility || null;
|
|
352
359
|
var onSetProjectAllowedUsers = opts.onSetProjectAllowedUsers || null;
|
|
353
360
|
var onGetProjectAccess = opts.onGetProjectAccess || null;
|
|
361
|
+
var onUserProvisioned = opts.onUserProvisioned || null;
|
|
362
|
+
var onUserDeleted = opts.onUserDeleted || null;
|
|
354
363
|
|
|
355
364
|
var authToken = pinHash || null;
|
|
356
365
|
var realVersion = require("../package.json").version;
|
|
@@ -508,6 +517,14 @@ function createServer(opts) {
|
|
|
508
517
|
res.end(JSON.stringify({ error: result.error }));
|
|
509
518
|
return;
|
|
510
519
|
}
|
|
520
|
+
// Auto-provision Linux account if OS users mode is enabled
|
|
521
|
+
if (osUsers && !result.user.linuxUser) {
|
|
522
|
+
var provision = provisionLinuxUser(result.user.username);
|
|
523
|
+
if (provision.ok) {
|
|
524
|
+
users.updateLinuxUser(result.user.id, provision.linuxUser);
|
|
525
|
+
if (onUserProvisioned) onUserProvisioned(result.user.id, provision.linuxUser);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
511
528
|
users.clearSetupCode();
|
|
512
529
|
var session = createMultiUserSession(result.user.id, tlsOptions);
|
|
513
530
|
res.writeHead(200, {
|
|
@@ -552,11 +569,13 @@ function createServer(opts) {
|
|
|
552
569
|
}
|
|
553
570
|
clearPinFailures(ip);
|
|
554
571
|
var session = createMultiUserSession(user.id, tlsOptions);
|
|
572
|
+
var loginResp = { ok: true, user: { id: user.id, username: user.username, role: user.role } };
|
|
573
|
+
if (user.mustChangePin) loginResp.mustChangePin = true;
|
|
555
574
|
res.writeHead(200, {
|
|
556
575
|
"Set-Cookie": session.cookie,
|
|
557
576
|
"Content-Type": "application/json",
|
|
558
577
|
});
|
|
559
|
-
res.end(JSON.stringify(
|
|
578
|
+
res.end(JSON.stringify(loginResp));
|
|
560
579
|
} catch (e) {
|
|
561
580
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
562
581
|
res.end('{"error":"Invalid request"}');
|
|
@@ -726,6 +745,14 @@ function createServer(opts) {
|
|
|
726
745
|
res.end(JSON.stringify({ error: result.error }));
|
|
727
746
|
return;
|
|
728
747
|
}
|
|
748
|
+
// Auto-provision Linux account if OS users mode is enabled
|
|
749
|
+
if (osUsers && !result.user.linuxUser) {
|
|
750
|
+
var provision = provisionLinuxUser(result.user.username);
|
|
751
|
+
if (provision.ok) {
|
|
752
|
+
users.updateLinuxUser(result.user.id, provision.linuxUser);
|
|
753
|
+
if (onUserProvisioned) onUserProvisioned(result.user.id, provision.linuxUser);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
729
756
|
users.markInviteUsed(data.inviteCode);
|
|
730
757
|
var session = createMultiUserSession(result.user.id, tlsOptions);
|
|
731
758
|
res.writeHead(200, {
|
|
@@ -934,6 +961,45 @@ function createServer(opts) {
|
|
|
934
961
|
return;
|
|
935
962
|
}
|
|
936
963
|
|
|
964
|
+
// Change own PIN (multi-user mode)
|
|
965
|
+
if (req.method === "PUT" && fullUrl === "/api/user/pin") {
|
|
966
|
+
if (!users.isMultiUser()) {
|
|
967
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
968
|
+
res.end('{"error":"Not found"}');
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
971
|
+
var mu = getMultiUserFromReq(req);
|
|
972
|
+
if (!mu) {
|
|
973
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
974
|
+
res.end('{"error":"unauthorized"}');
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
var body = "";
|
|
978
|
+
req.on("data", function (chunk) { body += chunk; });
|
|
979
|
+
req.on("end", function () {
|
|
980
|
+
try {
|
|
981
|
+
var data = JSON.parse(body);
|
|
982
|
+
if (!data.newPin || typeof data.newPin !== "string" || !/^\d{6}$/.test(data.newPin)) {
|
|
983
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
984
|
+
res.end('{"error":"PIN must be exactly 6 digits"}');
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
var result = users.updateUserPin(mu.id, data.newPin);
|
|
988
|
+
if (result.error) {
|
|
989
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
990
|
+
res.end(JSON.stringify({ error: result.error }));
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
994
|
+
res.end('{"ok":true}');
|
|
995
|
+
} catch (e) {
|
|
996
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
997
|
+
res.end('{"error":"Invalid request"}');
|
|
998
|
+
}
|
|
999
|
+
});
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
937
1003
|
// --- Admin API endpoints (multi-user mode only) ---
|
|
938
1004
|
|
|
939
1005
|
// List all users (admin only)
|
|
@@ -973,17 +1039,121 @@ function createServer(opts) {
|
|
|
973
1039
|
res.end('{"error":"Cannot remove yourself"}');
|
|
974
1040
|
return;
|
|
975
1041
|
}
|
|
1042
|
+
// Look up the user before deletion to get linuxUser for deactivation
|
|
1043
|
+
var targetUser = users.findUserById(targetUserId);
|
|
1044
|
+
var targetLinuxUser = targetUser ? targetUser.linuxUser : null;
|
|
976
1045
|
var result = users.removeUser(targetUserId);
|
|
977
1046
|
if (result.error) {
|
|
978
1047
|
res.writeHead(404, { "Content-Type": "application/json" });
|
|
979
1048
|
res.end(JSON.stringify({ error: result.error }));
|
|
980
1049
|
return;
|
|
981
1050
|
}
|
|
1051
|
+
// Deactivate the Linux account if applicable
|
|
1052
|
+
if (onUserDeleted && targetLinuxUser) {
|
|
1053
|
+
onUserDeleted(targetUserId, targetLinuxUser);
|
|
1054
|
+
}
|
|
982
1055
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
983
1056
|
res.end('{"ok":true}');
|
|
984
1057
|
return;
|
|
985
1058
|
}
|
|
986
1059
|
|
|
1060
|
+
// Create user (admin only) — generates a temporary PIN that must be changed on first login
|
|
1061
|
+
if (req.method === "POST" && fullUrl === "/api/admin/users") {
|
|
1062
|
+
if (!users.isMultiUser()) {
|
|
1063
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
1064
|
+
res.end('{"error":"Not found"}');
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1067
|
+
var mu = getMultiUserFromReq(req);
|
|
1068
|
+
if (!mu || mu.role !== "admin") {
|
|
1069
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
1070
|
+
res.end('{"error":"Admin access required"}');
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
1073
|
+
var body = "";
|
|
1074
|
+
req.on("data", function (chunk) { body += chunk; });
|
|
1075
|
+
req.on("end", function () {
|
|
1076
|
+
try {
|
|
1077
|
+
var data = JSON.parse(body);
|
|
1078
|
+
if (!data.username || typeof data.username !== "string" || data.username.trim().length < 1) {
|
|
1079
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1080
|
+
res.end('{"error":"Username is required"}');
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
var result = users.createUserByAdmin({
|
|
1084
|
+
username: data.username.trim(),
|
|
1085
|
+
displayName: data.displayName ? data.displayName.trim() : data.username.trim(),
|
|
1086
|
+
email: data.email ? data.email.trim() : null,
|
|
1087
|
+
role: data.role === "admin" ? "admin" : "user",
|
|
1088
|
+
});
|
|
1089
|
+
if (result.error) {
|
|
1090
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1091
|
+
res.end(JSON.stringify({ error: result.error }));
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
// Auto-provision Linux account if OS users mode is enabled
|
|
1095
|
+
if (osUsers && !result.user.linuxUser) {
|
|
1096
|
+
var provision = provisionLinuxUser(result.user.username);
|
|
1097
|
+
if (provision.ok) {
|
|
1098
|
+
users.updateLinuxUser(result.user.id, provision.linuxUser);
|
|
1099
|
+
if (onUserProvisioned) onUserProvisioned(result.user.id, provision.linuxUser);
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1103
|
+
res.end(JSON.stringify({
|
|
1104
|
+
ok: true,
|
|
1105
|
+
user: {
|
|
1106
|
+
id: result.user.id,
|
|
1107
|
+
username: result.user.username,
|
|
1108
|
+
displayName: result.user.displayName,
|
|
1109
|
+
role: result.user.role,
|
|
1110
|
+
},
|
|
1111
|
+
tempPin: result.tempPin,
|
|
1112
|
+
}));
|
|
1113
|
+
} catch (e) {
|
|
1114
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1115
|
+
res.end('{"error":"Invalid request"}');
|
|
1116
|
+
}
|
|
1117
|
+
});
|
|
1118
|
+
return;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
// Set Linux user mapping (admin only, OS-level multi-user)
|
|
1122
|
+
if (req.method === "PUT" && fullUrl.match(/^\/api\/admin\/users\/[^/]+\/linux-user$/)) {
|
|
1123
|
+
if (!users.isMultiUser()) {
|
|
1124
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
1125
|
+
res.end('{"error":"Not found"}');
|
|
1126
|
+
return;
|
|
1127
|
+
}
|
|
1128
|
+
var mu = getMultiUserFromReq(req);
|
|
1129
|
+
if (!mu || mu.role !== "admin") {
|
|
1130
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
1131
|
+
res.end('{"error":"Admin access required"}');
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
var urlParts = fullUrl.split("/");
|
|
1135
|
+
var targetUserId = urlParts[4]; // /api/admin/users/{userId}/linux-user
|
|
1136
|
+
var body = "";
|
|
1137
|
+
req.on("data", function(chunk) { body += chunk; });
|
|
1138
|
+
req.on("end", function() {
|
|
1139
|
+
try {
|
|
1140
|
+
var parsed = JSON.parse(body);
|
|
1141
|
+
var result = users.updateLinuxUser(targetUserId, parsed.linuxUser || null);
|
|
1142
|
+
if (result.error) {
|
|
1143
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1144
|
+
res.end(JSON.stringify({ error: result.error }));
|
|
1145
|
+
} else {
|
|
1146
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1147
|
+
res.end('{"ok":true}');
|
|
1148
|
+
}
|
|
1149
|
+
} catch (e) {
|
|
1150
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1151
|
+
res.end('{"error":"Invalid request body"}');
|
|
1152
|
+
}
|
|
1153
|
+
});
|
|
1154
|
+
return;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
987
1157
|
// Create invite (admin only)
|
|
988
1158
|
if (req.method === "POST" && fullUrl === "/api/admin/invites") {
|
|
989
1159
|
if (!users.isMultiUser()) {
|
|
@@ -1275,6 +1445,53 @@ function createServer(opts) {
|
|
|
1275
1445
|
return;
|
|
1276
1446
|
}
|
|
1277
1447
|
|
|
1448
|
+
// Set project owner (admin only)
|
|
1449
|
+
if (req.method === "PUT" && /^\/api\/admin\/projects\/[a-z0-9_-]+\/owner$/.test(fullUrl)) {
|
|
1450
|
+
if (!users.isMultiUser()) {
|
|
1451
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
1452
|
+
res.end('{"error":"Not found"}');
|
|
1453
|
+
return;
|
|
1454
|
+
}
|
|
1455
|
+
var mu = getMultiUserFromReq(req);
|
|
1456
|
+
if (!mu || mu.role !== "admin") {
|
|
1457
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
1458
|
+
res.end('{"error":"Admin access required"}');
|
|
1459
|
+
return;
|
|
1460
|
+
}
|
|
1461
|
+
var projSlug = fullUrl.split("/")[4];
|
|
1462
|
+
var body = "";
|
|
1463
|
+
req.on("data", function (chunk) { body += chunk; });
|
|
1464
|
+
req.on("end", function () {
|
|
1465
|
+
try {
|
|
1466
|
+
var data = JSON.parse(body);
|
|
1467
|
+
var targetCtx = projects.get(projSlug);
|
|
1468
|
+
if (!targetCtx) {
|
|
1469
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
1470
|
+
res.end('{"error":"Project not found"}');
|
|
1471
|
+
return;
|
|
1472
|
+
}
|
|
1473
|
+
var ownerId = data.userId || null;
|
|
1474
|
+
targetCtx.setProjectOwner(ownerId);
|
|
1475
|
+
if (onProjectOwnerChanged) {
|
|
1476
|
+
onProjectOwnerChanged(projSlug, ownerId);
|
|
1477
|
+
}
|
|
1478
|
+
// Broadcast to project clients
|
|
1479
|
+
var ownerName = null;
|
|
1480
|
+
if (ownerId) {
|
|
1481
|
+
var ownerUser = users.findUserById(ownerId);
|
|
1482
|
+
ownerName = ownerUser ? (ownerUser.displayName || ownerUser.username) : ownerId;
|
|
1483
|
+
}
|
|
1484
|
+
targetCtx.send({ type: "project_owner_changed", ownerId: ownerId, ownerName: ownerName });
|
|
1485
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1486
|
+
res.end('{"ok":true}');
|
|
1487
|
+
} catch (e) {
|
|
1488
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1489
|
+
res.end('{"error":"Invalid request"}');
|
|
1490
|
+
}
|
|
1491
|
+
});
|
|
1492
|
+
return;
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1278
1495
|
// Set project allowed users (admin only)
|
|
1279
1496
|
if (req.method === "PUT" && /^\/api\/admin\/projects\/[a-z0-9_-]+\/users$/.test(fullUrl)) {
|
|
1280
1497
|
if (!users.isMultiUser()) {
|
|
@@ -1363,8 +1580,10 @@ function createServer(opts) {
|
|
|
1363
1580
|
res.end('{"error":"unauthorized"}');
|
|
1364
1581
|
return;
|
|
1365
1582
|
}
|
|
1583
|
+
var meResp = { multiUser: true, smtpEnabled: smtp.isSmtpConfigured(), emailLoginEnabled: smtp.isEmailLoginEnabled(), user: { id: mu.id, username: mu.username, email: mu.email || null, displayName: mu.displayName, role: mu.role } };
|
|
1584
|
+
if (mu.mustChangePin) meResp.mustChangePin = true;
|
|
1366
1585
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1367
|
-
res.end(JSON.stringify(
|
|
1586
|
+
res.end(JSON.stringify(meResp));
|
|
1368
1587
|
return;
|
|
1369
1588
|
}
|
|
1370
1589
|
|
|
@@ -1574,6 +1793,11 @@ function createServer(opts) {
|
|
|
1574
1793
|
var qsIdx = req.url.indexOf("?");
|
|
1575
1794
|
var projectUrlWithQS = qsIdx >= 0 ? projectUrl + req.url.substring(qsIdx) : projectUrl;
|
|
1576
1795
|
|
|
1796
|
+
// Attach user info for project HTTP handler (OS-level isolation)
|
|
1797
|
+
if (users.isMultiUser()) {
|
|
1798
|
+
req._clayUser = getMultiUserFromReq(req);
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1577
1801
|
// Try project HTTP handler first (APIs)
|
|
1578
1802
|
var origUrl = req.url;
|
|
1579
1803
|
req.url = projectUrlWithQS;
|
|
@@ -1784,16 +2008,18 @@ function createServer(opts) {
|
|
|
1784
2008
|
}
|
|
1785
2009
|
|
|
1786
2010
|
// --- Project management ---
|
|
1787
|
-
function addProject(cwd, slug, title, icon) {
|
|
2011
|
+
function addProject(cwd, slug, title, icon, projectOwnerId) {
|
|
1788
2012
|
if (projects.has(slug)) return false;
|
|
1789
2013
|
var ctx = createProjectContext({
|
|
1790
2014
|
cwd: cwd,
|
|
1791
2015
|
slug: slug,
|
|
1792
2016
|
title: title || null,
|
|
1793
2017
|
icon: icon || null,
|
|
2018
|
+
projectOwnerId: projectOwnerId || null,
|
|
1794
2019
|
pushModule: pushModule,
|
|
1795
2020
|
debug: debug,
|
|
1796
2021
|
dangerouslySkipPermissions: dangerouslySkipPermissions,
|
|
2022
|
+
osUsers: osUsers,
|
|
1797
2023
|
currentVersion: currentVersion,
|
|
1798
2024
|
lanHost: lanHost,
|
|
1799
2025
|
getProjectCount: function () { return projects.size; },
|
|
@@ -1874,10 +2100,13 @@ function createServer(opts) {
|
|
|
1874
2100
|
onPresenceChange: broadcastPresenceChange,
|
|
1875
2101
|
onProcessingChanged: broadcastProcessingChange,
|
|
1876
2102
|
onAddProject: onAddProject,
|
|
2103
|
+
onCreateProject: onCreateProject,
|
|
2104
|
+
onCloneProject: onCloneProject,
|
|
1877
2105
|
onRemoveProject: onRemoveProject,
|
|
1878
2106
|
onReorderProjects: onReorderProjects,
|
|
1879
2107
|
onSetProjectTitle: onSetProjectTitle,
|
|
1880
2108
|
onSetProjectIcon: onSetProjectIcon,
|
|
2109
|
+
onProjectOwnerChanged: onProjectOwnerChanged,
|
|
1881
2110
|
onGetServerDefaultEffort: onGetServerDefaultEffort,
|
|
1882
2111
|
onSetServerDefaultEffort: onSetServerDefaultEffort,
|
|
1883
2112
|
onGetProjectDefaultEffort: onGetProjectDefaultEffort,
|
|
@@ -1893,13 +2122,103 @@ function createServer(opts) {
|
|
|
1893
2122
|
onGetDaemonConfig: onGetDaemonConfig,
|
|
1894
2123
|
onSetPin: onSetPin,
|
|
1895
2124
|
onSetKeepAwake: onSetKeepAwake,
|
|
2125
|
+
onSetUpdateChannel: onSetUpdateChannel,
|
|
2126
|
+
updateChannel: onGetDaemonConfig ? (onGetDaemonConfig().updateChannel || "stable") : "stable",
|
|
1896
2127
|
onShutdown: onShutdown,
|
|
2128
|
+
onDmMessage: handleDmMessage,
|
|
1897
2129
|
});
|
|
1898
2130
|
projects.set(slug, ctx);
|
|
1899
2131
|
ctx.warmup();
|
|
1900
2132
|
return true;
|
|
1901
2133
|
}
|
|
1902
2134
|
|
|
2135
|
+
// --- DM message handler (server-level, cross-project) ---
|
|
2136
|
+
function handleDmMessage(ws, msg) {
|
|
2137
|
+
if (!users.isMultiUser() || !ws._clayUser) return;
|
|
2138
|
+
var userId = ws._clayUser.id;
|
|
2139
|
+
|
|
2140
|
+
if (msg.type === "dm_list") {
|
|
2141
|
+
var dmList = dm.getDmList(userId);
|
|
2142
|
+
// Enrich with user info
|
|
2143
|
+
for (var i = 0; i < dmList.length; i++) {
|
|
2144
|
+
var otherUser = users.findUserById(dmList[i].otherUserId);
|
|
2145
|
+
if (otherUser) {
|
|
2146
|
+
var p = otherUser.profile || {};
|
|
2147
|
+
dmList[i].otherUser = {
|
|
2148
|
+
id: otherUser.id,
|
|
2149
|
+
displayName: p.name || otherUser.displayName || otherUser.username,
|
|
2150
|
+
username: otherUser.username,
|
|
2151
|
+
avatarStyle: p.avatarStyle || "thumbs",
|
|
2152
|
+
avatarSeed: p.avatarSeed || otherUser.username,
|
|
2153
|
+
avatarColor: p.avatarColor || "#7c3aed",
|
|
2154
|
+
};
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
ws.send(JSON.stringify({ type: "dm_list", dms: dmList }));
|
|
2158
|
+
return;
|
|
2159
|
+
}
|
|
2160
|
+
|
|
2161
|
+
if (msg.type === "dm_open") {
|
|
2162
|
+
if (!msg.targetUserId) return;
|
|
2163
|
+
var result = dm.openDm(userId, msg.targetUserId);
|
|
2164
|
+
var targetUser = users.findUserById(msg.targetUserId);
|
|
2165
|
+
var tp = targetUser ? (targetUser.profile || {}) : {};
|
|
2166
|
+
ws.send(JSON.stringify({
|
|
2167
|
+
type: "dm_history",
|
|
2168
|
+
dmKey: result.dmKey,
|
|
2169
|
+
messages: result.messages,
|
|
2170
|
+
targetUser: targetUser ? {
|
|
2171
|
+
id: targetUser.id,
|
|
2172
|
+
displayName: tp.name || targetUser.displayName || targetUser.username,
|
|
2173
|
+
username: targetUser.username,
|
|
2174
|
+
avatarStyle: tp.avatarStyle || "thumbs",
|
|
2175
|
+
avatarSeed: tp.avatarSeed || targetUser.username,
|
|
2176
|
+
avatarColor: tp.avatarColor || "#7c3aed",
|
|
2177
|
+
} : null,
|
|
2178
|
+
}));
|
|
2179
|
+
return;
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
if (msg.type === "dm_typing") {
|
|
2183
|
+
// Relay typing indicator to DM partner
|
|
2184
|
+
var dmKey = msg.dmKey;
|
|
2185
|
+
if (!dmKey) return;
|
|
2186
|
+
var parts = dmKey.split(":");
|
|
2187
|
+
if (parts.indexOf(userId) === -1) return;
|
|
2188
|
+
var targetId = parts[0] === userId ? parts[1] : parts[0];
|
|
2189
|
+
projects.forEach(function (ctx) {
|
|
2190
|
+
ctx.forEachClient(function (otherWs) {
|
|
2191
|
+
if (otherWs === ws) return;
|
|
2192
|
+
if (!otherWs._clayUser || otherWs._clayUser.id !== targetId) return;
|
|
2193
|
+
if (otherWs.readyState !== 1) return;
|
|
2194
|
+
otherWs.send(JSON.stringify({ type: "dm_typing", dmKey: dmKey, userId: userId, typing: !!msg.typing }));
|
|
2195
|
+
});
|
|
2196
|
+
});
|
|
2197
|
+
return;
|
|
2198
|
+
}
|
|
2199
|
+
|
|
2200
|
+
if (msg.type === "dm_send") {
|
|
2201
|
+
if (!msg.dmKey || !msg.text) return;
|
|
2202
|
+
// Verify sender is a participant
|
|
2203
|
+
var parts = msg.dmKey.split(":");
|
|
2204
|
+
if (parts.indexOf(userId) === -1) return;
|
|
2205
|
+
var message = dm.sendMessage(msg.dmKey, userId, msg.text);
|
|
2206
|
+
// Send confirmation to sender
|
|
2207
|
+
ws.send(JSON.stringify({ type: "dm_message", dmKey: msg.dmKey, message: message }));
|
|
2208
|
+
// Broadcast to target user's connections across all projects
|
|
2209
|
+
var targetId = parts[0] === userId ? parts[1] : parts[0];
|
|
2210
|
+
projects.forEach(function (ctx) {
|
|
2211
|
+
ctx.forEachClient(function (otherWs) {
|
|
2212
|
+
if (otherWs === ws) return;
|
|
2213
|
+
if (!otherWs._clayUser || otherWs._clayUser.id !== targetId) return;
|
|
2214
|
+
if (otherWs.readyState !== 1) return;
|
|
2215
|
+
otherWs.send(JSON.stringify({ type: "dm_message", dmKey: msg.dmKey, message: message }));
|
|
2216
|
+
});
|
|
2217
|
+
});
|
|
2218
|
+
return;
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
|
|
1903
2222
|
function removeProject(slug) {
|
|
1904
2223
|
var ctx = projects.get(slug);
|
|
1905
2224
|
if (!ctx) return false;
|
|
@@ -1989,6 +2308,18 @@ function createServer(opts) {
|
|
|
1989
2308
|
return;
|
|
1990
2309
|
}
|
|
1991
2310
|
var serverUsers = getServerUsers();
|
|
2311
|
+
var allUsers = users.getAllUsers().map(function (u) {
|
|
2312
|
+
var p = u.profile || {};
|
|
2313
|
+
return {
|
|
2314
|
+
id: u.id,
|
|
2315
|
+
displayName: p.name || u.displayName || u.username,
|
|
2316
|
+
username: u.username,
|
|
2317
|
+
role: u.role,
|
|
2318
|
+
avatarStyle: p.avatarStyle || "thumbs",
|
|
2319
|
+
avatarSeed: p.avatarSeed || u.username,
|
|
2320
|
+
avatarColor: p.avatarColor || "#7c3aed",
|
|
2321
|
+
};
|
|
2322
|
+
});
|
|
1992
2323
|
// Build per-user filtered lists, send individually
|
|
1993
2324
|
var sentUsers = {};
|
|
1994
2325
|
projects.forEach(function (ctx) {
|
|
@@ -2014,6 +2345,7 @@ function createServer(opts) {
|
|
|
2014
2345
|
projects: filteredProjects,
|
|
2015
2346
|
projectCount: projects.size,
|
|
2016
2347
|
serverUsers: serverUsers,
|
|
2348
|
+
allUsers: allUsers,
|
|
2017
2349
|
});
|
|
2018
2350
|
sentUsers[key] = msgStr;
|
|
2019
2351
|
ws.send(msgStr);
|
package/lib/sessions.js
CHANGED
|
@@ -508,6 +508,31 @@ function createSessionManager(opts) {
|
|
|
508
508
|
return results;
|
|
509
509
|
}
|
|
510
510
|
|
|
511
|
+
function migrateSessionTitles(getSDK, migrateCwd) {
|
|
512
|
+
var toMigrate = [];
|
|
513
|
+
sessions.forEach(function(s) {
|
|
514
|
+
if (s.cliSessionId && s.title && s.title !== "New Session" && s.title !== "Resumed session") {
|
|
515
|
+
toMigrate.push({ cliSessionId: s.cliSessionId, title: s.title });
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
if (toMigrate.length === 0) return;
|
|
519
|
+
getSDK().then(function(sdkMod) {
|
|
520
|
+
var chain = Promise.resolve();
|
|
521
|
+
for (var i = 0; i < toMigrate.length; i++) {
|
|
522
|
+
(function(item) {
|
|
523
|
+
chain = chain.then(function() {
|
|
524
|
+
return sdkMod.renameSession(item.cliSessionId, item.title, { dir: migrateCwd }).catch(function(e) {
|
|
525
|
+
console.error("[session] Migration failed for " + item.cliSessionId + ":", e.message);
|
|
526
|
+
});
|
|
527
|
+
});
|
|
528
|
+
})(toMigrate[i]);
|
|
529
|
+
}
|
|
530
|
+
chain.then(function() {
|
|
531
|
+
console.log("[session] Migrated " + toMigrate.length + " session title(s) to SDK format");
|
|
532
|
+
});
|
|
533
|
+
}).catch(function() {});
|
|
534
|
+
}
|
|
535
|
+
|
|
511
536
|
return {
|
|
512
537
|
get activeSessionId() { return activeSessionId; },
|
|
513
538
|
get nextLocalId() { return nextLocalId; },
|
|
@@ -532,6 +557,7 @@ function createSessionManager(opts) {
|
|
|
532
557
|
replayHistory: replayHistory,
|
|
533
558
|
searchSessions: searchSessions,
|
|
534
559
|
setResolveLoopInfo: setResolveLoopInfo,
|
|
560
|
+
migrateSessionTitles: migrateSessionTitles,
|
|
535
561
|
setSessionVisibility: function (localId, visibility) {
|
|
536
562
|
var session = sessions.get(localId);
|
|
537
563
|
if (!session) return { error: "Session not found" };
|
package/lib/terminal-manager.js
CHANGED
|
@@ -16,10 +16,10 @@ function createTerminalManager(opts) {
|
|
|
16
16
|
var nextId = 1;
|
|
17
17
|
var terminals = new Map(); // id -> terminal session
|
|
18
18
|
|
|
19
|
-
function create(cols, rows) {
|
|
19
|
+
function create(cols, rows, osUserInfo) {
|
|
20
20
|
if (terminals.size >= MAX_TERMINALS) return null;
|
|
21
21
|
|
|
22
|
-
var pty = createTerminal(cwd, cols, rows);
|
|
22
|
+
var pty = createTerminal(cwd, cols, rows, osUserInfo);
|
|
23
23
|
if (!pty) return null;
|
|
24
24
|
|
|
25
25
|
var id = nextId++;
|
package/lib/terminal.js
CHANGED
|
@@ -5,18 +5,34 @@ try {
|
|
|
5
5
|
pty = null;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
function createTerminal(cwd, cols, rows) {
|
|
8
|
+
function createTerminal(cwd, cols, rows, osUserInfo) {
|
|
9
9
|
if (!pty) return null;
|
|
10
10
|
|
|
11
11
|
var shell = process.env.SHELL
|
|
12
12
|
|| (process.platform === "win32" ? process.env.COMSPEC || "cmd.exe" : "/bin/bash");
|
|
13
|
-
|
|
13
|
+
|
|
14
|
+
// OS-level user isolation: spawn terminal as the mapped Linux user
|
|
15
|
+
var termEnv = Object.assign({}, process.env, { TERM: "xterm-256color" });
|
|
16
|
+
var spawnOpts = {
|
|
14
17
|
name: "xterm-256color",
|
|
15
18
|
cols: cols || 80,
|
|
16
19
|
rows: rows || 24,
|
|
17
20
|
cwd: cwd,
|
|
18
|
-
env:
|
|
19
|
-
}
|
|
21
|
+
env: termEnv,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
if (osUserInfo) {
|
|
25
|
+
spawnOpts.uid = osUserInfo.uid;
|
|
26
|
+
spawnOpts.gid = osUserInfo.gid;
|
|
27
|
+
// Use the target user's shell and home
|
|
28
|
+
termEnv.HOME = osUserInfo.home;
|
|
29
|
+
termEnv.USER = osUserInfo.user;
|
|
30
|
+
termEnv.LOGNAME = osUserInfo.user;
|
|
31
|
+
// Use target user's shell if available
|
|
32
|
+
if (osUserInfo.shell) shell = osUserInfo.shell;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
var term = pty.spawn(shell, [], spawnOpts);
|
|
20
36
|
|
|
21
37
|
return term;
|
|
22
38
|
}
|
package/lib/updater.js
CHANGED
|
@@ -21,9 +21,10 @@ var sym = {
|
|
|
21
21
|
|
|
22
22
|
function log(s) { console.log(" " + s); }
|
|
23
23
|
|
|
24
|
-
function
|
|
24
|
+
function fetchVersion(channel) {
|
|
25
|
+
var tag = channel === "beta" ? "beta" : "latest";
|
|
25
26
|
return new Promise(function (resolve) {
|
|
26
|
-
var req = https.get("https://registry.npmjs.org/clay-server/
|
|
27
|
+
var req = https.get("https://registry.npmjs.org/clay-server/" + tag, function (res) {
|
|
27
28
|
var data = "";
|
|
28
29
|
res.on("data", function (chunk) { data += chunk; });
|
|
29
30
|
res.on("end", function () {
|
|
@@ -42,22 +43,48 @@ function fetchLatestVersion() {
|
|
|
42
43
|
});
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
function fetchLatestVersion() {
|
|
47
|
+
return fetchVersion("stable");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function parseVersion(v) {
|
|
51
|
+
var dashIdx = v.indexOf("-");
|
|
52
|
+
var base = dashIdx === -1 ? v : v.substring(0, dashIdx);
|
|
53
|
+
var pre = dashIdx === -1 ? null : v.substring(dashIdx + 1);
|
|
54
|
+
var parts = base.split(".").map(Number);
|
|
55
|
+
var preNum = null;
|
|
56
|
+
if (pre) {
|
|
57
|
+
var m = pre.match(/\.(\d+)$/);
|
|
58
|
+
preNum = m ? parseInt(m[1], 10) : 0;
|
|
59
|
+
}
|
|
60
|
+
return { parts: parts, pre: pre, preNum: preNum };
|
|
61
|
+
}
|
|
62
|
+
|
|
45
63
|
function isNewer(latest, current) {
|
|
46
64
|
if (!latest || !current) return false;
|
|
47
|
-
var
|
|
48
|
-
var
|
|
65
|
+
var l = parseVersion(latest);
|
|
66
|
+
var c = parseVersion(current);
|
|
67
|
+
// Compare base version (major.minor.patch)
|
|
49
68
|
for (var i = 0; i < 3; i++) {
|
|
50
|
-
var
|
|
51
|
-
var
|
|
52
|
-
if (
|
|
53
|
-
if (
|
|
69
|
+
var lv = l.parts[i] || 0;
|
|
70
|
+
var cv = c.parts[i] || 0;
|
|
71
|
+
if (lv > cv) return true;
|
|
72
|
+
if (lv < cv) return false;
|
|
73
|
+
}
|
|
74
|
+
// Bases are equal: stable (no pre-release) beats pre-release
|
|
75
|
+
if (!l.pre && c.pre) return true;
|
|
76
|
+
if (l.pre && !c.pre) return false;
|
|
77
|
+
// Both pre-release with same base: compare pre-release number
|
|
78
|
+
if (l.pre && c.pre) {
|
|
79
|
+
return l.preNum > c.preNum;
|
|
54
80
|
}
|
|
55
81
|
return false;
|
|
56
82
|
}
|
|
57
83
|
|
|
58
|
-
function performUpdate() {
|
|
84
|
+
function performUpdate(channel) {
|
|
85
|
+
var tag = channel === "beta" ? "beta" : "latest";
|
|
59
86
|
try {
|
|
60
|
-
execSync("npm install -g clay-server@
|
|
87
|
+
execSync("npm install -g clay-server@" + tag, { stdio: "pipe" });
|
|
61
88
|
return true;
|
|
62
89
|
} catch (e) {
|
|
63
90
|
return false;
|
|
@@ -94,4 +121,4 @@ async function checkAndUpdate(currentVersion, skipUpdate) {
|
|
|
94
121
|
return false;
|
|
95
122
|
}
|
|
96
123
|
|
|
97
|
-
module.exports = { checkAndUpdate, fetchLatestVersion, isNewer };
|
|
124
|
+
module.exports = { checkAndUpdate, fetchLatestVersion, fetchVersion, isNewer };
|