clay-server 5.0.2-beta.1 → 5.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.
@@ -12,9 +12,8 @@ function inside(base, target) {
12
12
  return relative !== ".." && !relative.startsWith(".." + path.sep) && !path.isAbsolute(relative);
13
13
  }
14
14
 
15
- // Scope is server-derived for each request. In personal and mapped OS-user
16
- // modes, the actual filesystem operation enforces read/write permissions.
17
- // Unmapped multi-user deployments retain the project and symlink boundary.
15
+ // Scope is server-derived for each request. File-viewer operations follow the
16
+ // executing OS identity within the account-derived project boundary, if any.
18
17
  function resolveFilePath(cwd, requested, scope) {
19
18
  if (typeof requested !== "string" || !requested || requested.indexOf("\0") !== -1) {
20
19
  throw failure("INVALID_PATH", "Invalid file path");
@@ -60,10 +60,22 @@ function attachRequestAccess(ctx) {
60
60
  return info;
61
61
  }
62
62
 
63
+ function isSoleAccount(ws) {
64
+ if (!users || typeof users.getAllUsers !== "function") return false;
65
+ try {
66
+ var accounts = users.getAllUsers();
67
+ var user = userFor(ws);
68
+ return !!(user && user.id && Array.isArray(accounts) && accounts.length === 1 &&
69
+ accounts[0] && accounts[0].id === user.id);
70
+ } catch (e) { return false; }
71
+ }
72
+
63
73
  function fileScope(ws) {
64
74
  if (!canUseFiles(ws)) throw fileFailure("FILE_FORBIDDEN", "File browser access is not permitted");
65
75
  var identity = osIdentity(ws);
66
- return { identity: identity, projectBound: isMultiUser() && !identity };
76
+ // Recheck the account roster so adding another user restores the boundary
77
+ // for shared installations without OS isolation, including active watches.
78
+ return { identity: identity, projectBound: !identity && isMultiUser() && !isSoleAccount(ws) };
67
79
  }
68
80
 
69
81
  return { permitMessage: permitMessage, canAccessProject: canAccessProject, hasPermission: hasPermission,
@@ -1696,6 +1696,16 @@
1696
1696
  padding: 0;
1697
1697
  }
1698
1698
 
1699
+ .file-viewer-html-preview {
1700
+ display: block;
1701
+ width: 100%;
1702
+ height: 100%;
1703
+ min-height: 320px;
1704
+ border: 0;
1705
+ background: #fff;
1706
+ color-scheme: light;
1707
+ }
1708
+
1699
1709
  .file-viewer-body pre {
1700
1710
  margin: 0;
1701
1711
  padding: 16px 20px;
@@ -13,6 +13,7 @@ import { initFileViewerTabs, openFileViewerTab, previewFileViewerTab, updateFile
13
13
  import { initFileBrowserContextMenu, downloadProjectFile } from './filebrowser-context-menu.js';
14
14
  import { registerClayFileLinkOpener } from './clay-file-links.js';
15
15
  import { isPlantUmlFile, renderPlantUmlPreview, cancelPlantUmlPreview } from './plantuml-preview.js';
16
+ import { isHtmlFile, renderHtmlPreview } from './html-preview.js';
16
17
 
17
18
  var ctx;
18
19
  var showDropHint = function () {};
@@ -43,7 +44,7 @@ export function initFileBrowser(_ctx) {
43
44
  onFocus: function(path) {
44
45
  claimRightWorkbench("file-viewer");
45
46
  ctx.fileViewerEl.classList.remove("hidden");
46
- if (path !== currentFilePath) requestFileContent(path);
47
+ if (path !== currentFilePath || (store.get("fileViewerIsHtml") && isRendered && !document.querySelector(".file-viewer-html-preview"))) requestFileContent(path);
47
48
  },
48
49
  onEmpty: function() { teardownFileViewer(); },
49
50
  });
@@ -194,10 +195,14 @@ export function initFileBrowser(_ctx) {
194
195
  downloadProjectFile(currentFilePath);
195
196
  });
196
197
 
197
- // Markdown render toggle
198
+ // File preview/source toggle
198
199
  document.getElementById("file-viewer-render").addEventListener("click", function () {
199
- if (currentContent == null || (!currentIsMarkdown && !currentIsSvg && !store.get("fileViewerIsPlantUml"))) return;
200
+ if (currentContent == null || (!currentIsMarkdown && !currentIsSvg && !store.get("fileViewerIsPlantUml") && !store.get("fileViewerIsHtml"))) return;
200
201
  isRendered = !isRendered;
202
+ if (store.get("fileViewerIsHtml")) {
203
+ renderHtmlBody();
204
+ return;
205
+ }
201
206
  if (store.get("fileViewerIsPlantUml")) {
202
207
  renderPlantUmlBody();
203
208
  return;
@@ -491,6 +496,8 @@ export function closeFileViewer() {
491
496
 
492
497
  function teardownFileViewer() {
493
498
  cancelPlantUmlPreview();
499
+ var htmlFrame = document.querySelector(".file-viewer-html-preview");
500
+ if (htmlFrame) htmlFrame.remove();
494
501
  if (currentFilePath && isFollowingMarkdown(currentFilePath)) cancelMarkdownFollow();
495
502
  sendUnwatch();
496
503
  inlineDiffActive = false;
@@ -528,7 +535,7 @@ export function resetFileBrowser() {
528
535
  isRendered = false;
529
536
  currentIsMarkdown = false;
530
537
  currentIsSvg = false;
531
- store.set({ fileViewerIsPlantUml: false });
538
+ store.set({ fileViewerIsPlantUml: false, fileViewerIsHtml: false });
532
539
  cancelPlantUmlPreview();
533
540
  historyVisible = false;
534
541
  currentHistoryEntries = [];
@@ -1165,7 +1172,7 @@ function showFileContent(msg, expectedRevision) {
1165
1172
  currentFilePath = msg.path;
1166
1173
  currentIsMarkdown = false;
1167
1174
  currentIsSvg = false;
1168
- store.set({ fileViewerIsPlantUml: false });
1175
+ store.set({ fileViewerIsPlantUml: false, fileViewerIsHtml: false });
1169
1176
  cancelPlantUmlPreview();
1170
1177
  if (!keepRenderState) isRendered = false;
1171
1178
  var requestedExt = msg.path.split(".").pop().toLowerCase();
@@ -1193,19 +1200,20 @@ function showFileContent(msg, expectedRevision) {
1193
1200
  lightweightPreview = (msg.size || 0) > FILE_RICH_PREVIEW_MAX_BYTES;
1194
1201
  currentIsMarkdown = !lightweightPreview && (ext === "md" || ext === "mdx");
1195
1202
  currentIsSvg = !lightweightPreview && ext === "svg";
1203
+ store.set({ fileViewerIsHtml: !lightweightPreview && isHtmlFile(msg.path) });
1196
1204
  store.set({ fileViewerIsPlantUml: !lightweightPreview && isPlantUmlFile(msg.path) });
1197
1205
  if (lightweightPreview) isRendered = false;
1198
1206
  if (pendingRenderedOpen && currentIsMarkdown) isRendered = true;
1199
1207
 
1200
- if (currentIsMarkdown || currentIsSvg || store.get("fileViewerIsPlantUml")) {
1208
+ if (currentIsMarkdown || currentIsSvg || store.get("fileViewerIsPlantUml") || store.get("fileViewerIsHtml")) {
1201
1209
  renderBtn.classList.remove("hidden");
1202
1210
  renderBtn.title = currentIsSvg ? "Show SVG source" : "Render markdown";
1203
- copyBtn.title = store.get("fileViewerIsPlantUml") ? "Copy PlantUML source" : currentIsSvg ? "Copy SVG source" : "Copy Markdown source";
1211
+ copyBtn.title = store.get("fileViewerIsHtml") ? "Copy HTML source" : store.get("fileViewerIsPlantUml") ? "Copy PlantUML source" : currentIsSvg ? "Copy SVG source" : "Copy Markdown source";
1204
1212
  } else {
1205
1213
  copyBtn.title = "Copy contents";
1206
1214
  }
1207
1215
 
1208
- // Markdown starts as source; SVG starts as a safe image preview.
1216
+ // Markdown starts as source; HTML, SVG and diagrams start as previews.
1209
1217
  if (lightweightPreview) {
1210
1218
  renderLargeTextFile(bodyEl, msg.content, msg.size);
1211
1219
  } else if (currentIsMarkdown) {
@@ -1214,6 +1222,9 @@ function showFileContent(msg, expectedRevision) {
1214
1222
  ? (previousContent == null ? "" : previousContent)
1215
1223
  : null;
1216
1224
  renderBody(transitionFrom, refreshSlideIndex);
1225
+ } else if (store.get("fileViewerIsHtml")) {
1226
+ if (!keepRenderState) isRendered = !store.get("pendingFileNavigation");
1227
+ renderHtmlBody();
1217
1228
  } else if (store.get("fileViewerIsPlantUml")) {
1218
1229
  if (!keepRenderState) isRendered = !store.get("pendingFileNavigation");
1219
1230
  renderPlantUmlBody();
@@ -1283,6 +1294,15 @@ function showFileContent(msg, expectedRevision) {
1283
1294
  }
1284
1295
  }
1285
1296
 
1297
+ function renderHtmlBody() {
1298
+ var bodyEl = document.getElementById("file-viewer-body");
1299
+ var renderBtn = document.getElementById("file-viewer-render");
1300
+ renderBtn.classList.toggle("active", isRendered);
1301
+ renderBtn.title = isRendered ? "Show HTML source" : "Show HTML preview";
1302
+ if (isRendered) renderHtmlPreview(bodyEl, currentContent, currentFilePath);
1303
+ else renderCodeWithLineNumbers(bodyEl, currentContent, "html");
1304
+ }
1305
+
1286
1306
  function renderPlantUmlBody() {
1287
1307
  var bodyEl = document.getElementById("file-viewer-body");
1288
1308
  var renderBtn = document.getElementById("file-viewer-render");
@@ -0,0 +1,19 @@
1
+ export function isHtmlFile(path) {
2
+ return /\.html?$/i.test(String(path || ""));
3
+ }
4
+
5
+ export function renderHtmlPreview(body, source, path) {
6
+ var frame = document.createElement("iframe");
7
+ frame.className = "file-viewer-html-preview";
8
+ frame.title = "HTML preview of " + path;
9
+ // Scripts can drive the preview, but cannot access Clay's document or cookies.
10
+ frame.setAttribute("sandbox", "allow-scripts");
11
+ frame.setAttribute("referrerpolicy", "no-referrer");
12
+ var doc = new DOMParser().parseFromString(source, "text/html");
13
+ var policy = doc.createElement("meta");
14
+ policy.httpEquiv = "Content-Security-Policy";
15
+ policy.content = "default-src 'none'; script-src 'unsafe-inline' https: http:; style-src 'unsafe-inline' https: http:; img-src data: blob: https: http:; font-src data: https: http:; media-src data: blob: https: http:; connect-src 'none'; frame-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'";
16
+ doc.head.prepend(policy);
17
+ frame.srcdoc = "<!doctype html>\n" + doc.documentElement.outerHTML;
18
+ body.replaceChildren(frame);
19
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clay-server",
3
- "version": "5.0.2-beta.1",
3
+ "version": "5.1.0-beta.1",
4
4
  "description": "Self-hosted team workspace for Claude Code and Codex. Multi-user, browser-based, with persistent AI mates.",
5
5
  "bin": {
6
6
  "clay-server": "./bin/cli.js"