clay-server 5.3.0-beta.4 → 5.3.0-beta.6

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.
Files changed (144) hide show
  1. package/lib/ask-user-request-identity.js +18 -1
  2. package/lib/clay-product-control-tools.js +2 -2
  3. package/lib/clay-product-control.js +1 -1
  4. package/lib/clay-screen-controls.js +1 -0
  5. package/lib/clay-split-view-awareness.js +5 -2
  6. package/lib/clay-split-view-controls.js +14 -7
  7. package/lib/clay-split-view-input.js +5 -4
  8. package/lib/clay-split-view-render-proof.js +14 -4
  9. package/lib/clay-view-workspace-input.js +71 -0
  10. package/lib/project-delegated-follow-up.js +18 -9
  11. package/lib/project-file-history.js +11 -9
  12. package/lib/project-file-watch.js +26 -7
  13. package/lib/project-filesystem.js +5 -4
  14. package/lib/project-shared-browser.js +7 -3
  15. package/lib/project-split-view-document.js +16 -2
  16. package/lib/project-split-views.js +15 -294
  17. package/lib/project-user-message.js +4 -3
  18. package/lib/project-vendor-login.js +2 -2
  19. package/lib/project-view-input.js +11 -0
  20. package/lib/project-view-pair.js +26 -0
  21. package/lib/project-view-store.js +346 -0
  22. package/lib/project-view-terminal-binding.js +16 -0
  23. package/lib/project-view-workspace.js +126 -0
  24. package/lib/project.js +1 -0
  25. package/lib/public/app.js +2 -0
  26. package/lib/public/css/issues.css +3 -0
  27. package/lib/public/css/pane.css +5 -1
  28. package/lib/public/css/right-workbench.css +25 -3
  29. package/lib/public/css/scheduled-tasks.css +7 -0
  30. package/lib/public/css/shared-browser.css +21 -0
  31. package/lib/public/css/split-view-layout.css +37 -3
  32. package/lib/public/css/sticky-notes.css +3 -2
  33. package/lib/public/css/tool-pane.css +127 -0
  34. package/lib/public/index.html +3 -3
  35. package/lib/public/modules/app-header.js +24 -2
  36. package/lib/public/modules/app-messages.js +1 -1
  37. package/lib/public/modules/clay-file-links.js +2 -1
  38. package/lib/public/modules/clay-session-links.js +1 -1
  39. package/lib/public/modules/clay-split-view-screen.js +6 -3
  40. package/lib/public/modules/clay-tool-pane-screen.js +35 -0
  41. package/lib/public/modules/file-history-snapshot.js +12 -0
  42. package/lib/public/modules/file-pane-entrypoints.js +55 -0
  43. package/lib/public/modules/file-pane-history.js +78 -0
  44. package/lib/public/modules/file-pane-instances.js +140 -0
  45. package/lib/public/modules/file-pane-links.js +41 -0
  46. package/lib/public/modules/file-pane-render.js +48 -0
  47. package/lib/public/modules/file-pane-review.js +26 -0
  48. package/lib/public/modules/file-pane-toolbar.js +54 -0
  49. package/lib/public/modules/file-viewer-source.js +70 -0
  50. package/lib/public/modules/filebrowser.js +44 -83
  51. package/lib/public/modules/git-panel.js +5 -0
  52. package/lib/public/modules/issues.js +3 -4
  53. package/lib/public/modules/linear-panel.js +2 -2
  54. package/lib/public/modules/mate-knowledge-workbench.js +10 -6
  55. package/lib/public/modules/notifications.js +4 -1
  56. package/lib/public/modules/pane-activation.js +5 -0
  57. package/lib/public/modules/pane-binding.js +5 -1
  58. package/lib/public/modules/pane-browser-bridge.js +20 -6
  59. package/lib/public/modules/pane-browser-transport.js +110 -0
  60. package/lib/public/modules/pane-header-runtime.js +18 -0
  61. package/lib/public/modules/plantuml-preview.js +12 -12
  62. package/lib/public/modules/right-workbench.js +116 -67
  63. package/lib/public/modules/scheduled-tasks.js +7 -6
  64. package/lib/public/modules/shared-browser.js +75 -9
  65. package/lib/public/modules/split-pane-renderer.js +7 -8
  66. package/lib/public/modules/split-pane-runtime.js +23 -0
  67. package/lib/public/modules/split-view-drag-preview.js +90 -0
  68. package/lib/public/modules/split-view-layout-client.js +1 -1
  69. package/lib/public/modules/split-view-presentation-drag.js +36 -13
  70. package/lib/public/modules/split-view-presentation-drop.js +21 -18
  71. package/lib/public/modules/split-view-presentation-renderer.js +15 -10
  72. package/lib/public/modules/split-view-presentations.js +25 -33
  73. package/lib/public/modules/split-view.js +17 -16
  74. package/lib/public/modules/sticky-notes-browser.js +2 -2
  75. package/lib/public/modules/sticky-notes.js +8 -10
  76. package/lib/public/modules/terminal.js +45 -13
  77. package/lib/public/modules/theme.js +1 -1
  78. package/lib/public/modules/tool-pane-chrome.js +82 -0
  79. package/lib/public/modules/tool-pane-drag.js +134 -0
  80. package/lib/public/modules/tool-pane-layout.js +95 -0
  81. package/lib/public/modules/tool-pane-legacy.js +60 -0
  82. package/lib/public/modules/tool-pane-tree.js +51 -0
  83. package/lib/public/modules/view-legacy-coordinates.js +16 -0
  84. package/lib/public/modules/view-mutations.js +24 -0
  85. package/lib/public/modules/view-pair-presentation.js +74 -0
  86. package/lib/public/modules/view-pair-validation.js +12 -0
  87. package/lib/public/modules/view-presentation-state.js +37 -0
  88. package/lib/public/modules/view-workspace-controller.js +210 -0
  89. package/lib/public/modules/view-workspace-state.js +103 -0
  90. package/lib/public/modules/view-workspace-validation.js +47 -0
  91. package/lib/public/modules/wireframe-workbench.js +11 -0
  92. package/lib/public/modules/workbench-layout-preference.js +12 -5
  93. package/lib/public/modules/workbench-panels.js +21 -0
  94. package/lib/public/modules/workspace-assignment-card.js +9 -6
  95. package/lib/public/pane-assets/{app-ED2VCGNQ.js → app-7UZ727NA.js} +1 -1
  96. package/lib/public/pane-assets/chunk-2AIWCI6Y.js +1 -0
  97. package/lib/public/pane-assets/chunk-6MIHIQ2W.js +1 -0
  98. package/lib/public/pane-assets/chunk-6NYCNWVF.js +1 -0
  99. package/lib/public/pane-assets/chunk-6QFLYOVY.js +1 -0
  100. package/lib/public/pane-assets/chunk-6UJ2ALOB.js +1 -0
  101. package/lib/public/pane-assets/chunk-6XK2OATZ.js +1 -0
  102. package/lib/public/pane-assets/chunk-BKIDR2ME.js +1 -0
  103. package/lib/public/pane-assets/chunk-BMHTBRMO.js +1 -0
  104. package/lib/public/pane-assets/chunk-GEFCFZEN.js +1 -0
  105. package/lib/public/pane-assets/chunk-GN2NQEVC.js +2 -0
  106. package/lib/public/pane-assets/chunk-IMLMBPT4.js +166 -0
  107. package/lib/public/pane-assets/chunk-JIV7O5GN.js +1 -0
  108. package/lib/public/pane-assets/chunk-K5S6AILM.js +1 -0
  109. package/lib/public/pane-assets/chunk-LDIZLKB2.js +1 -0
  110. package/lib/public/pane-assets/chunk-OWNNLZ36.js +1 -0
  111. package/lib/public/pane-assets/chunk-P2C3LOWL.js +3 -0
  112. package/lib/public/pane-assets/chunk-PV4PCH2V.js +2 -0
  113. package/lib/public/pane-assets/chunk-R4LMPTKU.js +1 -0
  114. package/lib/public/pane-assets/chunk-R7QVVFUG.js +1 -0
  115. package/lib/public/pane-assets/chunk-TMGSDFJI.js +1 -0
  116. package/lib/public/pane-assets/chunk-VKYBAVWT.js +1 -0
  117. package/lib/public/pane-assets/chunk-VNAV23CM.js +1 -0
  118. package/lib/public/pane-assets/chunk-VWTHIQCE.js +1 -0
  119. package/lib/public/pane-assets/chunk-VZ2OKDAO.js +1 -0
  120. package/lib/public/pane-assets/chunk-WM3K5AFH.js +1 -0
  121. package/lib/public/pane-assets/chunk-XOB3O327.js +1 -0
  122. package/lib/public/pane-assets/chunk-XOOUCFYT.js +1 -0
  123. package/lib/public/pane-assets/manifest.json +383 -336
  124. package/lib/public/pane-assets/style-ZM7OOU3M.css +1 -0
  125. package/lib/public/style.css +1 -0
  126. package/lib/server-workbench-layout-settings.js +3 -3
  127. package/lib/server.js +1 -0
  128. package/lib/split-view-layout.js +11 -7
  129. package/lib/users-workbench-layout-preferences.js +14 -4
  130. package/lib/workbench-window-layout.js +30 -0
  131. package/lib/workspace-assignment-service.js +56 -11
  132. package/lib/workspace-follow-up-queue.js +92 -0
  133. package/lib/workspace-query-mcp-server.js +1 -1
  134. package/package.json +1 -1
  135. package/lib/public/pane-assets/chunk-6D4DPE5X.js +0 -1
  136. package/lib/public/pane-assets/chunk-6SM2WQJX.js +0 -1
  137. package/lib/public/pane-assets/chunk-7H36SUZA.js +0 -2
  138. package/lib/public/pane-assets/chunk-DGOFGEUK.js +0 -1
  139. package/lib/public/pane-assets/chunk-I57URYSL.js +0 -1
  140. package/lib/public/pane-assets/chunk-IYFILQHZ.js +0 -2
  141. package/lib/public/pane-assets/chunk-IZ2ZANEY.js +0 -167
  142. package/lib/public/pane-assets/chunk-JAEQCN5S.js +0 -1
  143. package/lib/public/pane-assets/chunk-ST2Q62ET.js +0 -1
  144. package/lib/public/pane-assets/style-JVCOCG3W.css +0 -1
