clay-server 4.0.0-beta.21 → 4.0.0-beta.23
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/public/app.js +4 -0
- package/lib/public/modules/app-connection.js +15 -2
- package/lib/public/modules/app-home-hub.js +5 -13
- package/lib/public/modules/app-messages.js +3 -3
- package/lib/public/modules/app-projects.js +46 -13
- package/lib/public/modules/home-surface-boot.js +7 -3
- package/lib/public/modules/project-activation.js +45 -0
- package/lib/server.js +19 -4
- package/lib/session-visibility.js +3 -1
- package/lib/users-permissions.js +4 -4
- package/package.json +1 -1
package/lib/public/app.js
CHANGED
|
@@ -277,6 +277,10 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
|
|
|
277
277
|
projectName: projectName,
|
|
278
278
|
cwd: "",
|
|
279
279
|
currentSlug: currentSlug,
|
|
280
|
+
activeProjectSlug: null,
|
|
281
|
+
sessionActivatedProjectSlug: null,
|
|
282
|
+
socketPath: null,
|
|
283
|
+
pendingHomeProjectSlug: null,
|
|
280
284
|
currentProjectOwnerId: null,
|
|
281
285
|
isOsUsers: false,
|
|
282
286
|
skipPermsEnabled: false,
|
|
@@ -221,15 +221,24 @@ export function connect() {
|
|
|
221
221
|
// two sockets can never both be pinging.
|
|
222
222
|
stopHeartbeat();
|
|
223
223
|
if (ws && ws.readyState === 1) setStatus("disconnected");
|
|
224
|
-
if (ws) {
|
|
224
|
+
if (ws) {
|
|
225
|
+
ws.onopen = null;
|
|
226
|
+
ws.onmessage = null;
|
|
227
|
+
ws.onerror = null;
|
|
228
|
+
ws.onclose = null;
|
|
229
|
+
ws.close();
|
|
230
|
+
}
|
|
225
231
|
if (connectTimeoutId) { clearTimeout(connectTimeoutId); connectTimeoutId = null; }
|
|
226
232
|
|
|
227
233
|
var protocol = location.protocol === "https:" ? "wss:" : "ws:";
|
|
228
|
-
var
|
|
234
|
+
var socketPath = store.get('wsPath');
|
|
235
|
+
store.set({ socketPath: socketPath, activeProjectSlug: null, sessionActivatedProjectSlug: null });
|
|
236
|
+
var newWs = new WebSocket(protocol + "//" + location.host + socketPath);
|
|
229
237
|
setWs(newWs);
|
|
230
238
|
|
|
231
239
|
// If not connected within 3s, force retry
|
|
232
240
|
connectTimeoutId = setTimeout(function () {
|
|
241
|
+
if (getWs() !== newWs) return;
|
|
233
242
|
if (!store.get('connected')) {
|
|
234
243
|
newWs.onclose = null;
|
|
235
244
|
newWs.onerror = null;
|
|
@@ -239,6 +248,7 @@ export function connect() {
|
|
|
239
248
|
}, 3000);
|
|
240
249
|
|
|
241
250
|
newWs.onopen = function () {
|
|
251
|
+
if (getWs() !== newWs) return;
|
|
242
252
|
if (connectTimeoutId) { clearTimeout(connectTimeoutId); connectTimeoutId = null; }
|
|
243
253
|
if (hasConnectedOnce && disconnectedAt) {
|
|
244
254
|
console.log("[clay] WebSocket reconnected after " + (Date.now() - disconnectedAt) + "ms");
|
|
@@ -267,6 +277,7 @@ export function connect() {
|
|
|
267
277
|
};
|
|
268
278
|
|
|
269
279
|
newWs.onclose = function (e) {
|
|
280
|
+
if (getWs() !== newWs) return;
|
|
270
281
|
if (connectTimeoutId) { clearTimeout(connectTimeoutId); connectTimeoutId = null; }
|
|
271
282
|
stopHeartbeat();
|
|
272
283
|
disconnectedAt = Date.now();
|
|
@@ -277,6 +288,7 @@ export function connect() {
|
|
|
277
288
|
" reason=" + (e.reason || "(none)") + " wasClean=false");
|
|
278
289
|
}
|
|
279
290
|
closeDmUserPicker();
|
|
291
|
+
store.set({ activeProjectSlug: null, sessionActivatedProjectSlug: null });
|
|
280
292
|
setStatus("disconnected");
|
|
281
293
|
setActivity(null);
|
|
282
294
|
scheduleReconnect();
|
|
@@ -285,6 +297,7 @@ export function connect() {
|
|
|
285
297
|
newWs.onerror = function () {};
|
|
286
298
|
|
|
287
299
|
newWs.onmessage = function (event) {
|
|
300
|
+
if (getWs() !== newWs) return;
|
|
288
301
|
// Backup: if we're receiving messages, we're connected
|
|
289
302
|
if (!store.get('connected')) {
|
|
290
303
|
setStatus("connected");
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { store } from './store.js';
|
|
4
4
|
import { getWs } from './ws-ref.js';
|
|
5
5
|
import { getCachedProjects, switchProject } from './app-projects.js';
|
|
6
|
+
import { chooseProjectActivationTarget } from './project-activation.js';
|
|
6
7
|
import { mateAvatarUrl } from './avatar.js';
|
|
7
8
|
import { exitDmMode } from './app-dm.js';
|
|
8
9
|
import { closeHomeDock, initHomeDock, renderDock, requestHomeDockPreference } from './home-dock.js';
|
|
@@ -45,18 +46,10 @@ function syncHomeCloseControl() {
|
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
function getHomeReturnSlug() {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
for (var pi = 0; pi < projects.length; pi++) {
|
|
53
|
-
if (projects[pi] && !projects[pi].isMate && projects[pi].slug === candidates[ci]) return candidates[ci];
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
for (var i = 0; i < projects.length; i++) {
|
|
57
|
-
if (projects[i] && !projects[i].isMate && projects[i].slug) return projects[i].slug;
|
|
58
|
-
}
|
|
59
|
-
return null;
|
|
49
|
+
return chooseProjectActivationTarget(
|
|
50
|
+
getCachedProjects(),
|
|
51
|
+
[store.get('currentSlug'), store.get('homeSurfaceProjectSlug')]
|
|
52
|
+
);
|
|
60
53
|
}
|
|
61
54
|
|
|
62
55
|
function getVisibleMates() {
|
|
@@ -407,7 +400,6 @@ export function hideHomeHub() {
|
|
|
407
400
|
export function minimizeHomeHub() {
|
|
408
401
|
var slug = getHomeReturnSlug();
|
|
409
402
|
if (!slug || !homeHubVisible) return;
|
|
410
|
-
rememberHomePrimarySurface("project");
|
|
411
403
|
switchProject(slug);
|
|
412
404
|
var projectInput = document.getElementById("input");
|
|
413
405
|
if (projectInput && !projectInput.disabled) projectInput.focus({ preventScroll: true });
|
|
@@ -63,7 +63,7 @@ import { resolvePaneSession, resolveSwitchedVendor } from './pane-session.js';
|
|
|
63
63
|
import { selectDefaultVendorForBlankSession } from './vendor-selection.js';
|
|
64
64
|
import { getModelInfoUpdate, modelEntryValue, modelEntryMatches, handleModelSelectionResult, requestVendorModels } from './model-picker.js';
|
|
65
65
|
import { getModelEffortLevels, accumulateUsage, updateUsagePanel, accumulateContext, updateContextPanel, renderCtxPopover, updateStatusPanel } from './app-panels.js';
|
|
66
|
-
import { updateProjectList, resetClientState, showUpdateAvailable, handleRemoveProjectCheckResult, handleRemoveProjectResult, handleBrowseDirResult, handleAddProjectResult, handleCloneProgress } from './app-projects.js';
|
|
66
|
+
import { updateProjectList, resetClientState, showUpdateAvailable, handleRemoveProjectCheckResult, handleRemoveProjectResult, handleBrowseDirResult, handleAddProjectResult, handleCloneProgress, finishProjectSessionActivation } from './app-projects.js';
|
|
67
67
|
import { updateHistorySentinel, prependOlderHistory } from './app-header.js';
|
|
68
68
|
import { hideHomeHub, showHomeHub } from './app-home-hub.js';
|
|
69
69
|
import { handleToolsState, handleToolInstalled, handleToolRemoved, handleToolStorageResult, handleToolLlmResult, handleToolsError, handleToolControlRequest, handleToolServerState, handleToolServerEvent, handleCapsuleGameSession } from './home-tools.js';
|
|
@@ -311,7 +311,7 @@ export function processMessage(msg) {
|
|
|
311
311
|
detachTuiView();
|
|
312
312
|
store.set({ projectName: msg.project || msg.cwd, vendorInfo: msg.vendors || {} });
|
|
313
313
|
if (msg.cwd) store.set({ cwd: msg.cwd });
|
|
314
|
-
if (msg.slug) store.set({ currentSlug: msg.slug });
|
|
314
|
+
if (msg.slug) store.set({ currentSlug: msg.slug, activeProjectSlug: msg.slug });
|
|
315
315
|
try { var _is = store.snap(); localStorage.setItem("clay-project-name-" + (_is.currentSlug || "default"), _is.projectName); } catch (e) {}
|
|
316
316
|
// In mate DM, keep title as mate name and re-apply mate color
|
|
317
317
|
if (store.get('dmMode') && store.get('dmTargetUser') && store.get('dmTargetUser').isMate) {
|
|
@@ -762,7 +762,6 @@ export function processMessage(msg) {
|
|
|
762
762
|
break;
|
|
763
763
|
|
|
764
764
|
case "session_switched":
|
|
765
|
-
hideHomeHub();
|
|
766
765
|
closeWhatsNewArticle();
|
|
767
766
|
// Save draft from outgoing session
|
|
768
767
|
var _prevSid = store.get('activeSessionId');
|
|
@@ -860,6 +859,7 @@ export function processMessage(msg) {
|
|
|
860
859
|
// Reload survival: a restored active session that is a split-group
|
|
861
860
|
// member reopens its group (no-op when a split is already open).
|
|
862
861
|
maybeRestoreSplitGroup();
|
|
862
|
+
if (finishProjectSessionActivation()) hideHomeHub();
|
|
863
863
|
break;
|
|
864
864
|
|
|
865
865
|
case "session_full_access_changed":
|
|
@@ -36,6 +36,8 @@ import { resetDebateState } from './debate.js';
|
|
|
36
36
|
import { removeDebateBottomBar } from './app-debate-ui.js';
|
|
37
37
|
import { closeProjectSettings } from './project-settings.js';
|
|
38
38
|
import { chooseProjectAfterRemoval } from './project-removal-target.js';
|
|
39
|
+
import { isProjectActivated, isProjectActivationPending, isProjectContextConnected, projectWsPath } from './project-activation.js';
|
|
40
|
+
import { rememberHomePrimarySurface } from './home-surface.js';
|
|
39
41
|
|
|
40
42
|
// --- Module-owned state ---
|
|
41
43
|
var cachedProjects = [];
|
|
@@ -417,30 +419,32 @@ export function switchProject(slug) {
|
|
|
417
419
|
if (!slug) return;
|
|
418
420
|
document.cookie = "clay_last_project=" + encodeURIComponent(slug) + "; Path=/; SameSite=Strict; Max-Age=31536000";
|
|
419
421
|
var st = store.snap();
|
|
420
|
-
var
|
|
421
|
-
var
|
|
422
|
+
var ws = getWs();
|
|
423
|
+
var targetWsPath = projectWsPath(slug);
|
|
424
|
+
var homeVisible = isHomeHubVisible();
|
|
425
|
+
var alreadyInProject = isProjectActivated(st, slug, ws);
|
|
422
426
|
var wasDm = st.dmMode;
|
|
423
427
|
var wasMate = st.dmMode && st.dmTargetUser && st.dmTargetUser.isMate;
|
|
424
428
|
if (st.dmMode) exitDmMode(wasMate);
|
|
425
429
|
closeWhatsNewArticle();
|
|
426
|
-
if (
|
|
430
|
+
if (homeVisible && alreadyInProject) {
|
|
431
|
+
rememberHomePrimarySurface("project", slug);
|
|
427
432
|
hideHomeHub();
|
|
428
|
-
if (
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
history.pushState(null, "", "/p/" + slug + "/");
|
|
433
|
-
}
|
|
434
|
-
return;
|
|
433
|
+
if (document.documentElement.classList.contains("pwa-standalone")) {
|
|
434
|
+
history.replaceState(null, "", "/p/" + slug + "/");
|
|
435
|
+
} else {
|
|
436
|
+
history.pushState(null, "", "/p/" + slug + "/");
|
|
435
437
|
}
|
|
438
|
+
return;
|
|
436
439
|
}
|
|
437
440
|
if (alreadyInProject) {
|
|
438
|
-
var ws = getWs();
|
|
439
441
|
if (wasDm && ws && ws.readyState === 1) {
|
|
440
442
|
ws.send(JSON.stringify({ type: "switch_session", id: store.get('activeSessionId') }));
|
|
441
443
|
}
|
|
442
444
|
return;
|
|
443
445
|
}
|
|
446
|
+
if (homeVisible) store.set({ pendingHomeProjectSlug: slug });
|
|
447
|
+
if (isProjectActivationPending(store.snap(), slug, ws)) return;
|
|
444
448
|
resetFileBrowser();
|
|
445
449
|
closeNotesBrowser();
|
|
446
450
|
hideMemory();
|
|
@@ -449,13 +453,42 @@ export function switchProject(slug) {
|
|
|
449
453
|
store.set({ currentSlug: slug });
|
|
450
454
|
store.set({ basePath: "/p/" + slug + "/" });
|
|
451
455
|
store.set({ wsPath: targetWsPath });
|
|
456
|
+
if (!homeVisible) {
|
|
457
|
+
if (document.documentElement.classList.contains("pwa-standalone")) {
|
|
458
|
+
history.replaceState(null, "", "/p/" + slug + "/");
|
|
459
|
+
} else {
|
|
460
|
+
history.pushState(null, "", "/p/" + slug + "/");
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
resetClientState();
|
|
464
|
+
connect();
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
export function completePendingProjectActivation() {
|
|
468
|
+
var slug = store.get('pendingHomeProjectSlug');
|
|
469
|
+
if (!slug || !isProjectActivated(store.snap(), slug, getWs())) return false;
|
|
470
|
+
store.set({ pendingHomeProjectSlug: null });
|
|
471
|
+
rememberHomePrimarySurface("project", slug);
|
|
452
472
|
if (document.documentElement.classList.contains("pwa-standalone")) {
|
|
453
473
|
history.replaceState(null, "", "/p/" + slug + "/");
|
|
454
474
|
} else {
|
|
455
475
|
history.pushState(null, "", "/p/" + slug + "/");
|
|
456
476
|
}
|
|
457
|
-
|
|
458
|
-
|
|
477
|
+
return true;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
export function confirmCurrentProjectSessionActivation() {
|
|
481
|
+
var slug = store.get('activeProjectSlug');
|
|
482
|
+
if (!slug || !isProjectContextConnected(store.snap(), slug, getWs())) return false;
|
|
483
|
+
store.set({ sessionActivatedProjectSlug: slug });
|
|
484
|
+
return true;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
export function finishProjectSessionActivation() {
|
|
488
|
+
var confirmed = confirmCurrentProjectSessionActivation();
|
|
489
|
+
if (!confirmed) return false;
|
|
490
|
+
if (!store.get('pendingHomeProjectSlug')) return true;
|
|
491
|
+
return completePendingProjectActivation();
|
|
459
492
|
}
|
|
460
493
|
|
|
461
494
|
export function showUpdateAvailable(msg) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// One-shot
|
|
1
|
+
// One-shot routing for a server-selected project or the no-project Home fallback.
|
|
2
2
|
import { store } from './store.js';
|
|
3
3
|
import { rememberHomePrimarySurface } from './home-surface.js';
|
|
4
4
|
|
|
@@ -10,9 +10,13 @@ function explicitProjectPath(pathname) {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
export function resolveHomeBootDestination(options) {
|
|
13
|
-
if (!options
|
|
13
|
+
if (!options) return "wait";
|
|
14
14
|
if (options.paneMode || explicitProjectPath(options.pathname)) return "project";
|
|
15
|
-
|
|
15
|
+
// The server-selected project embedded in the root shell is authoritative.
|
|
16
|
+
// Home is entered only through an explicit in-app action; reloading `/`
|
|
17
|
+
// returns to the project workspace even if Home was the last open surface.
|
|
18
|
+
if (options.currentSlug) return "project";
|
|
19
|
+
if (options.surfaceLoaded !== true) return "wait";
|
|
16
20
|
if (options.dockLoaded !== true) return "wait";
|
|
17
21
|
return "home";
|
|
18
22
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Pure project activation and Home-return selection helpers.
|
|
2
|
+
|
|
3
|
+
export function projectWsPath(slug) {
|
|
4
|
+
return slug ? "/p/" + slug + "/ws" : null;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function chooseProjectActivationTarget(projects, candidates) {
|
|
8
|
+
var available = Array.isArray(projects) ? projects : [];
|
|
9
|
+
var preferred = Array.isArray(candidates) ? candidates : [];
|
|
10
|
+
for (var ci = 0; ci < preferred.length; ci++) {
|
|
11
|
+
if (!preferred[ci]) continue;
|
|
12
|
+
for (var pi = 0; pi < available.length; pi++) {
|
|
13
|
+
if (available[pi] && !available[pi].isMate && available[pi].slug === preferred[ci]) return preferred[ci];
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
for (var i = 0; i < available.length; i++) {
|
|
17
|
+
if (available[i] && !available[i].isMate && available[i].slug) return available[i].slug;
|
|
18
|
+
}
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function isProjectActivationPending(state, slug, socket) {
|
|
23
|
+
var target = projectWsPath(slug);
|
|
24
|
+
return !!target
|
|
25
|
+
&& state.currentSlug === slug
|
|
26
|
+
&& state.wsPath === target
|
|
27
|
+
&& state.socketPath === target
|
|
28
|
+
&& !!socket
|
|
29
|
+
&& (socket.readyState === 0 || socket.readyState === 1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function isProjectActivated(state, slug, socket) {
|
|
33
|
+
return isProjectActivationPending(state, slug, socket)
|
|
34
|
+
&& socket.readyState === 1
|
|
35
|
+
&& state.connected === true
|
|
36
|
+
&& state.activeProjectSlug === slug
|
|
37
|
+
&& state.sessionActivatedProjectSlug === slug;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function isProjectContextConnected(state, slug, socket) {
|
|
41
|
+
return isProjectActivationPending(state, slug, socket)
|
|
42
|
+
&& socket.readyState === 1
|
|
43
|
+
&& state.connected === true
|
|
44
|
+
&& state.activeProjectSlug === slug;
|
|
45
|
+
}
|
package/lib/server.js
CHANGED
|
@@ -124,6 +124,14 @@ function stripPrefix(urlPath, slug) {
|
|
|
124
124
|
return rest || "/";
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
function injectRootProjectSlug(html, slug) {
|
|
128
|
+
var bodyPattern = /<body\b([^>]*)>/i;
|
|
129
|
+
if (!bodyPattern.test(html)) throw new Error("App shell body is missing");
|
|
130
|
+
return html.replace(bodyPattern, function (_match, attributes) {
|
|
131
|
+
return '<body' + attributes + ' data-home-project-slug="' + slug + '">';
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
127
135
|
/**
|
|
128
136
|
* Create a multi-project server.
|
|
129
137
|
* opts: { tlsOptions, caPath, pinHash, port, debug, dangerouslySkipPermissions }
|
|
@@ -741,7 +749,8 @@ function createServer(opts) {
|
|
|
741
749
|
// --- Skills routes (delegated to server-skills) ---
|
|
742
750
|
if (skills.handleRequest(req, res, fullUrl)) return;
|
|
743
751
|
|
|
744
|
-
// Root path — render
|
|
752
|
+
// Root path — render the default project workspace while keeping Home
|
|
753
|
+
// available as an explicit client-side surface.
|
|
745
754
|
if (fullUrl === "/" && req.method === "GET") {
|
|
746
755
|
if (!isRequestAuthed(req)) {
|
|
747
756
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
@@ -751,11 +760,16 @@ function createServer(opts) {
|
|
|
751
760
|
if (projects.size > 0) {
|
|
752
761
|
var targetSlug = null;
|
|
753
762
|
var reqUser = users.isMultiUser() ? getMultiUserFromReq(req) : null;
|
|
763
|
+
function isOrdinaryProject(slug) {
|
|
764
|
+
var candidate = slug ? projects.get(slug) : null;
|
|
765
|
+
var status = candidate && candidate.getStatus ? candidate.getStatus() : null;
|
|
766
|
+
return !!candidate && (!status || status.isMate !== true);
|
|
767
|
+
}
|
|
754
768
|
var homePreference = typeof users.getHomeSurfacePreference === "function"
|
|
755
769
|
? users.getHomeSurfacePreference(reqUser ? reqUser.id : "default")
|
|
756
770
|
: null;
|
|
757
771
|
var preferredContextSlug = homePreference && homePreference.projectSlug;
|
|
758
|
-
if (
|
|
772
|
+
if (isOrdinaryProject(preferredContextSlug)) {
|
|
759
773
|
if (reqUser && onGetProjectAccess) {
|
|
760
774
|
var preferredAccess = onGetProjectAccess(preferredContextSlug);
|
|
761
775
|
if (preferredAccess && !preferredAccess.error && users.canAccessProject(reqUser.id, preferredAccess)) {
|
|
@@ -767,7 +781,7 @@ function createServer(opts) {
|
|
|
767
781
|
}
|
|
768
782
|
// Check for last-visited project cookie
|
|
769
783
|
var lastProject = parseCookies(req)["clay_last_project"];
|
|
770
|
-
if (!targetSlug &&
|
|
784
|
+
if (!targetSlug && isOrdinaryProject(lastProject)) {
|
|
771
785
|
if (reqUser && onGetProjectAccess) {
|
|
772
786
|
var lpAccess = onGetProjectAccess(lastProject);
|
|
773
787
|
if (lpAccess && !lpAccess.error && users.canAccessProject(reqUser.id, lpAccess)) {
|
|
@@ -781,6 +795,7 @@ function createServer(opts) {
|
|
|
781
795
|
if (!targetSlug) {
|
|
782
796
|
projects.forEach(function (ctx, s) {
|
|
783
797
|
if (targetSlug) return;
|
|
798
|
+
if (!isOrdinaryProject(s)) return;
|
|
784
799
|
if (reqUser && onGetProjectAccess) {
|
|
785
800
|
var access = onGetProjectAccess(s);
|
|
786
801
|
if (access && !access.error && users.canAccessProject(reqUser.id, access)) {
|
|
@@ -794,7 +809,7 @@ function createServer(opts) {
|
|
|
794
809
|
if (targetSlug) {
|
|
795
810
|
try {
|
|
796
811
|
var homeHtml = fs.readFileSync(path.join(publicDir, "index.html"), "utf8");
|
|
797
|
-
homeHtml = homeHtml
|
|
812
|
+
homeHtml = injectRootProjectSlug(homeHtml, targetSlug);
|
|
798
813
|
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-cache" });
|
|
799
814
|
res.end(homeHtml);
|
|
800
815
|
} catch (e) {
|
|
@@ -25,9 +25,11 @@ function handleChange(ctx, ws, msg) {
|
|
|
25
25
|
|
|
26
26
|
function revokeViewers(ctx, session) {
|
|
27
27
|
if (ctx.usersModule.isMultiUser() && session.sessionVisibility === "private") {
|
|
28
|
+
var project = ctx.getProjectAccess ? ctx.getProjectAccess() : null;
|
|
28
29
|
ctx.clients.forEach(function(client) {
|
|
29
30
|
if (client._clayActiveSession !== session.localId) return;
|
|
30
|
-
|
|
31
|
+
var userId = client._clayUser && client._clayUser.id;
|
|
32
|
+
if (userId && ctx.usersModule.canAccessSession(userId, session, project)) return;
|
|
31
33
|
// Clear the server-side selection before reconnecting so no further
|
|
32
34
|
// session events or input can cross the revoked access boundary.
|
|
33
35
|
client._clayActiveSession = null;
|
package/lib/users-permissions.js
CHANGED
|
@@ -89,11 +89,11 @@ function attachPermissions(deps) {
|
|
|
89
89
|
function canAccessSession(userId, session, project) {
|
|
90
90
|
// Must have project access first
|
|
91
91
|
if (!canAccessProject(userId, project)) return false;
|
|
92
|
+
// Resolve the stored user record so client-supplied roles cannot elevate access.
|
|
93
|
+
var user = findUserById(userId);
|
|
94
|
+
if (user && user.role === "admin") return true;
|
|
92
95
|
// Sessions without ownerId are legacy -- only admin can see them
|
|
93
|
-
if (!session.ownerId)
|
|
94
|
-
var user = findUserById(userId);
|
|
95
|
-
return !!(user && user.role === "admin");
|
|
96
|
-
}
|
|
96
|
+
if (!session.ownerId) return false;
|
|
97
97
|
// Owner can always see their own sessions
|
|
98
98
|
if (session.ownerId === userId) return true;
|
|
99
99
|
// Sharing is opt-in; missing or invalid visibility stays private.
|
package/package.json
CHANGED