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/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({ ok: true, user: { id: user.id, username: user.username, role: user.role } }));
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({ 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 } }));
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" };
@@ -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
- var term = pty.spawn(shell, [], {
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: Object.assign({}, process.env, { TERM: "xterm-256color" }),
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 fetchLatestVersion() {
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/latest", function (res) {
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 lp = latest.split(".").map(Number);
48
- var cp = current.split(".").map(Number);
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 l = lp[i] || 0;
51
- var c = cp[i] || 0;
52
- if (l > c) return true;
53
- if (l < c) return false;
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@latest", { stdio: "pipe" });
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 };