clay-server 4.0.0 → 4.1.0-beta.1
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/daemon-project-access.js +118 -0
- package/lib/daemon-projects.js +19 -5
- package/lib/daemon.js +19 -18
- package/lib/project-logs-access-scope.js +94 -0
- package/lib/project-logs-query.js +3 -0
- package/lib/project-logs-service.js +86 -84
- package/lib/project-logs-store.js +5 -3
- package/lib/project-pair-lifecycle.js +71 -61
- package/lib/project-pair-message.js +50 -0
- package/lib/project-pair-replacement-state.js +72 -0
- package/lib/project-pair-task-control.js +264 -0
- package/lib/project-pair-usage.js +73 -0
- package/lib/project-session-pair.js +115 -116
- package/lib/project-sessions.js +40 -6
- package/lib/project-worker-proposal.js +54 -57
- package/lib/project.js +27 -4
- package/lib/public/app.js +1 -0
- package/lib/public/css/command-palette.css +26 -0
- package/lib/public/css/icon-strip.css +40 -0
- package/lib/public/index.html +2 -2
- package/lib/public/modules/app-message-router.js +1 -1
- package/lib/public/modules/app-messages.js +9 -0
- package/lib/public/modules/project-access.js +141 -0
- package/lib/public/modules/project-grouping.js +22 -0
- package/lib/public/modules/search-clay-chat.js +47 -6
- package/lib/public/modules/search-clay-controls.js +86 -0
- package/lib/public/modules/sidebar-projects.js +14 -171
- package/lib/public/modules/worker-proposal.js +4 -0
- package/lib/public/style.css +1 -1
- package/lib/sdk-bridge.js +21 -1
- package/lib/server-admin.js +3 -3
- package/lib/server-global-ws.js +10 -0
- package/lib/server-home-chat-events.js +3 -0
- package/lib/server-home-chat.js +35 -0
- package/lib/server-mates.js +4 -3
- package/lib/server-palette.js +17 -14
- package/lib/server.js +184 -102
- package/lib/session-pair-factory.js +3 -0
- package/lib/session-pair-history.js +40 -0
- package/lib/session-pair-mcp-server.js +12 -1
- package/lib/session-pair-operation.js +11 -0
- package/lib/session-pair-prompts.js +24 -2
- package/lib/session-pair-turn-control.js +8 -3
- package/lib/worker-proposal-control.js +69 -0
- package/lib/worker-proposal-decision.js +7 -0
- package/lib/worker-runtime-catalog.js +52 -0
- package/lib/workspace-query-access.js +3 -2
- package/lib/workspace-query-service.js +1 -1
- package/lib/worktree.js +8 -4
- package/lib/ws-schema.js +7 -0
- package/lib/yoke/adapters/codex.js +6 -2
- package/package.json +1 -1
package/lib/sdk-bridge.js
CHANGED
|
@@ -627,8 +627,18 @@ function createSDKBridge(opts) {
|
|
|
627
627
|
var PAIR_MCP_PREFIX = "mcp__clay-sessions__";
|
|
628
628
|
var pairMcpTools = {
|
|
629
629
|
propose_worker: true,
|
|
630
|
+
inspect_worker_proposal: true,
|
|
631
|
+
cancel_worker_proposal: true,
|
|
632
|
+
worker_runtime_catalog: true,
|
|
630
633
|
send_to_partner: true,
|
|
631
634
|
read_partner: true,
|
|
635
|
+
message_partner: true,
|
|
636
|
+
queue_partner_followup: true,
|
|
637
|
+
inspect_partner_followups: true,
|
|
638
|
+
cancel_partner_followup: true,
|
|
639
|
+
replace_partner_task: true,
|
|
640
|
+
resume_partner_task: true,
|
|
641
|
+
report_partner_outcome: true,
|
|
632
642
|
partner_status: true,
|
|
633
643
|
replace_partner: true,
|
|
634
644
|
interrupt_partner: true,
|
|
@@ -638,6 +648,16 @@ function createSDKBridge(opts) {
|
|
|
638
648
|
};
|
|
639
649
|
var pairDynamicTools = {
|
|
640
650
|
propose_worker: true,
|
|
651
|
+
inspect_worker_proposal: true,
|
|
652
|
+
cancel_worker_proposal: true,
|
|
653
|
+
worker_runtime_catalog: true,
|
|
654
|
+
message_partner: true,
|
|
655
|
+
queue_partner_followup: true,
|
|
656
|
+
inspect_partner_followups: true,
|
|
657
|
+
cancel_partner_followup: true,
|
|
658
|
+
replace_partner_task: true,
|
|
659
|
+
resume_partner_task: true,
|
|
660
|
+
report_partner_outcome: true,
|
|
641
661
|
partner_status: true,
|
|
642
662
|
replace_partner: true,
|
|
643
663
|
interrupt_partner: true,
|
|
@@ -866,7 +886,7 @@ function createSDKBridge(opts) {
|
|
|
866
886
|
}
|
|
867
887
|
|
|
868
888
|
var _nm = getNotificationsModule();
|
|
869
|
-
if (_nm) {
|
|
889
|
+
if (_nm && session.homeClayEntryMode !== "search") {
|
|
870
890
|
_nm.notify("permission_request", {
|
|
871
891
|
title: permissionPushTitle(toolName, input),
|
|
872
892
|
body: permissionPushBody(toolName, input),
|
package/lib/server-admin.js
CHANGED
|
@@ -632,9 +632,9 @@ function attachAdmin(ctx) {
|
|
|
632
632
|
return true;
|
|
633
633
|
}
|
|
634
634
|
var projSlug = fullUrl.split("/")[4];
|
|
635
|
-
if (
|
|
636
|
-
res.writeHead(
|
|
637
|
-
res.end('{"error":"
|
|
635
|
+
if (!projects.get(projSlug)) {
|
|
636
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
637
|
+
res.end('{"error":"Project not found"}');
|
|
638
638
|
return true;
|
|
639
639
|
}
|
|
640
640
|
var body = "";
|
package/lib/server-global-ws.js
CHANGED
|
@@ -40,6 +40,8 @@ function attachGlobalWs(opts) {
|
|
|
40
40
|
var onCloneProject = opts.onCloneProject;
|
|
41
41
|
var onHomePreferenceMessage = opts.onHomePreferenceMessage;
|
|
42
42
|
var onAppMessage = opts.onAppMessage;
|
|
43
|
+
var onConnection = opts.onConnection;
|
|
44
|
+
var clients = new Set();
|
|
43
45
|
|
|
44
46
|
function handleMessage(ws, msg) {
|
|
45
47
|
if (!msg || typeof msg !== "object") return;
|
|
@@ -184,16 +186,24 @@ function attachGlobalWs(opts) {
|
|
|
184
186
|
|
|
185
187
|
function handleConnection(ws, wsUser) {
|
|
186
188
|
ws._clayUser = wsUser || null;
|
|
189
|
+
clients.add(ws);
|
|
190
|
+
ws.on("close", function () { clients.delete(ws); });
|
|
187
191
|
ws.on("message", function (data) {
|
|
188
192
|
var msg;
|
|
189
193
|
try { msg = JSON.parse(data); } catch (e) { return; }
|
|
190
194
|
handleMessage(ws, msg);
|
|
191
195
|
});
|
|
196
|
+
if (typeof onConnection === "function") onConnection(ws);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function forEachClient(visitor) {
|
|
200
|
+
clients.forEach(function (ws) { visitor(ws); });
|
|
192
201
|
}
|
|
193
202
|
|
|
194
203
|
return {
|
|
195
204
|
handleConnection: handleConnection,
|
|
196
205
|
handleMessage: handleMessage,
|
|
206
|
+
forEachClient: forEachClient,
|
|
197
207
|
};
|
|
198
208
|
}
|
|
199
209
|
|
|
@@ -267,6 +267,9 @@ function transformEvent(event, mateId, session, requestId, stableSessionId) {
|
|
|
267
267
|
}
|
|
268
268
|
if (event.type === "tool_executing") return Object.assign({ type: "home_clay_activity", activityId: session._homeClayToolActivityId || "tool:" + (session._homeClayActivityStep || 1), phase: "searching", status: "active", label: searchToolLabel(event.name, event.input), step: session._homeClayActivityStep || 1 }, metadata);
|
|
269
269
|
if (event.type === "tool_result") return Object.assign({ type: "home_clay_activity", activityId: session._homeClayToolActivityId || "tool:" + (session._homeClayActivityStep || 1), phase: event.is_error ? "reconsidering" : "reviewing", status: event.is_error ? "error" : "done", label: event.is_error ? "That lead did not work; trying another route" : "Reviewed the search results", step: session._homeClayActivityStep || 1 }, metadata);
|
|
270
|
+
if (event.type === "permission_request") return Object.assign({ type: "home_mate_permission_request", permissionRequestId: event.requestId, toolName: event.toolName, toolInput: event.toolInput, decisionReason: event.decisionReason || "" }, metadata);
|
|
271
|
+
if (event.type === "permission_resolved") return Object.assign({ type: "home_mate_permission_resolved", permissionRequestId: event.requestId, decision: event.decision || "deny" }, metadata);
|
|
272
|
+
if (event.type === "permission_cancel") return Object.assign({ type: "home_mate_permission_cancelled", permissionRequestId: event.requestId }, metadata);
|
|
270
273
|
}
|
|
271
274
|
if (session && session.homeDebatePlanning === true && HOME_DEBATE_EVENT_TYPES[event.type]) {
|
|
272
275
|
var live = { type: "home_debate_event", eventType: event.type };
|
package/lib/server-home-chat.js
CHANGED
|
@@ -224,6 +224,12 @@ function attachHomeChat(deps) {
|
|
|
224
224
|
messages: historyToHomeChat(session.history || [], session.debateSetupMode === true, session.mateCreationMode === true),
|
|
225
225
|
isProcessing: session.isProcessing === true,
|
|
226
226
|
};
|
|
227
|
+
if (session.homeClayEntryMode === "search") {
|
|
228
|
+
payload.pendingPermissions = Object.keys(session.pendingPermissions || {}).map(function (permissionRequestId) {
|
|
229
|
+
var pending = session.pendingPermissions[permissionRequestId];
|
|
230
|
+
return { permissionRequestId: permissionRequestId, toolName: pending.toolName, toolInput: pending.toolInput, decisionReason: pending.decisionReason || "" };
|
|
231
|
+
});
|
|
232
|
+
}
|
|
227
233
|
if (session.debateSetupMode === true) payload.debatePlanning = true;
|
|
228
234
|
if (session.homeDebatePhase) payload.debatePhase = session.homeDebatePhase;
|
|
229
235
|
if (session.mateCreationMode === true) payload.mateCreation = true;
|
|
@@ -388,6 +394,35 @@ function attachHomeChat(deps) {
|
|
|
388
394
|
|
|
389
395
|
function handleMessage(ws, msg) {
|
|
390
396
|
if (!msg || typeof msg.type !== "string") return false;
|
|
397
|
+
if (msg.type === "home_mate_stop" || msg.type === "home_mate_permission_response") {
|
|
398
|
+
var controlUserId = ws._clayUser ? ws._clayUser.id : null;
|
|
399
|
+
if (users.isMultiUser() && !controlUserId) {
|
|
400
|
+
sendError(ws, msg.mateId || null, "Not authenticated.", msg.requestId || null, msg.sessionId || null);
|
|
401
|
+
return true;
|
|
402
|
+
}
|
|
403
|
+
var controlFound = findMateProject(controlUserId, msg.mateId, true);
|
|
404
|
+
var controlTap = messageTap(ws, msg);
|
|
405
|
+
var controlManager = controlFound ? controlFound.ctx.getSessionManager() : null;
|
|
406
|
+
var controlSession = controlManager && controlTap && controlTap.mateId === controlFound.mate.id && controlTap.requestId === msg.requestId
|
|
407
|
+
? controlManager.sessions.get(controlTap.sessionId) : null;
|
|
408
|
+
var controlReference = controlSession ? sessionReference(controlSession) : null;
|
|
409
|
+
if (!controlFound || !controlSession || controlReference !== msg.sessionId || !ownsSession(controlSession, controlUserId) || controlSession.homeClayEntryMode !== "search") {
|
|
410
|
+
sendError(ws, msg.mateId || null, "Conversation control is no longer available.", msg.requestId || null, msg.sessionId || null, "session_not_found");
|
|
411
|
+
return true;
|
|
412
|
+
}
|
|
413
|
+
if (msg.type === "home_mate_permission_response") {
|
|
414
|
+
var controlDecision = msg.decision === "allow" ? "allow" : (msg.decision === "deny" ? "deny" : null);
|
|
415
|
+
if (!controlDecision || typeof msg.permissionRequestId !== "string" || !controlSession.pendingPermissions[msg.permissionRequestId]) {
|
|
416
|
+
sendError(ws, controlFound.mate.id, "This permission request is no longer pending.", msg.requestId || null, msg.sessionId || null, "permission_not_found", { toolId: msg.permissionRequestId });
|
|
417
|
+
return true;
|
|
418
|
+
}
|
|
419
|
+
controlFound.ctx.handleHomePermissionResponse(ws, { requestId: msg.permissionRequestId, decision: controlDecision }, controlSession);
|
|
420
|
+
} else {
|
|
421
|
+
sendMessage(ws, { type: "home_mate_stopping", mateId: controlFound.mate.id, sessionId: controlReference, requestId: msg.requestId || null });
|
|
422
|
+
controlFound.ctx.stopHomeSession(controlSession);
|
|
423
|
+
}
|
|
424
|
+
return true;
|
|
425
|
+
}
|
|
391
426
|
if (msg.type !== "home_debates_list" && msg.type !== "home_clay_ask" && msg.type !== "home_clay_session_resolve" && msg.type !== "home_clay_log_resolve" && msg.type !== "home_mate_present" && msg.type !== "home_mate_open" && msg.type !== "home_mate_sessions_list" && msg.type !== "home_mate_session_open" && msg.type !== "home_mate_send" && msg.type !== "home_mate_new_session" && msg.type !== "home_mate_debate_plan" && msg.type !== "home_debate_proposal_response" && msg.type !== "home_debate_question_response" && msg.type !== "home_debate_control" && msg.type !== "home_mate_creation_plan" && msg.type !== "home_mate_creation_question_response" && msg.type !== "home_mate_creation_proposal_response" && msg.type !== "home_mate_close" && msg.type !== "home_mate_memory_list" && msg.type !== "home_mate_knowledge_list" && msg.type !== "home_mate_models_get" && msg.type !== "home_mate_model_set") {
|
|
392
427
|
return false;
|
|
393
428
|
}
|
package/lib/server-mates.js
CHANGED
|
@@ -20,11 +20,12 @@ function attachMates(ctx) {
|
|
|
20
20
|
var projList = [];
|
|
21
21
|
projects.forEach(function (pCtx, pSlug) {
|
|
22
22
|
var st = pCtx.getStatus();
|
|
23
|
-
if (st.isMate
|
|
23
|
+
if (st.isMate) return;
|
|
24
24
|
// Filter by user access in multi-user mode
|
|
25
|
-
if (
|
|
25
|
+
if (users.isMultiUser() && onGetProjectAccess) {
|
|
26
|
+
if (!userId) return;
|
|
26
27
|
var access = onGetProjectAccess(pSlug);
|
|
27
|
-
if (access
|
|
28
|
+
if (!access || access.error || !users.canAccessProject(userId, access)) return;
|
|
28
29
|
}
|
|
29
30
|
projList.push(st);
|
|
30
31
|
});
|
package/lib/server-palette.js
CHANGED
|
@@ -5,6 +5,17 @@ function attachPalette(ctx) {
|
|
|
5
5
|
var projects = ctx.projects;
|
|
6
6
|
var getMultiUserFromReq = ctx.getMultiUserFromReq;
|
|
7
7
|
var onGetProjectAccess = ctx.onGetProjectAccess;
|
|
8
|
+
var canUserAccessSlug = ctx.canUserAccessSlug;
|
|
9
|
+
|
|
10
|
+
function accessFor(user, slug, status) {
|
|
11
|
+
if (!user) return { visibility: "public" };
|
|
12
|
+
if (typeof canUserAccessSlug !== "function" || !canUserAccessSlug(user.id, slug)) return null;
|
|
13
|
+
if (status && status.isMate) {
|
|
14
|
+
return { visibility: "private", ownerId: status.projectOwnerId || null, allowedUsers: [] };
|
|
15
|
+
}
|
|
16
|
+
var access = onGetProjectAccess ? onGetProjectAccess(slug) : null;
|
|
17
|
+
return access && !access.error ? access : null;
|
|
18
|
+
}
|
|
8
19
|
|
|
9
20
|
function handleRequest(req, res, fullUrl) {
|
|
10
21
|
if (req.method !== "GET" || fullUrl !== "/api/palette/search") return false;
|
|
@@ -26,17 +37,13 @@ function attachPalette(ctx) {
|
|
|
26
37
|
// Recent mode: return all sessions sorted by lastActivity
|
|
27
38
|
projects.forEach(function (pCtx, pSlug) {
|
|
28
39
|
var status = pCtx.getStatus();
|
|
29
|
-
|
|
30
|
-
if (paletteUser &&
|
|
31
|
-
var pAccess = onGetProjectAccess(pSlug);
|
|
32
|
-
if (pAccess && !pAccess.error && !users.canAccessProject(paletteUser.id, pAccess)) return;
|
|
33
|
-
}
|
|
40
|
+
var pAccess = accessFor(paletteUser, pSlug, status);
|
|
41
|
+
if (paletteUser && !pAccess) return;
|
|
34
42
|
pCtx.sm.sessions.forEach(function (session) {
|
|
35
43
|
if (session.hidden) return;
|
|
36
44
|
if (paletteUser) {
|
|
37
45
|
if (users.isMultiUser()) {
|
|
38
|
-
|
|
39
|
-
if (!users.canAccessSession(paletteUser.id, session, sAccess)) return;
|
|
46
|
+
if (!users.canAccessSession(paletteUser.id, session, pAccess)) return;
|
|
40
47
|
}
|
|
41
48
|
} else {
|
|
42
49
|
if (session.ownerId) return;
|
|
@@ -65,18 +72,14 @@ function attachPalette(ctx) {
|
|
|
65
72
|
var projectSessions = [];
|
|
66
73
|
projects.forEach(function (pCtx, pSlug) {
|
|
67
74
|
var status = pCtx.getStatus();
|
|
68
|
-
|
|
69
|
-
if (paletteUser &&
|
|
70
|
-
var pAccess = onGetProjectAccess(pSlug);
|
|
71
|
-
if (pAccess && !pAccess.error && !users.canAccessProject(paletteUser.id, pAccess)) return;
|
|
72
|
-
}
|
|
75
|
+
var pAccess = accessFor(paletteUser, pSlug, status);
|
|
76
|
+
if (paletteUser && !pAccess) return;
|
|
73
77
|
var accessibleSessions = [];
|
|
74
78
|
pCtx.sm.sessions.forEach(function (session) {
|
|
75
79
|
if (session.hidden) return;
|
|
76
80
|
if (paletteUser) {
|
|
77
81
|
if (users.isMultiUser()) {
|
|
78
|
-
|
|
79
|
-
if (!users.canAccessSession(paletteUser.id, session, sAccess)) return;
|
|
82
|
+
if (!users.canAccessSession(paletteUser.id, session, pAccess)) return;
|
|
80
83
|
}
|
|
81
84
|
} else {
|
|
82
85
|
if (session.ownerId) return;
|
package/lib/server.js
CHANGED
|
@@ -202,6 +202,23 @@ function createServer(opts) {
|
|
|
202
202
|
var isRequestAuthed = auth.isRequestAuthed;
|
|
203
203
|
var parseCookies = auth.parseCookies;
|
|
204
204
|
|
|
205
|
+
function canUserAccessSlug(userId, slug) {
|
|
206
|
+
if (!userId || !slug) return false;
|
|
207
|
+
var project = projects && projects.get(slug);
|
|
208
|
+
var status = project && project.getStatus ? project.getStatus() : null;
|
|
209
|
+
if (status && status.isMate === true) {
|
|
210
|
+
return users.canAccessProject(userId, {
|
|
211
|
+
visibility: "private",
|
|
212
|
+
ownerId: status.projectOwnerId || null,
|
|
213
|
+
allowedUsers: [],
|
|
214
|
+
}) === true;
|
|
215
|
+
}
|
|
216
|
+
if (!onGetProjectAccess) return false;
|
|
217
|
+
var access = onGetProjectAccess(slug);
|
|
218
|
+
if (!access || access.error) return false;
|
|
219
|
+
return users.canAccessProject(userId, access) === true;
|
|
220
|
+
}
|
|
221
|
+
|
|
205
222
|
var realVersion = require("../package.json").version;
|
|
206
223
|
var currentVersion = debug ? "0.0.9" : realVersion;
|
|
207
224
|
|
|
@@ -339,6 +356,13 @@ function createServer(opts) {
|
|
|
339
356
|
if (!access || access.error) return false;
|
|
340
357
|
return users.canAccessProject(userId, access) === true;
|
|
341
358
|
},
|
|
359
|
+
hasFullProjectAccess: function (userId, status) {
|
|
360
|
+
if (!status || !status.slug || !onGetProjectAccess) return false;
|
|
361
|
+
var access = onGetProjectAccess(status.slug);
|
|
362
|
+
if (!access || access.error) return false;
|
|
363
|
+
var governing = access.isWorktree && access.parentAccess ? access.parentAccess : access;
|
|
364
|
+
return users.canAccessProject(userId, governing) === true;
|
|
365
|
+
},
|
|
342
366
|
resolveMate: function (userId, mateId) {
|
|
343
367
|
return mates.getMate(mates.buildMateCtx(users.isMultiUser() ? userId : null), mateId);
|
|
344
368
|
},
|
|
@@ -403,6 +427,7 @@ function createServer(opts) {
|
|
|
403
427
|
projects: projects,
|
|
404
428
|
getMultiUserFromReq: getMultiUserFromReq,
|
|
405
429
|
onGetProjectAccess: onGetProjectAccess,
|
|
430
|
+
canUserAccessSlug: canUserAccessSlug,
|
|
406
431
|
});
|
|
407
432
|
|
|
408
433
|
// --- Push module (global) ---
|
|
@@ -662,7 +687,11 @@ function createServer(opts) {
|
|
|
662
687
|
if (isRequestAuthed(req)) {
|
|
663
688
|
var mem = process.memoryUsage();
|
|
664
689
|
var activeSessions = 0;
|
|
665
|
-
|
|
690
|
+
var healthProjects = 0;
|
|
691
|
+
var healthUser = users.isMultiUser() ? getMultiUserFromReq(req) : null;
|
|
692
|
+
projects.forEach(function (ctx, slug) {
|
|
693
|
+
if (users.isMultiUser() && (!healthUser || !canUserAccessSlug(healthUser.id, slug))) return;
|
|
694
|
+
healthProjects++;
|
|
666
695
|
if (ctx && ctx.clients) {
|
|
667
696
|
activeSessions += ctx.clients.size || 0;
|
|
668
697
|
}
|
|
@@ -671,7 +700,7 @@ function createServer(opts) {
|
|
|
671
700
|
health.version = pkg.version;
|
|
672
701
|
health.node = process.version;
|
|
673
702
|
health.sessions = activeSessions;
|
|
674
|
-
health.projects =
|
|
703
|
+
health.projects = healthProjects;
|
|
675
704
|
health.memory = {
|
|
676
705
|
rss: mem.rss,
|
|
677
706
|
heapUsed: mem.heapUsed,
|
|
@@ -850,7 +879,9 @@ function createServer(opts) {
|
|
|
850
879
|
return;
|
|
851
880
|
}
|
|
852
881
|
var projectList = [];
|
|
882
|
+
var infoUser = users.isMultiUser() ? getMultiUserFromReq(req) : null;
|
|
853
883
|
projects.forEach(function (ctx, slug) {
|
|
884
|
+
if (users.isMultiUser() && (!infoUser || !canUserAccessSlug(infoUser.id, slug))) return;
|
|
854
885
|
projectList.push({ slug: slug, project: ctx.project });
|
|
855
886
|
});
|
|
856
887
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
@@ -912,15 +943,12 @@ function createServer(opts) {
|
|
|
912
943
|
res.setHeader("Set-Cookie", "clay_last_project=" + slug + "; Path=/; SameSite=Strict; Max-Age=31536000" + (tlsOptions ? "; Secure" : ""));
|
|
913
944
|
|
|
914
945
|
// Multi-user: check project access for HTTP requests
|
|
915
|
-
if (users.isMultiUser()
|
|
946
|
+
if (users.isMultiUser()) {
|
|
916
947
|
var httpUser = getMultiUserFromReq(req);
|
|
917
|
-
if (httpUser) {
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
res.end();
|
|
922
|
-
return;
|
|
923
|
-
}
|
|
948
|
+
if (!httpUser || !canUserAccessSlug(httpUser.id, slug)) {
|
|
949
|
+
res.writeHead(302, { "Location": "/" });
|
|
950
|
+
res.end();
|
|
951
|
+
return;
|
|
924
952
|
}
|
|
925
953
|
}
|
|
926
954
|
|
|
@@ -1029,6 +1057,9 @@ function createServer(opts) {
|
|
|
1029
1057
|
onAppMessage: function (ws, msg) {
|
|
1030
1058
|
return handleDmMessage(ws, msg);
|
|
1031
1059
|
},
|
|
1060
|
+
onConnection: function (ws) {
|
|
1061
|
+
sendProjectsUpdatedToClient(ws);
|
|
1062
|
+
},
|
|
1032
1063
|
});
|
|
1033
1064
|
|
|
1034
1065
|
server.on("upgrade", function (req, socket, head) {
|
|
@@ -1106,19 +1137,19 @@ function createServer(opts) {
|
|
|
1106
1137
|
if (users.isMultiUser()) {
|
|
1107
1138
|
wsUser = getMultiUserFromReq(req);
|
|
1108
1139
|
// Check project access for multi-user mode
|
|
1109
|
-
if (wsUser
|
|
1110
|
-
|
|
1111
|
-
var accessSlug = (wsSlug.indexOf("--") !== -1) ? wsSlug.split("--")[0] : wsSlug;
|
|
1112
|
-
var projectAccess = onGetProjectAccess(accessSlug);
|
|
1140
|
+
if (wsUser) {
|
|
1141
|
+
var projectAccess = onGetProjectAccess ? onGetProjectAccess(wsSlug) : null;
|
|
1113
1142
|
if (debug) console.log("[server] WS access check:", wsSlug, "user:", wsUser.id, "role:", wsUser.role, "visibility:", projectAccess && projectAccess.visibility, "ownerId:", projectAccess && projectAccess.ownerId, "allowed:", projectAccess && projectAccess.allowedUsers);
|
|
1114
|
-
if (
|
|
1115
|
-
if (
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
return;
|
|
1120
|
-
}
|
|
1143
|
+
if (!canUserAccessSlug(wsUser.id, wsSlug)) {
|
|
1144
|
+
if (debug) console.log("[server] WS rejected: access denied for", wsUser.id, "on", wsSlug);
|
|
1145
|
+
socket.write("HTTP/1.1 403 Forbidden\r\n\r\n");
|
|
1146
|
+
socket.destroy();
|
|
1147
|
+
return;
|
|
1121
1148
|
}
|
|
1149
|
+
} else {
|
|
1150
|
+
socket.write("HTTP/1.1 403 Forbidden\r\n\r\n");
|
|
1151
|
+
socket.destroy();
|
|
1152
|
+
return;
|
|
1122
1153
|
}
|
|
1123
1154
|
}
|
|
1124
1155
|
|
|
@@ -1200,13 +1231,9 @@ function createServer(opts) {
|
|
|
1200
1231
|
var filtered = allProjectsList;
|
|
1201
1232
|
if (users.isMultiUser() && onGetProjectAccess) {
|
|
1202
1233
|
var wsUser = ws._clayUser;
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
if (!access || access.error) return true;
|
|
1207
|
-
return users.canAccessProject(wsUser.id, access);
|
|
1208
|
-
});
|
|
1209
|
-
}
|
|
1234
|
+
filtered = wsUser ? allProjectsList.filter(function (p) {
|
|
1235
|
+
return canUserAccessSlug(wsUser.id, p.slug);
|
|
1236
|
+
}) : [];
|
|
1210
1237
|
}
|
|
1211
1238
|
// Attach per-project unread counts for this client
|
|
1212
1239
|
var unreadMap = getCrossProjectUnread(ws);
|
|
@@ -1306,20 +1333,12 @@ function createServer(opts) {
|
|
|
1306
1333
|
authToken: pinHash || null,
|
|
1307
1334
|
getProjectCount: function () { return projects.size; },
|
|
1308
1335
|
getProjectList: function (userId) {
|
|
1309
|
-
|
|
1310
|
-
projects.forEach(function (ctx, s) {
|
|
1311
|
-
var status = ctx.getStatus();
|
|
1312
|
-
if (userId && users.isMultiUser() && onGetProjectAccess) {
|
|
1313
|
-
var access = onGetProjectAccess(s);
|
|
1314
|
-
if (access && !access.error && !users.canAccessProject(userId, access)) return;
|
|
1315
|
-
}
|
|
1316
|
-
list.push(status);
|
|
1317
|
-
});
|
|
1318
|
-
return list;
|
|
1336
|
+
return getVisibleProjects(userId);
|
|
1319
1337
|
},
|
|
1320
|
-
getHubSchedules: function () {
|
|
1338
|
+
getHubSchedules: function (userId) {
|
|
1321
1339
|
var allSchedules = [];
|
|
1322
1340
|
projects.forEach(function (ctx, s) {
|
|
1341
|
+
if (users.isMultiUser() && (!userId || !canUserAccessSlug(userId, s))) return;
|
|
1323
1342
|
var status = ctx.getStatus();
|
|
1324
1343
|
var recs = ctx.getSchedules();
|
|
1325
1344
|
for (var i = 0; i < recs.length; i++) {
|
|
@@ -1379,6 +1398,7 @@ function createServer(opts) {
|
|
|
1379
1398
|
if (!ctx) return 0;
|
|
1380
1399
|
return ctx.getSchedules().length;
|
|
1381
1400
|
},
|
|
1401
|
+
canAccessProjectSlug: canUserAccessSlug,
|
|
1382
1402
|
onPresenceChange: broadcastPresenceChange,
|
|
1383
1403
|
onProcessingChanged: broadcastProcessingChange,
|
|
1384
1404
|
onSessionDone: function () { onSessionDone(slug); },
|
|
@@ -1592,27 +1612,81 @@ function createServer(opts) {
|
|
|
1592
1612
|
function getServerUsers() {
|
|
1593
1613
|
var seen = {};
|
|
1594
1614
|
var list = [];
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
avatarCustom: p.avatarCustom || "",
|
|
1610
|
-
});
|
|
1615
|
+
forEachAppClient(function (ws) {
|
|
1616
|
+
if (!ws._clayUser) return;
|
|
1617
|
+
var u = ws._clayUser;
|
|
1618
|
+
if (seen[u.id]) return;
|
|
1619
|
+
seen[u.id] = true;
|
|
1620
|
+
var p = u.profile || {};
|
|
1621
|
+
list.push({
|
|
1622
|
+
id: u.id,
|
|
1623
|
+
displayName: p.name || u.displayName || u.username,
|
|
1624
|
+
username: u.username,
|
|
1625
|
+
avatarStyle: p.avatarStyle || "imprint",
|
|
1626
|
+
avatarSeed: p.avatarSeed || u.username,
|
|
1627
|
+
avatarColor: p.avatarColor || "#5857fc",
|
|
1628
|
+
avatarCustom: p.avatarCustom || "",
|
|
1611
1629
|
});
|
|
1612
1630
|
});
|
|
1613
1631
|
return list;
|
|
1614
1632
|
}
|
|
1615
1633
|
|
|
1634
|
+
function forEachAppClient(visitor) {
|
|
1635
|
+
if (globalWs && typeof globalWs.forEachClient === "function") {
|
|
1636
|
+
globalWs.forEachClient(visitor);
|
|
1637
|
+
}
|
|
1638
|
+
projects.forEach(function (ctx) {
|
|
1639
|
+
ctx.forEachClient(visitor);
|
|
1640
|
+
});
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
function getVisibleProjects(userId) {
|
|
1644
|
+
var visible = [];
|
|
1645
|
+
projects.forEach(function (ctx, slug) {
|
|
1646
|
+
if (users.isMultiUser() && (!userId || !canUserAccessSlug(userId, slug))) return;
|
|
1647
|
+
visible.push(ctx.getStatus());
|
|
1648
|
+
});
|
|
1649
|
+
return visible;
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
function sendProjectsUpdatedToClient(ws, serverUsers, allUsers) {
|
|
1653
|
+
if (!ws || ws.readyState !== 1) return;
|
|
1654
|
+
var userId = ws._clayUser ? ws._clayUser.id : null;
|
|
1655
|
+
var visibleProjects = getVisibleProjects(userId);
|
|
1656
|
+
var message = {
|
|
1657
|
+
type: "projects_updated",
|
|
1658
|
+
projects: visibleProjects,
|
|
1659
|
+
projectCount: visibleProjects.length,
|
|
1660
|
+
removedProjects: getRemovedProjects(userId),
|
|
1661
|
+
};
|
|
1662
|
+
if (users.isMultiUser()) {
|
|
1663
|
+
message.serverUsers = serverUsers || getServerUsers();
|
|
1664
|
+
message.allUsers = allUsers || users.getAllUsers().map(function (user) {
|
|
1665
|
+
var profile = user.profile || {};
|
|
1666
|
+
return {
|
|
1667
|
+
id: user.id,
|
|
1668
|
+
displayName: profile.name || user.displayName || user.username,
|
|
1669
|
+
username: user.username,
|
|
1670
|
+
role: user.role,
|
|
1671
|
+
avatarStyle: profile.avatarStyle || "imprint",
|
|
1672
|
+
avatarSeed: profile.avatarSeed || user.username,
|
|
1673
|
+
avatarColor: profile.avatarColor || "#5857fc",
|
|
1674
|
+
avatarCustom: profile.avatarCustom || "",
|
|
1675
|
+
};
|
|
1676
|
+
});
|
|
1677
|
+
message.dmFavorites = userId ? users.getDmFavorites(userId) : [];
|
|
1678
|
+
message.dmConversations = [];
|
|
1679
|
+
if (userId) {
|
|
1680
|
+
var hidden = users.getDmHidden(userId);
|
|
1681
|
+
var dmList = dm.getDmList(userId);
|
|
1682
|
+
for (var i = 0; i < dmList.length; i++) {
|
|
1683
|
+
if (hidden.indexOf(dmList[i].otherUserId) === -1) message.dmConversations.push(dmList[i].otherUserId);
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
ws.send(JSON.stringify(message));
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1616
1690
|
// Debounced broadcast of projects_updated when presence changes
|
|
1617
1691
|
// Sends per-user filtered project lists + server-wide user list
|
|
1618
1692
|
var presenceTimer = null;
|
|
@@ -1621,12 +1695,7 @@ function createServer(opts) {
|
|
|
1621
1695
|
presenceTimer = setTimeout(function () {
|
|
1622
1696
|
presenceTimer = null;
|
|
1623
1697
|
if (!users.isMultiUser()) {
|
|
1624
|
-
|
|
1625
|
-
type: "projects_updated",
|
|
1626
|
-
projects: getProjects(),
|
|
1627
|
-
projectCount: projects.size,
|
|
1628
|
-
removedProjects: getRemovedProjects(),
|
|
1629
|
-
});
|
|
1698
|
+
forEachAppClient(function (ws) { sendProjectsUpdatedToClient(ws); });
|
|
1630
1699
|
return;
|
|
1631
1700
|
}
|
|
1632
1701
|
var serverUsers = getServerUsers();
|
|
@@ -1643,61 +1712,73 @@ function createServer(opts) {
|
|
|
1643
1712
|
avatarCustom: p.avatarCustom || "",
|
|
1644
1713
|
};
|
|
1645
1714
|
});
|
|
1646
|
-
// Build per-user filtered lists, send individually
|
|
1715
|
+
// Build per-user filtered lists, send individually to both project and
|
|
1716
|
+
// slug-less Home sockets. A Home fallback must replace, never retain,
|
|
1717
|
+
// the project cache from its previous authenticated project socket.
|
|
1647
1718
|
var sentUsers = {};
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
// Per-user DM data
|
|
1667
|
-
var userDmFavorites = userId ? users.getDmFavorites(userId) : [];
|
|
1668
|
-
var userDmHidden = userId ? users.getDmHidden(userId) : [];
|
|
1669
|
-
var userDmConversations = [];
|
|
1670
|
-
if (userId) {
|
|
1671
|
-
var dmList = dm.getDmList(userId);
|
|
1672
|
-
for (var di = 0; di < dmList.length; di++) {
|
|
1673
|
-
if (userDmHidden.indexOf(dmList[di].otherUserId) === -1) {
|
|
1674
|
-
userDmConversations.push(dmList[di].otherUserId);
|
|
1675
|
-
}
|
|
1719
|
+
forEachAppClient(function (ws) {
|
|
1720
|
+
var userId = ws._clayUser ? ws._clayUser.id : null;
|
|
1721
|
+
var key = userId || "__anon__";
|
|
1722
|
+
if (sentUsers[key]) {
|
|
1723
|
+
// Already computed for this user, just send the cached msg
|
|
1724
|
+
if (ws.readyState === 1) ws.send(sentUsers[key]);
|
|
1725
|
+
return;
|
|
1726
|
+
}
|
|
1727
|
+
var filteredProjects = getVisibleProjects(userId);
|
|
1728
|
+
// Per-user DM data
|
|
1729
|
+
var userDmFavorites = userId ? users.getDmFavorites(userId) : [];
|
|
1730
|
+
var userDmHidden = userId ? users.getDmHidden(userId) : [];
|
|
1731
|
+
var userDmConversations = [];
|
|
1732
|
+
if (userId) {
|
|
1733
|
+
var dmList = dm.getDmList(userId);
|
|
1734
|
+
for (var di = 0; di < dmList.length; di++) {
|
|
1735
|
+
if (userDmHidden.indexOf(dmList[di].otherUserId) === -1) {
|
|
1736
|
+
userDmConversations.push(dmList[di].otherUserId);
|
|
1676
1737
|
}
|
|
1677
1738
|
}
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
sentUsers[key] = msgStr;
|
|
1689
|
-
ws.send(msgStr);
|
|
1739
|
+
}
|
|
1740
|
+
var msgStr = JSON.stringify({
|
|
1741
|
+
type: "projects_updated",
|
|
1742
|
+
projects: filteredProjects,
|
|
1743
|
+
projectCount: filteredProjects.length,
|
|
1744
|
+
serverUsers: serverUsers,
|
|
1745
|
+
allUsers: allUsers,
|
|
1746
|
+
dmFavorites: userDmFavorites,
|
|
1747
|
+
dmConversations: userDmConversations,
|
|
1748
|
+
removedProjects: getRemovedProjects(userId),
|
|
1690
1749
|
});
|
|
1750
|
+
sentUsers[key] = msgStr;
|
|
1751
|
+
if (ws.readyState === 1) ws.send(msgStr);
|
|
1691
1752
|
});
|
|
1692
1753
|
}, 300);
|
|
1693
1754
|
}
|
|
1694
1755
|
|
|
1695
1756
|
function broadcastAll(msg) {
|
|
1757
|
+
if (msg && msg.type === "projects_updated") {
|
|
1758
|
+
broadcastPresenceChange();
|
|
1759
|
+
return;
|
|
1760
|
+
}
|
|
1696
1761
|
projects.forEach(function (ctx) {
|
|
1697
1762
|
ctx.send(msg);
|
|
1698
1763
|
});
|
|
1699
1764
|
}
|
|
1700
1765
|
|
|
1766
|
+
function refreshProjectAccess() {
|
|
1767
|
+
if (!users.isMultiUser()) return;
|
|
1768
|
+
projects.forEach(function (ctx, slug) {
|
|
1769
|
+
if (!ctx || typeof ctx.forEachClient !== "function") return;
|
|
1770
|
+
ctx.forEachClient(function (ws) {
|
|
1771
|
+
var userId = ws._clayUser && ws._clayUser.id;
|
|
1772
|
+
if (userId && canUserAccessSlug(userId, slug)) return;
|
|
1773
|
+
try {
|
|
1774
|
+
if (ws.readyState === 1) ws.send(JSON.stringify({ type: "access_revoked", slug: slug }));
|
|
1775
|
+
ws.close(1008, "Project access revoked");
|
|
1776
|
+
} catch (e) {}
|
|
1777
|
+
});
|
|
1778
|
+
});
|
|
1779
|
+
broadcastPresenceChange();
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1701
1782
|
// Send a message to every live ws belonging to a specific user across all projects.
|
|
1702
1783
|
// Used by user-targeted notifications (e.g. user-to-user @mentions).
|
|
1703
1784
|
function sendToUser(userId, msg) {
|
|
@@ -1813,6 +1894,7 @@ function createServer(opts) {
|
|
|
1813
1894
|
setRecovery: auth.setRecovery,
|
|
1814
1895
|
clearRecovery: auth.clearRecovery,
|
|
1815
1896
|
broadcastAll: broadcastAll,
|
|
1897
|
+
refreshProjectAccess: refreshProjectAccess,
|
|
1816
1898
|
forEachProject: forEachProject,
|
|
1817
1899
|
destroyProject: removeProject,
|
|
1818
1900
|
destroyAll: destroyAll,
|