@@ -45,13 +45,14 @@ function attachFilesystem(ctx) {
45
45
  // Resolve live authority before every operation; never trust message roles.
46
46
  if (/^fs_/.test(msg.type) && msg.type !== "fs_unwatch") {
47
47
  try {
48
+ if (msg.instanceId && (msg.projectSlug !== slug || (msg.accountId || null) !== (ws._clayUser && ws._clayUser.id || null))) throw new Error('File pane context changed');
48
49
  fileScope = access.fileScope(ws);
49
50
  if (/^fs_(list|read|write|watch)$/.test(msg.type)) {
50
51
  fileTarget = resolveFilePath(cwd, msg.type === "fs_list" ? msg.path || "." : msg.path, fileScope);
51
52
  }
52
53
  } catch (e) {
53
54
  var denied = fileError(e);
54
- sendTo(ws, { type: msg.type + "_result", path: msg.path, requestId: msg.requestId, projectSlug: msg.projectSlug, sessionId: msg.sessionId, accountId: msg.accountId, ok: false, error: denied.message, errorCode: denied.code });
55
+ sendTo(ws, { type: msg.type + "_result", path: msg.path, instanceId: msg.instanceId, requestId: msg.requestId, projectSlug: msg.projectSlug, sessionId: msg.sessionId, accountId: msg.accountId, ok: false, error: denied.message, errorCode: denied.code });
55
56
  return true;
56
57
  }
57
58
  }
@@ -115,7 +116,7 @@ function attachFilesystem(ctx) {
115
116
 
116
117
  // --- fs_read ---
117
118
  if (msg.type === "fs_read") {
118
- var metadata = { type: 'fs_read_result', path: msg.path, requestId: msg.requestId, projectSlug: msg.projectSlug, sessionId: msg.sessionId, accountId: msg.accountId };
119
+ var metadata = { type: 'fs_read_result', path: msg.path, instanceId: msg.instanceId, requestId: msg.requestId, projectSlug: msg.projectSlug, sessionId: msg.sessionId, accountId: msg.accountId };
119
120
  try {
120
121
  var readResult = operations.read(fileScope, msg.path);
121
122
  try { var fileSnapshot = operations.snapshot(fileScope, msg.path); if (fileSnapshot.hash === require('crypto').createHash('sha256').update(readResult.content).digest('hex')) readResult.controlRevision = require('./clay-control-target-projection').digest(fileSnapshot); } catch (ignored) {}
@@ -210,12 +211,12 @@ function attachFilesystem(ctx) {
210
211
 
211
212
  // --- File watcher ---
212
213
  if (msg.type === "fs_watch") {
213
- if (msg.path) startFileWatch(ws, msg.path);
214
+ if (msg.path) startFileWatch(ws, msg.path, msg.instanceId ? { instanceId: msg.instanceId, requestId: msg.requestId } : null);
214
215
  return true;
215
216
  }
216
217
 
217
218
  if (msg.type === "fs_unwatch") {
218
- stopFileWatch(ws);
219
+ stopFileWatch(ws, msg.instanceId ? { instanceId: msg.instanceId, requestId: msg.requestId } : { instanceId: undefined, requestId: undefined });
219
220
  return true;
220
221
  }
221
222
 
@@ -52,9 +52,13 @@ function attachSharedBrowser(ctx) {
52
52
  if (tabs.length > 1) throw new Error("Choose a browserId from status before operating a tab");
53
53
  return tabs[0] || null;
54
54
  }
55
+ function sendBrowserMessage(ws, session, message) {
56
+ var payload = require('./pane-session-transport').outbound(ws, message, session.localId);
57
+ if (payload) ctx.sendTo(ws, payload);
58
+ }
55
59
  function sendState(ws, session, record, present, focus) {
56
60
  if (!ws || ws.readyState !== 1 || !allowed(session, ws) || (record && !recordAllowed(record, ws))) return;
57
- ctx.sendTo(ws, { type: "shared_browser_state", sessionId: session.localId, browser: projection(record), browsers: sessionRecords(session).map(projection), present: !!present, focus: !!focus });
61
+ sendBrowserMessage(ws, session, { type: "shared_browser_state", sessionId: session.localId, browser: projection(record), browsers: sessionRecords(session).map(projection), present: !!present, focus: !!focus });
58
62
  }
59
63
  function broadcast(record, present) {
60
64
  ctx.clients.forEach(function (ws) {
@@ -110,7 +114,7 @@ function attachSharedBrowser(ctx) {
110
114
  record.frame = { type: "shared_browser_frame", sessionId: record.session.localId, browserId: record.id,
111
115
  data: message.data, width: record.width, height: record.height };
112
116
  viewers.forEach(function (id, ws) {
113
- if (watching(record, ws) && ws.bufferedAmount < 1024 * 1024) ctx.sendTo(ws, record.frame);
117
+ if (watching(record, ws) && ws.bufferedAmount < 1024 * 1024) sendBrowserMessage(ws, record.session, record.frame);
114
118
  });
115
119
  visibility(record);
116
120
  } else broadcast(record);
@@ -287,7 +291,7 @@ function attachSharedBrowser(ctx) {
287
291
  record = selectRecord(session, msg.browserId);
288
292
  if (!record || !recordAllowed(record, ws) || msg.browserId !== record.id) throw new Error("Browser session expired");
289
293
  if (msg.type === "shared_browser_view") {
290
- if (msg.visible) { var previous = records.get(viewers.get(ws)); viewers.set(ws, record.id); if (previous && previous !== record) visibility(previous); if (record.frame) ctx.sendTo(ws, record.frame); }
294
+ if (msg.visible) { var previous = records.get(viewers.get(ws)); viewers.set(ws, record.id); if (previous && previous !== record) visibility(previous); if (record.frame) sendBrowserMessage(ws, record.session, record.frame); }
291
295
  else viewers.delete(ws);
292
296
  visibility(record);
293
297
  } else if (msg.type === "shared_browser_control") {
@@ -1,9 +1,11 @@
1
1
  var layout = require("./split-view-layout");
2
+ var workspace = require("./project-view-workspace");
2
3
 
3
4
  function validDocument(value) {
4
5
  if (!value || value.version !== 1 || !value.projects || typeof value.projects !== "object" || Array.isArray(value.projects)) return false;
5
6
  var projectSlugs = Object.keys(value.projects);
6
7
  for (var i = 0; i < projectSlugs.length; i++) {
8
+ if (["__proto__", "constructor", "prototype"].indexOf(projectSlugs[i]) >= 0) return false;
7
9
  var project = value.projects[projectSlugs[i]];
8
10
  if (!project || !project.owners || typeof project.owners !== "object" || Array.isArray(project.owners)) return false;
9
11
  var ownerKeys = Object.keys(project.owners);
@@ -11,7 +13,7 @@ function validDocument(value) {
11
13
  var record = project.owners[ownerKeys[j]];
12
14
  if (!record || !(record.ownerId === null || typeof record.ownerId === "string") ||
13
15
  !(record.linuxUser === null || typeof record.linuxUser === "string") ||
14
- !Number.isInteger(record.revision) || record.revision < 0 || !Array.isArray(record.views)) return false;
16
+ !Number.isSafeInteger(record.revision) || record.revision < 0 || !Array.isArray(record.views)) return false;
15
17
  var ids = [], memberships = [];
16
18
  var ownerIdForKey = ownerKeys[j] === "default" ? null : ownerKeys[j].slice(5);
17
19
  if (ownerKeys[j] !== "default" && (ownerKeys[j].slice(0, 5) !== "user:" || !ownerIdForKey || ownerIdForKey !== record.ownerId)) return false;
@@ -25,9 +27,21 @@ function validDocument(value) {
25
27
  memberships.push(checked.origins[m]);
26
28
  }
27
29
  }
30
+ if (record.activeViewId != null && ids.indexOf(record.activeViewId) < 0) return false;
28
31
  }
29
32
  }
30
33
  return true;
31
34
  }
32
35
 
33
- module.exports = { validDocument: validDocument };
36
+ // Normalize copies only. Legacy records keep their original revision and bytes
37
+ // until an explicit mutation; no resource lookup or implicit migration write.
38
+ function normalizeDocument(value) {
39
+ if (!validDocument(value)) throw new TypeError('Invalid saved View document');
40
+ var result = JSON.parse(JSON.stringify(value));
41
+ Object.keys(result.projects).forEach(function (slug) {
42
+ var owners = result.projects[slug].owners;
43
+ Object.keys(owners).forEach(function (key) { owners[key].views = owners[key].views.map(workspace.normalized); });
44
+ });
45
+ return result;
46
+ }
47
+ module.exports = { validDocument: validDocument, normalizeDocument: normalizeDocument };
@@ -1,301 +1,15 @@
1
- var fs = require("fs");
2
- var path = require("path");
3
- var crypto = require("crypto");
4
1
  var layout = require("./split-view-layout");
5
2
  var names = require("./project-split-view-names");
6
3
  var sessionProvenance = require("./session-provenance");
7
4
  var validDocument = require("./project-split-view-document").validDocument;
5
+ var createSplitViewsStore = require("./project-view-store").createSplitViewsStore;
6
+ var workspace = require("./project-view-workspace");
8
7
  function clone(value) { return JSON.parse(JSON.stringify(value)); }
9
- function ownerKey(ownerId) { return ownerId == null ? "default" : "user:" + String(ownerId); }
10
- function emptyDocument() { return { version: 1, projects: {} }; }
11
- function emptyOwner(ownerId, linuxUser) { return { ownerId: ownerId == null ? null : String(ownerId), linuxUser: linuxUser || null, revision: 0, views: [] }; }
12
- function createSplitViewsStore(opts) {
13
- var slug = opts.slug;
14
- var sessionsDir = opts.sessionsDir;
15
- var filePath = opts.filePath || path.join(sessionsDir, "presentation-split-views.json");
16
- var fsImpl = opts.fs || fs;
17
- var persist = opts.persist || null;
18
- var document = emptyDocument();
19
- var loadError = null;
20
- try {
21
- var loaded = JSON.parse(fsImpl.readFileSync(filePath, "utf8"));
22
- if (!validDocument(loaded)) throw new Error("Stored Split view data is invalid");
23
- document = loaded;
24
- } catch (error) {
25
- if (!error || error.code !== "ENOENT") loadError = error;
26
- }
27
- function projectRecord(source, create) {
28
- if (!source.projects[slug] && create) source.projects[slug] = { owners: {} };
29
- return source.projects[slug] || null;
30
- }
31
- function ownerRecord(source, ownerId, linuxUser, create) {
32
- var project = projectRecord(source, create);
33
- if (!project) return null;
34
- var key = ownerKey(ownerId), record = project.owners[key];
35
- if (!record && create) { record = emptyOwner(ownerId, linuxUser); project.owners[key] = record; }
36
- if (record && (record.ownerId !== (ownerId == null ? null : String(ownerId)) || record.linuxUser !== (linuxUser || null))) return { identityMismatch: true };
37
- return record || null;
38
- }
39
- function save(next) {
40
- if (!validDocument(next)) throw new Error("Split view state failed validation");
41
- var serialized = JSON.stringify(next, null, 2) + "\n";
42
- if (persist) {
43
- if (persist(filePath, serialized) === false) throw new Error("Split view persistence returned false");
44
- return;
45
- }
46
- fsImpl.mkdirSync(sessionsDir, { recursive: true });
47
- var tempPath = filePath + ".tmp." + process.pid + "." + crypto.randomBytes(4).toString("hex");
48
- try {
49
- fsImpl.writeFileSync(tempPath, serialized, { encoding: "utf8", flag: "wx", mode: 0o600 });
50
- if (process.platform !== "win32") try { fsImpl.chmodSync(tempPath, 0o600); } catch (e) {}
51
- fsImpl.renameSync(tempPath, filePath);
52
- } catch (error) {
53
- try { fsImpl.unlinkSync(tempPath); } catch (e) {}
54
- throw error;
55
- }
56
- }
57
- function state(ownerId, linuxUser) {
58
- if (loadError) return { ok: false, error: "Stored Split view data could not be read; no changes were made", code: "storage", revision: 0, views: [] };
59
- var record = ownerRecord(document, ownerId, linuxUser, false);
60
- if (record && record.identityMismatch) return { ok: false, error: "Split view owner or OS identity changed", code: "identity", revision: 0, views: [] };
61
- return record ? { ok: true, revision: record.revision, views: clone(record.views) } : { ok: true, revision: 0, views: [] };
62
- }
63
- function conflict(actual) { return { ok: false, error: "Split view state changed. Reload and try again.", code: "conflict", revision: actual }; }
64
- function validateRevision(value) { return Number.isInteger(value) && value >= 0; }
65
- function prepareRecord(ownerId, linuxUser, expectedRevision) {
66
- var current = state(ownerId, linuxUser);
67
- if (!current.ok) return current;
68
- if (!validateRevision(expectedRevision) || expectedRevision !== current.revision) return conflict(current.revision);
69
- var next = clone(document), record = ownerRecord(next, ownerId, linuxUser, true);
70
- return { ok: true, next: next, record: record };
71
- }
72
- function commit(ownerId, record, changedView) {
73
- record.revision++;
74
- if (Array.isArray(changedView)) changedView.forEach(function (view) { if (view) view.revision = record.revision; });
75
- else if (changedView) changedView.revision = record.revision;
76
- try { save(documentForOwner(record, ownerId)); }
77
- catch (error) { return { ok: false, error: error.message || String(error), code: "storage", revision: state(ownerId, record.linuxUser).revision }; }
78
- document = documentForOwner(record, ownerId);
79
- var resultView = Array.isArray(changedView) ? changedView[changedView.length - 1] : changedView;
80
- return { ok: true, revision: record.revision, view: resultView ? clone(resultView) : null, views: clone(record.views) };
81
- }
82
- function documentForOwner(record, ownerId) {
83
- var next = clone(document), project = projectRecord(next, true);
84
- project.owners[ownerKey(ownerId)] = clone(record);
85
- return next;
86
- }
87
- function validExpectedView(record, id) {
88
- if (typeof id !== "string" || !id) return { error: "Split view not found" };
89
- var index = record.views.findIndex(function (view) { return view.id === id; });
90
- if (index === -1) return { error: "Split view not found" };
91
- return { index: index, view: record.views[index] };
92
- }
93
- function refsFor(origins, resolvedRefs) {
94
- if (!Array.isArray(origins) || origins.length < 2 || origins.length > layout.MAX_SESSIONS ||
95
- origins.some(function (origin) { return !layout.validOrigin(origin); }) || new Set(origins).size !== origins.length) return { error: "A Split view requires two to eight unique session origins" };
96
- if (!Array.isArray(resolvedRefs) || resolvedRefs.length !== origins.length) return { error: "One or more exact sessions could not be resolved" };
97
- var refs = [];
98
- for (var i = 0; i < origins.length; i++) {
99
- var ref = resolvedRefs.find(function (item) { return item && item.originId === origins[i]; });
100
- if (!ref) return { error: "One or more exact sessions could not be resolved" };
101
- refs.push({ originId: ref.originId, ownerId: ref.ownerId == null ? null : String(ref.ownerId), linuxUser: ref.linuxUser || null });
102
- }
103
- return { refs: refs };
104
- }
105
- function validateComposition(input, refs) {
106
- var origins = refs.map(function (ref) { return ref.originId; }), tree = layout.validateTree(input.tree, origins), preset = input.layout;
107
- if (!tree.ok) return { error: tree.error };
108
- if (!layout.validatePreset(preset)) return { error: "Unknown Split view layout" };
109
- if (typeof input.activeSessionOriginId !== "string" || origins.indexOf(input.activeSessionOriginId) === -1) return { error: "Active session must belong to the Split view" };
110
- return { origins: origins };
111
- }
112
- function uniqueMembership(views, refs, exceptId, secondExceptId) {
113
- var used = [];
114
- views.forEach(function (view) {
115
- if (view.id === exceptId || view.id === secondExceptId) return;
116
- view.sessionRefs.forEach(function (ref) { used.push(ref.originId); });
117
- });
118
- return refs.every(function (ref) { return used.indexOf(ref.originId) === -1; });
119
- }
120
- function create(ownerId, linuxUser, input, resolvedRefs) {
121
- input = input || {};
122
- var prepared = prepareRecord(ownerId, linuxUser, input.expectedRevision);
123
- if (!prepared.ok) return prepared;
124
- var name = layout.validateName(input.name), refs = refsFor(input.sessionOrigins, resolvedRefs);
125
- if (!name.ok) return name;
126
- if (refs.error) return { ok: false, error: refs.error };
127
- var composition = validateComposition(input, refs.refs);
128
- if (composition.error) return { ok: false, error: composition.error };
129
- if (!uniqueMembership(prepared.record.views, refs.refs)) return { ok: false, error: "A session can belong to only one Split view" };
130
- var nameMode = input.nameMode === "custom" ? "custom" : "automatic";
131
- var view = { id: crypto.randomUUID(), name: nameMode === "automatic" ? names.automaticName(input.liveSessions) : name.name, nameMode: nameMode,
132
- folderId: null, favorite: false, revision: prepared.record.revision + 1, layout: input.layout,
133
- activeSessionOriginId: input.activeSessionOriginId, tree: clone(input.tree), sessionRefs: refs.refs };
134
- prepared.record.views.push(view);
135
- return commit(ownerId, prepared.record, view);
136
- }
137
- function update(ownerId, linuxUser, input, resolvedRefs) {
138
- input = input || {};
139
- var prepared = prepareRecord(ownerId, linuxUser, input.expectedRevision);
140
- if (!prepared.ok) return prepared;
141
- var found = validExpectedView(prepared.record, input.id, input.expectedRevision);
142
- if (found.error) return { ok: false, error: found.error };
143
- if (found.conflict) return conflict(found.revision);
144
- var refs = refsFor(input.sessionOrigins, resolvedRefs);
145
- if (refs.error) return { ok: false, error: refs.error };
146
- var ordered = names.update(found.view, refs.refs, input.liveSessions);
147
- if (!ordered) return { ok: false, error: "One or more exact sessions could not be resolved" };
148
- refs.refs = ordered.refs; input.liveSessions = ordered.sessions;
149
- var composition = validateComposition(input, refs.refs);
150
- if (composition.error) return { ok: false, error: composition.error };
151
- if (!uniqueMembership(prepared.record.views, refs.refs, input.id)) return { ok: false, error: "A session can belong to only one Split view" };
152
- if (names.mode(found.view) === "automatic") found.view.name = names.automaticName(input.liveSessions);
153
- found.view.layout = input.layout; found.view.activeSessionOriginId = input.activeSessionOriginId;
154
- found.view.tree = clone(input.tree); found.view.sessionRefs = refs.refs;
155
- return commit(ownerId, prepared.record, found.view);
156
- }
157
- function rename(ownerId, linuxUser, input) {
158
- input = input || {};
159
- var prepared = prepareRecord(ownerId, linuxUser, input.expectedRevision);
160
- if (!prepared.ok) return prepared;
161
- var found = validExpectedView(prepared.record, input.id, input.expectedRevision);
162
- if (found.error) return { ok: false, error: found.error };
163
- if (found.conflict) return conflict(found.revision);
164
- var name = layout.validateName(input.name);
165
- if (!name.ok) return name;
166
- found.view.name = name.name;
167
- found.view.nameMode = "custom";
168
- return commit(ownerId, prepared.record, found.view);
169
- }
170
- function separate(ownerId, linuxUser, input) {
171
- input = input || {};
172
- var prepared = prepareRecord(ownerId, linuxUser, input.expectedRevision);
173
- if (!prepared.ok) return prepared;
174
- var found = validExpectedView(prepared.record, input.id, input.expectedRevision);
175
- if (found.error) return { ok: false, error: found.error };
176
- if (found.conflict) return conflict(found.revision);
177
- var removed = prepared.record.views.splice(found.index, 1)[0];
178
- return commit(ownerId, prepared.record, null);
179
- }
180
- function move(ownerId, linuxUser, input, resolvedSourceRefs, resolvedTargetRefs) {
181
- input = input || {};
182
- var prepared = prepareRecord(ownerId, linuxUser, input.expectedRevision);
183
- if (!prepared.ok) return prepared;
184
- if (typeof input.sourceSessionOriginId !== "string" || !layout.validOrigin(input.sourceSessionOriginId)) return { ok: false, error: "A valid source session is required", code: "invalid" };
185
- var source = validExpectedView(prepared.record, input.sourceViewId);
186
- if (source.error) return { ok: false, error: "Source Split view not found", code: "not_found", revision: prepared.record.revision };
187
- var sourceOrigins = source.view.sessionRefs.map(function (ref) { return ref.originId; });
188
- if (sourceOrigins.indexOf(input.sourceSessionOriginId) === -1) return { ok: false, error: "Source session does not belong to the source Split view", code: "session", revision: prepared.record.revision };
189
- if (!Array.isArray(resolvedSourceRefs) || resolvedSourceRefs.length !== source.view.sessionRefs.length) return { ok: false, error: "Source sessions could not be resolved", code: "session", revision: prepared.record.revision };
190
- for (var i = 0; i < source.view.sessionRefs.length; i++) {
191
- var storedSource = source.view.sessionRefs[i], liveSource = resolvedSourceRefs.find(function (ref) { return ref && ref.originId === storedSource.originId; });
192
- if (!liveSource || (liveSource.ownerId || null) !== (storedSource.ownerId || null) || (liveSource.linuxUser || null) !== (storedSource.linuxUser || null)) {
193
- return { ok: false, error: "A source session was replaced or changed identity", code: "session", revision: prepared.record.revision };
194
- }
195
- }
196
- var target = null, targetSessionOriginId = input.targetSessionOriginId;
197
- var hasTargetView = typeof input.targetViewId === "string" && input.targetViewId.length > 0;
198
- var hasStandaloneTarget = typeof targetSessionOriginId === "string" && layout.validOrigin(targetSessionOriginId);
199
- if (hasTargetView === hasStandaloneTarget || (input.targetViewId != null && !hasTargetView)) {
200
- return { ok: false, error: "Choose exactly one target Split view or standalone session", code: "invalid", revision: prepared.record.revision };
201
- }
202
- if (hasTargetView) {
203
- target = validExpectedView(prepared.record, input.targetViewId);
204
- if (target.error || target.view.id === source.view.id) return { ok: false, error: "Target Split view is unavailable", code: "not_found", revision: prepared.record.revision };
205
- }
206
- if (Array.isArray(input.targetSessionOrigins) && input.targetSessionOrigins.length > layout.MAX_SESSIONS) {
207
- return { ok: false, error: "A Split view can contain at most eight sessions", code: "limit", revision: prepared.record.revision };
208
- }
209
- var targetRefs = refsFor(input.targetSessionOrigins, resolvedTargetRefs);
210
- if (targetRefs.error) return { ok: false, error: targetRefs.error, code: "session", revision: prepared.record.revision };
211
- var requestedTargetOrigins = targetRefs.refs.map(function (ref) { return ref.originId; });
212
- if (requestedTargetOrigins.indexOf(input.sourceSessionOriginId) === -1) return { ok: false, error: "Target layout must include the moved source session", code: "invalid", revision: prepared.record.revision };
213
- if (target) {
214
- var requestedPriorOrigins = target.view.sessionRefs.map(function (ref) { return ref.originId; });
215
- if (requestedTargetOrigins.length !== requestedPriorOrigins.length + 1 || requestedPriorOrigins.some(function (origin) { return requestedTargetOrigins.indexOf(origin) === -1; })) {
216
- return { ok: false, error: "Target layout must preserve every existing target session", code: "invalid", revision: prepared.record.revision };
217
- }
218
- if (!uniqueMembership(prepared.record.views, targetRefs.refs, target.view.id, source.view.id)) return { ok: false, error: "A target session belongs to another Split view", code: "membership", revision: prepared.record.revision };
219
- } else if (targetSessionOriginId === input.sourceSessionOriginId || requestedTargetOrigins.length !== 2 || requestedTargetOrigins.indexOf(targetSessionOriginId) === -1) {
220
- return { ok: false, error: "A standalone target transfer requires exactly the target and moved sessions", code: "invalid", revision: prepared.record.revision };
221
- } else if (!uniqueMembership(prepared.record.views, targetRefs.refs, source.view.id)) {
222
- return { ok: false, error: "A standalone target session already belongs to a Split view", code: "membership", revision: prepared.record.revision };
223
- }
224
- if (target) { var orderedTarget = names.append(target.view, targetRefs.refs, input.liveSessions, input.sourceSessionOriginId);
225
- if (!orderedTarget) return { ok: false, error: "Target layout must preserve existing sessions and include the moved source exactly once", code: "invalid", revision: prepared.record.revision };
226
- targetRefs.refs = orderedTarget.refs; input.liveSessions = orderedTarget.sessions; }
227
- else { var orderedStandalone = names.targetFirst(targetRefs.refs, input.liveSessions, targetSessionOriginId, input.sourceSessionOriginId);
228
- if (!orderedStandalone) return { ok: false, error: "A standalone target transfer requires exactly the target and moved sessions", code: "invalid", revision: prepared.record.revision };
229
- targetRefs.refs = orderedStandalone.refs; input.liveSessions = orderedStandalone.sessions; }
230
- var targetOrigins = targetRefs.refs.map(function (ref) { return ref.originId; });
231
- if (!layout.validatePreset(input.layout) || !layout.validateTree(input.tree, targetOrigins).ok || targetOrigins.length > layout.MAX_SESSIONS) {
232
- return { ok: false, error: "Target layout is invalid or exceeds the eight-session limit", code: targetOrigins.length > layout.MAX_SESSIONS ? "limit" : "invalid", revision: prepared.record.revision };
233
- }
234
- if (typeof input.activeSessionOriginId !== "string" || targetOrigins.indexOf(input.activeSessionOriginId) === -1) {
235
- return { ok: false, error: "Active session must belong to the target Split view", code: "invalid", revision: prepared.record.revision };
236
- }
237
- if (!target) {
238
- var name = layout.validateName(input.name);
239
- if (!name.ok) return { ok: false, error: name.error, code: "invalid", revision: prepared.record.revision };
240
- }
241
- var nextSourceOrigins = sourceOrigins.filter(function (origin) { return origin !== input.sourceSessionOriginId; });
242
- source.view.tree = layout.removeLeaf(source.view.tree, input.sourceSessionOriginId);
243
- source.view.sessionRefs = source.view.sessionRefs.filter(function (ref) { return ref.originId !== input.sourceSessionOriginId; });
244
- if (source.view.activeSessionOriginId === input.sourceSessionOriginId && nextSourceOrigins.length) source.view.activeSessionOriginId = nextSourceOrigins[0];
245
- if (nextSourceOrigins.length < 2) prepared.record.views.splice(source.index, 1);
246
- if (nextSourceOrigins.length >= 2 && names.mode(source.view) === "automatic") source.view.name = names.automaticName(input.sourceLiveSessions.filter(function (session) { return session.sessionOriginId !== input.sourceSessionOriginId; }));
247
- if (target) {
248
- target.view.tree = clone(input.tree); target.view.layout = input.layout;
249
- target.view.activeSessionOriginId = input.activeSessionOriginId; target.view.sessionRefs = targetRefs.refs;
250
- if (names.mode(target.view) === "automatic") target.view.name = names.automaticName(input.liveSessions);
251
- } else {
252
- target = { index: -1, view: { id: crypto.randomUUID(), folderId: null, favorite: false, name: names.automaticName(input.liveSessions), nameMode: "automatic", revision: prepared.record.revision + 1,
253
- layout: input.layout, activeSessionOriginId: input.activeSessionOriginId, tree: clone(input.tree), sessionRefs: targetRefs.refs } };
254
- prepared.record.views.push(target.view);
255
- }
256
- var changed = [target.view];
257
- if (nextSourceOrigins.length >= 2) changed.unshift(source.view);
258
- return commit(ownerId, prepared.record, changed);
259
- }
260
- function removeOrigin(originId) {
261
- var project = projectRecord(document, false), changedOwners = [], failures = [];
262
- if (!project) return { owners: changedOwners, failures: failures };
263
- var keys = Object.keys(project.owners);
264
- for (var i = 0; i < keys.length; i++) {
265
- var source = project.owners[keys[i]], next = clone(source), changed = false, retained = [];
266
- for (var j = 0; j < next.views.length; j++) {
267
- var view = next.views[j], member = view.sessionRefs.some(function (ref) { return ref.originId === originId; });
268
- if (!member) { retained.push(view); continue; }
269
- changed = true;
270
- var tree = layout.removeLeaf(view.tree, originId), origins = view.sessionRefs.filter(function (ref) { return ref.originId !== originId; });
271
- if (origins.length < 2) continue;
272
- view.tree = tree;
273
- view.sessionRefs = origins;
274
- if (view.activeSessionOriginId === originId) view.activeSessionOriginId = origins[0].originId;
275
- retained.push(view);
276
- }
277
- if (!changed) continue;
278
- next.views = retained;
279
- next.revision++;
280
- next.views.forEach(function (view) { view.revision = next.revision; });
281
- try {
282
- var replacement = documentForOwner(next, source.ownerId);
283
- save(replacement);
284
- document = replacement;
285
- changedOwners.push(source.ownerId);
286
- } catch (error) { failures.push({ ownerId: source.ownerId, error: error.message || String(error) }); }
287
- }
288
- return { owners: changedOwners, failures: failures };
289
- }
290
- var organization = require('./project-split-view-organization').attachOrganization({ prepare: prepareRecord, commit: commit, state: state });
291
- return { organize: organization.organize, reconcileFolders: organization.reconcileFolders, state: state, create: create, update: update, rename: rename, separate: separate, move: move, removeOrigin: removeOrigin,
292
- get filePath() { return filePath; }, get loadError() { return loadError; } };
293
- }
294
8
  function attachProjectSplitViews(ctx) {
295
9
  var store = createSplitViewsStore({ sessionsDir: ctx.sm.sessionsDir, slug: ctx.slug, persist: ctx.persist });
296
10
  var users = ctx.usersModule;
297
11
  function actor(ws) {
298
- if (ctx.isMate || !ws || !ctx.clients.has(ws) || ws._clayPaneActivation || !ctx.canAccessProject(ws)) return null;
12
+ if (ctx.isMate || !ws || ws.readyState !== 1 || !ctx.clients.has(ws) || ws._clayPaneActivation || !ctx.canAccessProject(ws)) return null;
299
13
  if (users.isMultiUser()) {
300
14
  var user = ws._clayUser && users.findUserById(ws._clayUser.id);
301
15
  if (!user || !ctx.canAccessProject(ws)) return null;
@@ -320,7 +34,7 @@ function attachProjectSplitViews(ctx) {
320
34
  return { session: found, ref: { originId: found.sessionOriginId, ownerId: found.ownerId || null, linuxUser: linuxUser || null } };
321
35
  }
322
36
  function resolvedRefs(ws, origins) {
323
- if (!Array.isArray(origins) || origins.length < 2) return { error: "A Split view requires two to eight sessions", code: "invalid" };
37
+ if (!Array.isArray(origins)) return { error: "View session origins must be an array", code: "invalid" };
324
38
  if (origins.length > layout.MAX_SESSIONS) return { error: "A Split view can contain at most eight sessions", code: "limit" };
325
39
  if (new Set(origins).size !== origins.length || origins.some(function (origin) { return !layout.validOrigin(origin); })) return { error: "Split view session origins must be unique valid values", code: "invalid" };
326
40
  var refs = [], sessions = [];
@@ -353,7 +67,7 @@ function attachProjectSplitViews(ctx) {
353
67
  (resolved.ref.linuxUser || null) !== (stored.linuxUser || null)) return null;
354
68
  refs.push({ id: resolved.session.localId, sessionOriginId: resolved.session.sessionOriginId, title: resolved.session.title || "" });
355
69
  }
356
- var projected = names.project(view, refs); projected.tree = clone(view.tree); return projected;
70
+ var projected = names.project(view, refs); projected.tree = clone(view.tree); projected.workspace = workspace.normalized(view).workspace; return projected;
357
71
  }
358
72
  function folderIds(ownerId) {
359
73
  var accountId = users.isMultiUser() ? ownerId : 'default';
@@ -365,12 +79,12 @@ function attachProjectSplitViews(ctx) {
365
79
  var state = store.reconcileFolders(current.id, current.linuxUser, folderIds(current.id));
366
80
  if (!state.ok) return { owner: current, revision: state.revision, views: [], error: state.error };
367
81
  var views = state.views.map(function (view) { return visibleView(ws, view); }).filter(Boolean);
368
- return { owner: current, revision: state.revision, views: views };
82
+ return { owner: current, revision: state.revision, activeViewId: state.activeViewId || null, views: views };
369
83
  }
370
84
  function sendState(ws) {
371
85
  var state = stateFor(ws);
372
86
  if (!state) return false;
373
- ctx.sendTo(ws, { type: "project_split_views", projectSlug: ctx.slug, revision: state.revision, views: state.views, error: state.error || null });
87
+ ctx.sendTo(ws, { type: "project_split_views", projectSlug: ctx.slug, revision: state.revision, activeViewId: state.activeViewId || null, views: state.views, error: state.error || null });
374
88
  return true;
375
89
  }
376
90
  function broadcastOwner(ownerId) {
@@ -405,15 +119,21 @@ function attachProjectSplitViews(ctx) {
405
119
  function execute(action, input) {
406
120
  var current = currentActor(), result;
407
121
  if (!current) return { ok: false, error: "Split view access is no longer permitted", code: "forbidden" };
408
- input = input || {};
122
+ input = Object.assign({}, input || {});
123
+ var identity = require("./project-view-input").check(input, ctx.slug, current.id, input.workspace !== undefined);
124
+ if (!identity.ok) return identity;
409
125
  var currentState = store.state(current.id, current.linuxUser);
410
126
  if (!currentState.ok) return currentState;
411
127
  if (!Number.isInteger(input.expectedRevision) || input.expectedRevision !== currentState.revision) {
412
128
  return { ok: false, error: "Split view state changed. Reload and try again.", code: "conflict", revision: currentState.revision };
413
129
  }
130
+ var targetId = action === "move" ? input.sourceViewId : input.id;
131
+ var target = currentState.views.find(function (view) { return view.id === targetId; });
132
+ if (target && input.expectedViewRevision !== undefined && target.revision !== input.expectedViewRevision) return { ok: false, code: "conflict", error: "View revision changed. Reload and try again." };
414
133
  if (action === "create" || action === "update") {
415
134
  var refs = resolvedRefs(ws, input.sessionOrigins);
416
135
  if (!refs.refs) return refs;
136
+ if (input.workspace && !require('./project-view-pair').authorized(ctx, ws, input.workspace.linkedPair)) return { ok: false, code: 'pair', error: 'The exact linked Worker pair is unavailable.' };
417
137
  input.liveSessions = refs.sessions;
418
138
  if (action === "update") {
419
139
  var exact = verifyStoredView(ws, current, input.id);
@@ -437,6 +157,7 @@ function attachProjectSplitViews(ctx) {
437
157
  }
438
158
  var targetResolved = resolvedRefs(ws, input.targetSessionOrigins);
439
159
  if (!targetResolved.refs) return targetResolved;
160
+ if (input.targetWorkspace && !require('./project-view-pair').authorized(ctx, ws, input.targetWorkspace.linkedPair)) return { ok: false, code: 'pair', error: 'The exact linked Worker pair is unavailable.' };
440
161
  input.liveSessions = targetResolved.sessions;
441
162
  return store.move(current.id, current.linuxUser, input, sourceResolved.refs, targetResolved.refs);
442
163
  }
@@ -270,12 +270,13 @@ function attachUserMessage(ctx) {
270
270
  return true;
271
271
  }
272
272
  tm.attach(t.id, ws);
273
- send({ type: "term_list", terminals: tm.list() });
273
+ send({ type: "term_list", terminals: tm.list(true) });
274
274
  sendTo(ws, { type: "term_created", id: t.id });
275
275
  return true;
276
276
  }
277
277
 
278
278
  if (msg.type === "term_attach") {
279
+ if (require("./project-view-terminal-binding").attachSaved(ctx, ws, msg)) return true;
279
280
  if (msg.id) tm.attach(msg.id, ws);
280
281
  return true;
281
282
  }
@@ -300,7 +301,7 @@ function attachUserMessage(ctx) {
300
301
  if (msg.type === "term_close") {
301
302
  if (msg.id) {
302
303
  tm.close(msg.id);
303
- send({ type: "term_list", terminals: tm.list() });
304
+ send({ type: "term_list", terminals: tm.list(true) });
304
305
  // Remove closed terminal from context sources
305
306
  var _termSessionId = ws._clayActiveSession || null;
306
307
  var saved = loadContextSources(slug, _termSessionId);
@@ -317,7 +318,7 @@ function attachUserMessage(ctx) {
317
318
  if (msg.type === "term_rename") {
318
319
  if (msg.id && msg.title) {
319
320
  tm.rename(msg.id, msg.title);
320
- send({ type: "term_list", terminals: tm.list() });
321
+ send({ type: "term_list", terminals: tm.list(true) });
321
322
  }
322
323
  return true;
323
324
  }
@@ -163,7 +163,7 @@ function attachVendorLogin(ctx) {
163
163
  if (!flow || typeof flow.terminalId !== "number") return;
164
164
  flow.closing = true;
165
165
  try { tm.close(flow.terminalId); } catch (e) {}
166
- send({ type: "term_list", terminals: tm.list() });
166
+ send({ type: "term_list", terminals: tm.list(true) });
167
167
  }
168
168
 
169
169
  // Restart the vendor adapter so the next query spawns a process that reads
@@ -299,7 +299,7 @@ function attachVendorLogin(ctx) {
299
299
 
300
300
  flow.terminalId = terminal.id;
301
301
  if (!flow.browser) tm.attach(terminal.id, ws);
302
- send({ type: "term_list", terminals: tm.list() });
302
+ send({ type: "term_list", terminals: tm.list(true) });
303
303
  return flow;
304
304
  }
305
305
 
@@ -0,0 +1,11 @@
1
+ function check(input, slug, ownerId, extended, view) {
2
+ if (input.projectSlug !== undefined && input.projectSlug !== slug || input.accountId !== undefined && input.accountId !== ownerId ||
3
+ extended && (input.projectSlug !== slug || input.accountId !== ownerId)) {
4
+ return { ok: false, code: 'identity', error: 'View account or project identity does not match' };
5
+ }
6
+ if (view && (extended || input.expectedViewRevision !== undefined) && input.expectedViewRevision !== view.revision) {
7
+ return { ok: false, code: 'conflict', error: 'View revision changed. Reload and try again.' };
8
+ }
9
+ return { ok: true };
10
+ }
11
+ module.exports = { check: check };
@@ -0,0 +1,26 @@
1
+ // Pair presentation is UI state, never Worker membership or execution authority.
2
+ function valid(pair, value, view) {
3
+ if (!pair || Object.keys(pair).sort().join(',') !== 'driverOriginId,groupId,tree,workerOrigins' || typeof pair.groupId !== 'string' || !/^[A-Za-z0-9_-]{1,128}$/.test(pair.groupId) ||
4
+ !view.sessionRefs.some(function (r) { return r.originId === pair.driverOriginId; }) || !Array.isArray(pair.workerOrigins) || !pair.workerOrigins.length || pair.workerOrigins.length + view.sessionRefs.length > 8 || new Set(pair.workerOrigins).size !== pair.workerOrigins.length || pair.workerOrigins.indexOf(pair.driverOriginId) >= 0 || pair.workerOrigins.some(function (id) { return typeof id !== 'string' || !/^[A-Za-z0-9_-]{1,128}$/.test(id); })) return false;
5
+ var mapping = {}, expected = pair.workerOrigins.slice();
6
+ value.panes.forEach(function (p) { if (!p.open || p.placement !== 'docked') return; var id = p.kind === 'session' ? p.resource.originId : 'tool-' + (p.kind === 'workbench' ? p.resource.name : p.kind === 'file' ? 'file-' + p.instanceId : p.kind); mapping[id] = p.instanceId; expected.push(id); });
7
+ var count = 0, seen = [];
8
+ function walk(n) { if (!n || ++count > 47) return false; if (Object.keys(n).join(',') === 'id') { seen.push(n.id); return expected.indexOf(n.id) >= 0; } return Object.keys(n).sort().join(',') === 'axis,left,ratio,right' && ['horizontal','vertical'].indexOf(n.axis) >= 0 && Number.isFinite(n.ratio) && n.ratio >= .05 && n.ratio <= .95 && walk(n.left) && walk(n.right); }
9
+ function project(n) { if (!n) return null; if (n.id) return mapping[n.id] ? { id: mapping[n.id] } : null; var l = project(n.left), r = project(n.right); return !l ? r : !r ? l : { axis:n.axis,ratio:n.ratio,left:l,right:r }; }
10
+ function same(a,b) { return !a || !b ? a === b : a.id || b.id ? a.id === b.id : a.axis === b.axis && a.ratio === b.ratio && same(a.left,b.left) && same(a.right,b.right); }
11
+ return expected.length <= 24 && walk(pair.tree) && seen.length === expected.length && new Set(seen).size === seen.length && same(project(pair.tree),value.tree);
12
+ }
13
+ function authorized(ctx, ws, pair) {
14
+ if (!pair) return true;
15
+ if (typeof pair !== 'object' || !Array.isArray(pair.workerOrigins)) return false;
16
+ var driver;
17
+ ctx.sm.sessions.forEach(function (s) { if (s.sessionOriginId === pair.driverOriginId) driver = s; });
18
+ var group = driver && ctx.getViewPairGroup && ctx.getViewPairGroup(driver.localId);
19
+ // The registry's pair fields support legacy and current Worker lists.
20
+ var p = group && group.pair, ids = p && (p.workerIds || [p.workerId]);
21
+ return !!(driver && (driver.ownerId || null) === (ws._clayUser && ws._clayUser.id || null) && group && group.id === pair.groupId && p && p.driverId === driver.localId && ids.length === pair.workerOrigins.length && ids.every(function (id, i) {
22
+ var s = ctx.sm.sessions.get(id);
23
+ return s && (s.ownerId || null) === (driver.ownerId || null) && require('./session-provenance').isWorker(s) && s.sessionProvenance.parentSessionOriginId === driver.sessionOriginId && s.sessionOriginId === pair.workerOrigins[i] && ctx.canAccessSession(ws,s);
24
+ }));
25
+ }
26
+ module.exports = { valid: valid, authorized: authorized };