collabmd 0.1.45 → 0.1.46

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 (190) hide show
  1. package/README.md +39 -19
  2. package/bin/collabmd.js +27 -1
  3. package/dist/client/assets/CodeMirrorEditor-UCMRAMCV-CYM5nxUi.js +1 -0
  4. package/dist/client/assets/browser-utils--H_DkyEF.js +1 -0
  5. package/dist/client/assets/chunk-ERSWUTIN-hHdvnhaY.js +41 -0
  6. package/dist/client/assets/comment-thread-store-CdcwrObc.js +1 -0
  7. package/dist/client/assets/comment-threads-9J6Em-_y.js +4 -0
  8. package/dist/client/assets/comment-ui-shared-BC4pt4w8.js +7 -0
  9. package/dist/client/assets/dist-B5HTH2BO.js +1 -0
  10. package/dist/client/assets/{dist-DhLvC8DS.js → dist-B5Jf1SLZ.js} +2 -2
  11. package/dist/client/assets/{dist-CnL7Y2sM.js → dist-BDt3rz7l.js} +1 -1
  12. package/dist/client/assets/dist-BEwK4csK.js +1 -0
  13. package/dist/client/assets/dist-BKW4PALF.js +1 -0
  14. package/dist/client/assets/dist-BWcHHOx-.js +1 -0
  15. package/dist/client/assets/{dist-CYZmbokb.js → dist-BalScOwZ.js} +9 -9
  16. package/dist/client/assets/dist-Bf8f4HmM.js +6 -0
  17. package/dist/client/assets/dist-BjHJDtyU.js +1 -0
  18. package/dist/client/assets/dist-Bm6pf9Bl.js +1 -0
  19. package/dist/client/assets/dist-BoVBdSjk.js +1 -0
  20. package/dist/client/assets/dist-CC5IrQMG.js +1 -0
  21. package/dist/client/assets/{dist-C11hHqv_.js → dist-CCP_AKPG.js} +12 -12
  22. package/dist/client/assets/dist-CZgOmxK6.js +1 -0
  23. package/dist/client/assets/{dist-D2AQ-76g.js → dist-Cqhv5LAk.js} +1 -1
  24. package/dist/client/assets/{dist-MvcV4190.js → dist-D-Z6FLem.js} +1 -1
  25. package/dist/client/assets/dist-DLMiIsNC.js +11 -0
  26. package/dist/client/assets/{dist-3bgCp7iY.js → dist-DZKu9Cs8.js} +1 -1
  27. package/dist/client/assets/dist-DaYdhcVs.js +1 -0
  28. package/dist/client/assets/dist-FWJsgGYI.js +1 -0
  29. package/dist/client/assets/dist-Kh2COra_.js +1 -0
  30. package/dist/client/assets/{dist-JYEDRrPT.js → dist-PDZ3bSqm.js} +1 -1
  31. package/dist/client/assets/{dist-Drp8uu5J.js → dist-TjMh8n1u.js} +1 -1
  32. package/dist/client/assets/{dist-DGb27Dl6.js → dist-dtAdgL39.js} +1 -1
  33. package/dist/client/assets/dist-jv4WdPnK.js +9 -0
  34. package/dist/client/assets/{dist-DK5Gs2wa.js → dist-rGBmqxC7.js} +1 -1
  35. package/dist/client/assets/{dist-DCzuX1AF.js → dist-shrl6K_0.js} +1 -1
  36. package/dist/client/assets/dist-tOaegDYS2.js +1 -0
  37. package/dist/client/assets/dist-ule2n6ak.js +1 -0
  38. package/dist/client/assets/dist-xElaxbia.js +1 -0
  39. package/dist/client/assets/{drawio-editor-ClE8jpoG.js → drawio-editor-FzY1cVw8.js} +1 -1
  40. package/dist/client/assets/drawioEditor-BjiKJqlc.js +2 -0
  41. package/dist/client/assets/editor-session-Cm9ekeNo.js +54 -0
  42. package/dist/client/assets/embedded-editor-base-CwTkk911.css +1 -0
  43. package/dist/client/assets/{preview-render-profile-BZzGSg6K.js → es-DXGV3RHd.js} +13 -13
  44. package/dist/client/assets/excalidraw-editor-B46zsTFI.js +9 -0
  45. package/dist/client/assets/{excalidraw-editor-CyGwOhLz.css → excalidraw-editor-Bt7gMSoh.css} +1 -1
  46. package/dist/client/assets/excalidraw-scene-BOben2gr.js +1 -0
  47. package/dist/client/assets/excalidrawEditor-CT2pHq7e.js +2 -0
  48. package/dist/client/assets/{export-page-CUfws2T-.js → export-page-A7NHawJY.js} +3 -3
  49. package/dist/client/assets/exportDocument-B2L8fe34.css +1 -0
  50. package/dist/client/assets/exportDocument-BaqcOAAW.js +2 -0
  51. package/dist/client/assets/file-icon-svg-CQAjlWjK.js +1 -0
  52. package/dist/client/assets/file-kind-BbqgGzpc.js +1 -0
  53. package/dist/client/assets/image-QXRYHUNP-SWEV8H0J.js +1 -0
  54. package/dist/client/assets/index-ClbjMBVt.js +2 -0
  55. package/dist/client/assets/index-Pum8ET-5.css +1 -0
  56. package/dist/client/assets/main-B3uPyqic.js +893 -0
  57. package/dist/client/assets/pdf-BL9Pr2D5.js +54 -0
  58. package/dist/client/assets/pdf.worker-CLesOks4.mjs +64857 -0
  59. package/dist/client/assets/pdf_viewer-FVkkThnz.js +551 -0
  60. package/dist/client/assets/{preview-render-compiler-BSaQO_p3.js → preview-render-compiler-BD-XhkS2.js} +2 -2
  61. package/dist/client/assets/preview-render-compiler-BKedaMxx.js +1 -0
  62. package/dist/client/assets/preview-render-profile-CE-5rVTC.js +1 -0
  63. package/dist/client/assets/{preview-render-worker-B42LWvrD.js → preview-render-worker-CiPqc9n6.js} +20 -20
  64. package/dist/client/assets/prod-Dy20X7eT.js +1 -0
  65. package/dist/client/assets/prod-hnwh7wRM.js +177 -0
  66. package/dist/client/assets/quick-switcher-controller-fqOYR7qR.js +12 -0
  67. package/dist/client/assets/runtime-config-BLrEKVO1.js +1 -0
  68. package/dist/client/assets/vault-api-client-_LpV1aIn.js +6 -0
  69. package/dist/client/drawio-editor.html +1 -1
  70. package/dist/client/excalidraw-editor.html +1 -1
  71. package/dist/client/export-document.html +2 -2
  72. package/dist/client/index.html +15 -3
  73. package/docker-compose.yml +42 -0
  74. package/package.json +17 -4
  75. package/scripts/local-structurizr-compose.mjs +101 -0
  76. package/src/client/app/index.html +13 -1
  77. package/src/client/application/app-shell/comments-feature.js +19 -10
  78. package/src/client/application/app-shell/git-feature.js +12 -0
  79. package/src/client/application/app-shell/presence-feature.js +2 -1
  80. package/src/client/application/app-shell/ui-feature-shell.js +45 -2
  81. package/src/client/application/app-shell/ui-feature-toolbar.js +2 -52
  82. package/src/client/application/app-shell/workspace-feature.js +5 -1
  83. package/src/client/application/app-shell-elements.js +2 -0
  84. package/src/client/application/pdf-preview-controller.js +992 -0
  85. package/src/client/application/structurizr-preview-controller.js +395 -0
  86. package/src/client/application/workspace-coordinator.js +25 -3
  87. package/src/client/application/workspace-preview-controller.js +91 -2
  88. package/src/client/bootstrap/collabmd-app-shell.js +31 -5
  89. package/src/client/browser-utils.js +44 -0
  90. package/src/client/domain/runtime-paths.js +26 -5
  91. package/src/client/domain/structurizr-language.js +99 -0
  92. package/src/client/domain/vault-paths.js +48 -0
  93. package/src/client/excalidraw-editor.js +833 -51
  94. package/src/client/infrastructure/browser-preferences-port.js +10 -0
  95. package/src/client/infrastructure/comment-thread-store.js +34 -13
  96. package/src/client/infrastructure/editor-session.js +10 -0
  97. package/src/client/infrastructure/editor-view-adapter.js +46 -2
  98. package/src/client/infrastructure/excalidraw-room-client.js +103 -14
  99. package/src/client/infrastructure/git-api-client.js +5 -0
  100. package/src/client/infrastructure/vault-api-client.js +26 -0
  101. package/src/client/presentation/comment-ui/comment-ui-shared.js +4 -0
  102. package/src/client/presentation/drawio-embed-controller.js +9 -102
  103. package/src/client/presentation/drawio-viewer.js +118 -0
  104. package/src/client/presentation/excalidraw-embed-controller.js +74 -8
  105. package/src/client/presentation/file-action-controller.js +102 -0
  106. package/src/client/presentation/file-explorer-view.js +10 -29
  107. package/src/client/presentation/file-icon-svg.js +40 -0
  108. package/src/client/presentation/file-tree-state.js +2 -0
  109. package/src/client/presentation/git-diff-view-controller.js +731 -3
  110. package/src/client/presentation/outline-controller.js +86 -0
  111. package/src/client/presentation/quick-switcher-controller.js +25 -13
  112. package/src/client/presentation/quick-switcher-text-search.js +1 -1
  113. package/src/client/styles/components/navigation.css +11 -1
  114. package/src/client/styles/features/diagram-preview.css +76 -0
  115. package/src/client/styles/features/diff-special.css +309 -0
  116. package/src/client/styles/foundation/themes.css +6 -0
  117. package/src/client/styles/layout/shell.css +358 -1
  118. package/src/client/styles/layout/view-modes.css +1 -0
  119. package/src/client/styles/style.css +1 -0
  120. package/src/client/styles/surfaces/excalidraw-editor.css +294 -0
  121. package/src/domain/comment-threads.js +63 -5
  122. package/src/domain/excalidraw-room-codec.js +6 -4
  123. package/src/domain/file-kind.js +35 -3
  124. package/src/server/config/env.js +79 -0
  125. package/src/server/create-app-server.js +12 -0
  126. package/src/server/domain/bases/base-definition.js +1 -1
  127. package/src/server/domain/bases/base-expression-runtime.js +1 -4
  128. package/src/server/domain/bases/base-index-snapshot-store.js +1 -4
  129. package/src/server/domain/bases/base-transform.js +1 -4
  130. package/src/server/domain/collaboration/collaboration-room.js +55 -21
  131. package/src/server/domain/comment-overview.js +15 -5
  132. package/src/server/domain/ripgrep-search-service.js +4 -0
  133. package/src/server/infrastructure/git/diff-service.js +5 -0
  134. package/src/server/infrastructure/git/git-service.js +21 -0
  135. package/src/server/infrastructure/git/history-service.js +54 -0
  136. package/src/server/infrastructure/git/image-mime.js +14 -0
  137. package/src/server/infrastructure/git/untracked-files.js +28 -1
  138. package/src/server/infrastructure/http/create-git-api-query-handler.js +31 -1
  139. package/src/server/infrastructure/http/create-request-handler.js +16 -23
  140. package/src/server/infrastructure/http/create-static-handler.js +5 -2
  141. package/src/server/infrastructure/http/create-structurizr-api-handler.js +145 -0
  142. package/src/server/infrastructure/http/create-vault-api-command-handler.js +66 -4
  143. package/src/server/infrastructure/http/create-vault-api-query-handler.js +51 -3
  144. package/src/server/infrastructure/http/http-request-helpers.js +22 -0
  145. package/src/server/infrastructure/persistence/path-utils.js +1 -1
  146. package/src/server/infrastructure/persistence/vault-file-store.js +3 -0
  147. package/src/server/infrastructure/structurizr/structurizr-embed.css +43 -0
  148. package/src/server/infrastructure/structurizr/structurizr-workspace-service.js +374 -0
  149. package/src/server/infrastructure/websocket/attach-collaboration-gateway.js +4 -22
  150. package/dist/client/assets/CodeMirrorEditor-UCMRAMCV-CbSISC1w.js +0 -1
  151. package/dist/client/assets/browser-utils-CKtI_rZl.js +0 -1
  152. package/dist/client/assets/chunk-ERSWUTIN-ME9RVNel.js +0 -41
  153. package/dist/client/assets/comment-threads-DDegmaFO.js +0 -4
  154. package/dist/client/assets/dist-BH87JoHx.js +0 -11
  155. package/dist/client/assets/dist-BIkPRZO7.js +0 -6
  156. package/dist/client/assets/dist-BqCsFpx4.js +0 -1
  157. package/dist/client/assets/dist-C2JHCt8U.js +0 -1
  158. package/dist/client/assets/dist-CZjTtmTu.js +0 -1
  159. package/dist/client/assets/dist-Cig4Ov_X.js +0 -1
  160. package/dist/client/assets/dist-CqQEC8gR.js +0 -1
  161. package/dist/client/assets/dist-D4Rzqod1.js +0 -1
  162. package/dist/client/assets/dist-D5sn-mwk.js +0 -9
  163. package/dist/client/assets/dist-D7G-n87v.js +0 -1
  164. package/dist/client/assets/dist-DRqOrNgN.js +0 -1
  165. package/dist/client/assets/dist-DetkMS_4.js +0 -1
  166. package/dist/client/assets/dist-Dp6FVkQ1.js +0 -1
  167. package/dist/client/assets/dist-DtbjNB28.js +0 -1
  168. package/dist/client/assets/dist-DyrYcvpo2.js +0 -1
  169. package/dist/client/assets/dist-GrygD2zE.js +0 -1
  170. package/dist/client/assets/dist-HmEpWUow.js +0 -1
  171. package/dist/client/assets/dist-RTa81Km7.js +0 -1
  172. package/dist/client/assets/drawioEditor-CXaTM9Ds.js +0 -2
  173. package/dist/client/assets/editor-session-GTwZS6Df.js +0 -16
  174. package/dist/client/assets/embedded-editor-base-CiNEc8vy.css +0 -1
  175. package/dist/client/assets/excalidraw-editor-BZVM81BO.js +0 -9
  176. package/dist/client/assets/excalidraw-scene-C8n9CHsK.js +0 -176
  177. package/dist/client/assets/excalidrawEditor-DxJanCze.js +0 -2
  178. package/dist/client/assets/exportDocument-BCuxqXDY.js +0 -2
  179. package/dist/client/assets/exportDocument-DV-OX74E.css +0 -1
  180. package/dist/client/assets/file-kind-BKPOIRPE.js +0 -1
  181. package/dist/client/assets/image-QXRYHUNP-lfTHE3bb.js +0 -1
  182. package/dist/client/assets/index-C3YnUgMD.css +0 -1
  183. package/dist/client/assets/index-D2wXraJ1.js +0 -2
  184. package/dist/client/assets/main-DtiGz7uI.js +0 -795
  185. package/dist/client/assets/preview-render-compiler-csGM5ZFz.js +0 -1
  186. package/dist/client/assets/quick-switcher-controller-shW_6vPY.js +0 -12
  187. package/dist/client/assets/room-CQLoqtMo.js +0 -1
  188. package/dist/client/assets/runtime-config-DSGmGE9V.js +0 -1
  189. package/dist/client/assets/vault-api-client-C6bcGwy-.js +0 -5
  190. package/src/server/domain/collaboration/room-persistence-controller.js +0 -80
@@ -3,7 +3,7 @@ import { readdir } from 'node:fs/promises';
3
3
  import { basename } from 'node:path';
4
4
  import { join } from 'node:path';
5
5
 
6
- import { isImageAttachmentFilePath } from '../../../domain/file-kind.js';
6
+ import { getVaultFileKind, isImageAttachmentFilePath } from '../../../domain/file-kind.js';
7
7
  import { isIgnoredVaultEntry } from '../persistence/path-utils.js';
8
8
  import { parseJsonBody } from './request-body.js';
9
9
  import {
@@ -19,6 +19,36 @@ const SVG_ATTACHMENT_CSP = "default-src 'none'; img-src 'self' data: blob:; styl
19
19
  const DEFAULT_MAX_ARCHIVE_ENTRIES = 10_000;
20
20
  const DEFAULT_MAX_DOWNLOAD_FILE_BYTES = 268_435_456;
21
21
 
22
+ function createSearchCancellation(req, res) {
23
+ const controller = new AbortController();
24
+
25
+ const abort = () => {
26
+ if (controller.signal.aborted || res.writableEnded || res.writableFinished) {
27
+ return;
28
+ }
29
+
30
+ controller.abort();
31
+ };
32
+ const handleRequestClose = () => {
33
+ if (!req.complete) {
34
+ abort();
35
+ }
36
+ };
37
+
38
+ req.once('aborted', abort);
39
+ req.once('close', handleRequestClose);
40
+ res.once('close', abort);
41
+
42
+ return {
43
+ cleanup() {
44
+ req.off('aborted', abort);
45
+ req.off('close', handleRequestClose);
46
+ res.off('close', abort);
47
+ },
48
+ signal: controller.signal,
49
+ };
50
+ }
51
+
22
52
  function createAttachmentHeaders(attachment) {
23
53
  const fileName = basename(String(attachment?.path ?? 'attachment'));
24
54
  const headers = {
@@ -35,10 +65,10 @@ function createAttachmentHeaders(attachment) {
35
65
  return headers;
36
66
  }
37
67
 
38
- function createDownloadHeaders(fileName, contentType) {
68
+ function createDownloadHeaders(fileName, contentType, { inline = false } = {}) {
39
69
  return {
40
70
  'Cache-Control': 'no-store',
41
- 'Content-Disposition': `attachment; filename="${createSafeAsciiFilename(fileName)}"; filename*=UTF-8''${encodeContentDispositionFilename(fileName)}`,
71
+ 'Content-Disposition': `${inline ? 'inline' : 'attachment'}; filename="${createSafeAsciiFilename(fileName)}"; filename*=UTF-8''${encodeContentDispositionFilename(fileName)}`,
42
72
  'Content-Type': contentType,
43
73
  'X-Content-Type-Options': 'nosniff',
44
74
  };
@@ -304,6 +334,7 @@ async function handleFileDownload(req, res, requestUrl, { config, vaultFileStore
304
334
  headers: createDownloadHeaders(
305
335
  basename(String(download.path ?? 'download')),
306
336
  download.mimeType || 'application/octet-stream',
337
+ { inline: getVaultFileKind(download.path) === 'pdf' },
307
338
  ),
308
339
  statusCode: 200,
309
340
  stream: download.stream,
@@ -418,13 +449,28 @@ async function handleSearch(req, res, requestUrl, { searchService }) {
418
449
  return;
419
450
  }
420
451
 
452
+ const cancellation = createSearchCancellation(req, res);
453
+
421
454
  try {
422
455
  const result = await searchService.search({
423
456
  limit: requestUrl.searchParams.get('limit') || '',
424
457
  query: requestUrl.searchParams.get('q') || '',
458
+ signal: cancellation.signal,
425
459
  });
460
+ if (cancellation.signal.aborted || res.destroyed || res.writableEnded) {
461
+ return;
462
+ }
463
+
426
464
  jsonResponse(req, res, 200, result);
427
465
  } catch (error) {
466
+ if (
467
+ cancellation.signal.aborted
468
+ || res.destroyed
469
+ || res.writableEnded
470
+ ) {
471
+ return;
472
+ }
473
+
428
474
  const statusCode = Number(error?.statusCode) || 500;
429
475
  console.error('[api] Failed to search vault:', error.message);
430
476
  jsonResponse(req, res, statusCode, {
@@ -435,6 +481,8 @@ async function handleSearch(req, res, requestUrl, { searchService }) {
435
481
  backend: 'ripgrep',
436
482
  },
437
483
  });
484
+ } finally {
485
+ cancellation.cleanup();
438
486
  }
439
487
  }
440
488
 
@@ -1,6 +1,28 @@
1
1
  import { getRequestErrorStatusCode } from './http-errors.js';
2
2
  import { jsonResponse } from './http-response.js';
3
3
 
4
+ export function stripBasePath(pathname, basePath) {
5
+ if (!basePath) {
6
+ return pathname;
7
+ }
8
+
9
+ if (pathname === basePath) {
10
+ return '/';
11
+ }
12
+
13
+ if (pathname.startsWith(`${basePath}/`)) {
14
+ return pathname.slice(basePath.length) || '/';
15
+ }
16
+
17
+ return pathname;
18
+ }
19
+
20
+ export function createRequestUrlWithPathname(requestUrl, pathname) {
21
+ const nextUrl = new URL(requestUrl.toString());
22
+ nextUrl.pathname = pathname || '/';
23
+ return nextUrl;
24
+ }
25
+
4
26
  export function readRequestId(req) {
5
27
  const value = String(req.headers['x-collabmd-request-id'] || '').trim();
6
28
  return value ? value.slice(0, 120) : null;
@@ -3,7 +3,7 @@ import { isAbsolute, normalize, relative, resolve } from 'path';
3
3
  import { isVaultFilePath } from '../../../domain/file-kind.js';
4
4
 
5
5
  export const IGNORED_DIRECTORIES = new Set(['.git', '.obsidian', '.trash', 'node_modules', '.DS_Store']);
6
- export const VAULT_FILE_PATH_REQUIREMENT = '.md, .base, .excalidraw, .drawio, .mmd, .mermaid, .puml, .plantuml, .png, .jpg, .jpeg, .webp, .gif, or .svg';
6
+ export const VAULT_FILE_PATH_REQUIREMENT = '.md, .markdown, .mdx, .base, .excalidraw, .drawio, .mmd, .mermaid, .puml, .plantuml, .dsl, .png, .jpg, .jpeg, .webp, .gif, or .svg';
7
7
  export const INVALID_VAULT_FILE_PATH_ERROR = `Invalid file path — must end in ${VAULT_FILE_PATH_REQUIREMENT}`;
8
8
  export const INVALID_DIRECTORY_PATH_ERROR = 'Invalid directory path';
9
9
 
@@ -31,6 +31,7 @@ const EDITABLE_VAULT_CONTENT_PATH_ERRORS = {
31
31
  markdown: 'Invalid file path',
32
32
  mermaid: 'Invalid file path — must end in .mmd or .mermaid',
33
33
  plantuml: 'Invalid file path — must end in .puml or .plantuml',
34
+ structurizr: 'Invalid file path — must end in .dsl',
34
35
  };
35
36
 
36
37
  function getEditableVaultContentKind(filePath) {
@@ -67,7 +68,9 @@ const TEXT_FILE_MIME_TYPES = Object.freeze({
67
68
  excalidraw: 'application/json; charset=utf-8',
68
69
  markdown: 'text/markdown; charset=utf-8',
69
70
  mermaid: 'text/plain; charset=utf-8',
71
+ pdf: 'application/pdf',
70
72
  plantuml: 'text/plain; charset=utf-8',
73
+ structurizr: 'text/plain; charset=utf-8',
71
74
  });
72
75
 
73
76
  function createTransactionalPath(targetPath, label) {
@@ -0,0 +1,43 @@
1
+ html, body, #content {
2
+ height: 100% !important;
3
+ min-height: 0 !important;
4
+ }
5
+
6
+ body {
7
+ margin: 0 !important;
8
+ overflow: hidden !important;
9
+ background: transparent !important;
10
+ }
11
+
12
+ #diagramControls,
13
+ #diagramNavigationPanel,
14
+ #embeddedControls {
15
+ display: none !important;
16
+ }
17
+
18
+ #diagramControls + .row,
19
+ #diagramControls + .row > .col-10,
20
+ #diagramControls + .row #diagram,
21
+ #diagram-viewport {
22
+ height: 100% !important;
23
+ min-height: 0 !important;
24
+ }
25
+
26
+ #diagramControls + .row > .col-10 {
27
+ width: 100% !important;
28
+ }
29
+
30
+ #diagram {
31
+ background: transparent !important;
32
+ }
33
+
34
+ #diagram-viewport {
35
+ overflow: auto !important;
36
+ touch-action: pan-x pan-y !important;
37
+ overscroll-behavior: contain !important;
38
+ background: transparent !important;
39
+ }
40
+
41
+ #diagram-canvas {
42
+ margin: 0 auto !important;
43
+ }
@@ -0,0 +1,374 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { mkdir, readFile, readdir, realpath, rm, stat, utimes, writeFile } from 'node:fs/promises';
3
+ import { basename, dirname, join, resolve } from 'node:path';
4
+
5
+ import {
6
+ isStructurizrFilePath,
7
+ isVaultFilePath,
8
+ } from '../../../domain/file-kind.js';
9
+ import { sanitizeVaultPath } from '../persistence/path-utils.js';
10
+
11
+ const MANIFEST_FILE_NAME = '.collabmd-manifest.json';
12
+ const WORKSPACE_DSL_FILE_NAME = 'workspace.dsl';
13
+ const WORKSPACE_JSON_FILE_NAME = 'workspace.json';
14
+ const SIDECAR_TIMEOUT_MS = 15_000;
15
+
16
+ function createServiceError(message, statusCode, code) {
17
+ const error = new Error(message);
18
+ error.statusCode = statusCode;
19
+ error.requestCode = code;
20
+ return error;
21
+ }
22
+
23
+ function normalizeWorkspaceRootPath(pathValue) {
24
+ const normalized = String(pathValue ?? '').trim().replace(/\\/g, '/');
25
+ if (!normalized || normalized.startsWith('/') || normalized.split('/').some((segment) => !segment || segment === '.' || segment === '..')) {
26
+ return '';
27
+ }
28
+
29
+ return normalized;
30
+ }
31
+
32
+ function validateStructurizrDsl(source, { trustedExecutableDsl = false } = {}) {
33
+ if (trustedExecutableDsl) {
34
+ return;
35
+ }
36
+
37
+ const text = String(source ?? '');
38
+ if (/^\s*!script\b/im.test(text) || /^\s*!plugin\b/im.test(text)) {
39
+ throw createServiceError(
40
+ 'Executable Structurizr DSL is disabled for this workspace.',
41
+ 422,
42
+ 'STRUCTURIZR_EXECUTABLE_DSL_DISABLED',
43
+ );
44
+ }
45
+
46
+ if (/^\s*!include\s+<?(?:https?|file):/im.test(text)) {
47
+ throw createServiceError(
48
+ 'Remote Structurizr DSL includes are disabled for this workspace.',
49
+ 422,
50
+ 'STRUCTURIZR_REMOTE_INCLUDE_DISABLED',
51
+ );
52
+ }
53
+ }
54
+
55
+ function isMirrorFileAllowed(relativePath) {
56
+ return basename(relativePath) === WORKSPACE_JSON_FILE_NAME || isVaultFilePath(relativePath);
57
+ }
58
+
59
+ async function collectWorkspaceFiles(directoryPath, prefix = '') {
60
+ const files = new Map();
61
+ const entries = (await readdir(directoryPath, { withFileTypes: true }))
62
+ .sort((left, right) => left.name.localeCompare(right.name, undefined, { sensitivity: 'base' }));
63
+
64
+ for (const entry of entries) {
65
+ if (entry.name.startsWith('.') || entry.name === 'node_modules' || entry.name === '.git') {
66
+ continue;
67
+ }
68
+
69
+ const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
70
+ const absolutePath = join(directoryPath, entry.name);
71
+ if (entry.isDirectory()) {
72
+ const nested = await collectWorkspaceFiles(absolutePath, relativePath);
73
+ nested.forEach((content, pathValue) => files.set(pathValue, content));
74
+ continue;
75
+ }
76
+
77
+ if (!entry.isFile() || !isMirrorFileAllowed(relativePath)) {
78
+ continue;
79
+ }
80
+
81
+ files.set(relativePath, await readFile(absolutePath));
82
+ }
83
+
84
+ return files;
85
+ }
86
+
87
+ async function collectMirrorPaths(directoryPath, prefix = '') {
88
+ let entries;
89
+ try {
90
+ entries = await readdir(directoryPath, { withFileTypes: true });
91
+ } catch (error) {
92
+ if (error.code === 'ENOENT') {
93
+ return [];
94
+ }
95
+ throw error;
96
+ }
97
+
98
+ const paths = [];
99
+ for (const entry of entries) {
100
+ if (entry.name === '.structurizr') {
101
+ continue;
102
+ }
103
+
104
+ const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
105
+ const absolutePath = join(directoryPath, entry.name);
106
+ if (entry.isDirectory()) {
107
+ paths.push(...await collectMirrorPaths(absolutePath, relativePath));
108
+ } else if (entry.isFile()) {
109
+ paths.push(relativePath);
110
+ }
111
+ }
112
+
113
+ return paths;
114
+ }
115
+
116
+ async function readManifest(manifestPath) {
117
+ try {
118
+ const parsed = JSON.parse(await readFile(manifestPath, 'utf8'));
119
+ return Array.isArray(parsed?.paths) ? parsed.paths : [];
120
+ } catch (error) {
121
+ if (error.code === 'ENOENT' || error instanceof SyntaxError) {
122
+ return [];
123
+ }
124
+ throw error;
125
+ }
126
+ }
127
+
128
+ async function writeMirrorFiles(mirrorDir, files) {
129
+ await Promise.all(Array.from(files, async ([relativePath, content]) => {
130
+ const targetPath = join(mirrorDir, relativePath);
131
+ await mkdir(dirname(targetPath), { recursive: true });
132
+ await writeFile(targetPath, content);
133
+ }));
134
+ }
135
+
136
+ async function removeMirrorPaths(mirrorDir, paths) {
137
+ await Promise.all(paths.map((relativePath) => rm(join(mirrorDir, relativePath), {
138
+ force: true,
139
+ recursive: true,
140
+ })));
141
+ }
142
+
143
+ async function snapshotMirror(mirrorDir) {
144
+ const paths = await collectMirrorPaths(mirrorDir);
145
+ const files = new Map();
146
+ await Promise.all(paths.map(async (relativePath) => {
147
+ files.set(relativePath, await readFile(join(mirrorDir, relativePath)));
148
+ }));
149
+ return files;
150
+ }
151
+
152
+ async function restoreMirror(mirrorDir, snapshot) {
153
+ await removeMirrorPaths(mirrorDir, await collectMirrorPaths(mirrorDir));
154
+ await writeMirrorFiles(mirrorDir, snapshot);
155
+ }
156
+
157
+ function hashFiles(files) {
158
+ const hash = createHash('sha256');
159
+ Array.from(files.keys()).sort().forEach((pathValue) => {
160
+ hash.update(pathValue);
161
+ hash.update('\0');
162
+ hash.update(files.get(pathValue));
163
+ hash.update('\0');
164
+ });
165
+ return hash.digest('hex');
166
+ }
167
+
168
+ export class StructurizrWorkspaceService {
169
+ constructor({
170
+ mirrorDir,
171
+ serverUrl = '',
172
+ trustedExecutableDsl = false,
173
+ vaultDir,
174
+ }) {
175
+ this.mirrorDir = resolve(mirrorDir);
176
+ this.serverUrl = String(serverUrl ?? '').trim().replace(/\/+$/u, '');
177
+ this.trustedExecutableDsl = Boolean(trustedExecutableDsl);
178
+ this.vaultDir = resolve(vaultDir);
179
+ this.syncPromise = Promise.resolve();
180
+ }
181
+
182
+ isEnabled() {
183
+ return Boolean(this.serverUrl);
184
+ }
185
+
186
+ isProxyPath(pathname) {
187
+ return this.isEnabled() && (
188
+ pathname === '/workspace/1'
189
+ || pathname.startsWith('/workspace/1/')
190
+ || pathname.startsWith('/static/')
191
+ || pathname === '/api/workspace/1'
192
+ || pathname.startsWith('/api/workspace/1/')
193
+ );
194
+ }
195
+
196
+ async sync({ content, rootPath }) {
197
+ if (!this.isEnabled()) {
198
+ throw createServiceError('Structurizr renderer is not configured.', 503, 'STRUCTURIZR_NOT_CONFIGURED');
199
+ }
200
+
201
+ const run = async () => this.syncWorkspace({ content, rootPath });
202
+ this.syncPromise = this.syncPromise.catch(() => {}).then(run);
203
+ return this.syncPromise;
204
+ }
205
+
206
+ async syncWorkspace({ content, rootPath }) {
207
+ const normalizedRootPath = normalizeWorkspaceRootPath(rootPath);
208
+ if (!isStructurizrFilePath(normalizedRootPath)) {
209
+ throw createServiceError('Structurizr previews require a workspace.dsl root file.', 400, 'STRUCTURIZR_ROOT_REQUIRED');
210
+ }
211
+
212
+ const rootAbsolutePath = sanitizeVaultPath(this.vaultDir, normalizedRootPath);
213
+ if (!rootAbsolutePath) {
214
+ throw createServiceError('Invalid Structurizr workspace path.', 400, 'STRUCTURIZR_ROOT_INVALID');
215
+ }
216
+
217
+ let rootInfo;
218
+ try {
219
+ rootInfo = await stat(rootAbsolutePath);
220
+ } catch (error) {
221
+ if (error.code === 'ENOENT') {
222
+ throw createServiceError('Structurizr workspace not found.', 404, 'STRUCTURIZR_ROOT_NOT_FOUND');
223
+ }
224
+ throw error;
225
+ }
226
+ if (!rootInfo.isFile()) {
227
+ throw createServiceError('Structurizr workspace root is not a file.', 400, 'STRUCTURIZR_ROOT_INVALID');
228
+ }
229
+
230
+ const [vaultRealPath, rootRealPath] = await Promise.all([
231
+ realpath(this.vaultDir),
232
+ realpath(rootAbsolutePath),
233
+ ]);
234
+ if (rootRealPath !== vaultRealPath && !rootRealPath.startsWith(`${vaultRealPath}/`)) {
235
+ throw createServiceError('Structurizr workspace must remain inside the vault.', 400, 'STRUCTURIZR_ROOT_INVALID');
236
+ }
237
+
238
+ const sourceDirectory = dirname(rootRealPath);
239
+ const files = await collectWorkspaceFiles(sourceDirectory);
240
+ const sourceRootFileName = basename(normalizedRootPath);
241
+ const rootSource = typeof content === 'string'
242
+ ? content
243
+ : files.get(sourceRootFileName)?.toString('utf8');
244
+ if (typeof rootSource !== 'string') {
245
+ throw createServiceError('Structurizr workspace source is unavailable.', 400, 'STRUCTURIZR_ROOT_UNREADABLE');
246
+ }
247
+
248
+ files.delete(sourceRootFileName);
249
+ files.set(WORKSPACE_DSL_FILE_NAME, Buffer.from(rootSource, 'utf8'));
250
+ for (const [pathValue, fileContent] of files) {
251
+ if (pathValue.toLowerCase().endsWith('.dsl')) {
252
+ validateStructurizrDsl(fileContent.toString('utf8'), {
253
+ trustedExecutableDsl: this.trustedExecutableDsl,
254
+ });
255
+ }
256
+ }
257
+
258
+ await mkdir(this.mirrorDir, { recursive: true });
259
+ const snapshot = await snapshotMirror(this.mirrorDir);
260
+ const previousManifest = await readManifest(join(this.mirrorDir, MANIFEST_FILE_NAME));
261
+ const sourcePaths = Array.from(files.keys());
262
+ const stalePaths = previousManifest.filter((pathValue) => !files.has(pathValue));
263
+
264
+ try {
265
+ await removeMirrorPaths(this.mirrorDir, stalePaths);
266
+ await writeMirrorFiles(this.mirrorDir, files);
267
+ // Local caches workspace.json; an old mtime makes it parse the DSL without a delete race.
268
+ const workspaceJsonPath = join(this.mirrorDir, WORKSPACE_JSON_FILE_NAME);
269
+ try {
270
+ await utimes(workspaceJsonPath, new Date(0), new Date(0));
271
+ } catch (error) {
272
+ if (error.code !== 'ENOENT') {
273
+ throw error;
274
+ }
275
+ }
276
+ await this.validateMirror();
277
+ if (files.has(WORKSPACE_JSON_FILE_NAME)) {
278
+ await writeMirrorFiles(this.mirrorDir, new Map([
279
+ [WORKSPACE_JSON_FILE_NAME, files.get(WORKSPACE_JSON_FILE_NAME)],
280
+ ]));
281
+ }
282
+ await writeFile(
283
+ join(this.mirrorDir, MANIFEST_FILE_NAME),
284
+ `${JSON.stringify({ paths: sourcePaths }, null, 2)}\n`,
285
+ 'utf8',
286
+ );
287
+ } catch (error) {
288
+ await restoreMirror(this.mirrorDir, snapshot);
289
+ throw error;
290
+ }
291
+
292
+ return {
293
+ ok: true,
294
+ rootPath: normalizedRootPath,
295
+ version: hashFiles(files),
296
+ };
297
+ }
298
+
299
+ async validateMirror() {
300
+ for (let attempt = 0; attempt < 8; attempt += 1) {
301
+ let response;
302
+ try {
303
+ response = await fetch(`${this.serverUrl}/api/workspace/1`, {
304
+ headers: { Accept: 'application/json' },
305
+ signal: AbortSignal.timeout(SIDECAR_TIMEOUT_MS),
306
+ });
307
+ } catch (error) {
308
+ throw createServiceError(
309
+ `Structurizr renderer is unavailable: ${error.message}`,
310
+ 503,
311
+ 'STRUCTURIZR_UNAVAILABLE',
312
+ );
313
+ }
314
+
315
+ const payload = await response.json().catch(() => ({}));
316
+ if (response.ok && payload.success !== false) {
317
+ return;
318
+ }
319
+
320
+ const message = String(payload.message || payload.error || `Structurizr renderer returned HTTP ${response.status}`)
321
+ .replace(/^com\.structurizr\.dsl\.StructurizrDslParserException:\s*/u, '');
322
+ const isTransientStartupFailure = response.status >= 500
323
+ && /(?:No workspace|FileNotFoundException|workspace\.json)/iu.test(message);
324
+ if (isTransientStartupFailure && attempt < 7) {
325
+ await new Promise((resolvePromise) => setTimeout(resolvePromise, 250));
326
+ continue;
327
+ }
328
+
329
+ throw createServiceError(
330
+ message,
331
+ response.status >= 400 && response.status < 500 ? 422 : 502,
332
+ response.status >= 400 && response.status < 500
333
+ ? 'STRUCTURIZR_DSL_INVALID'
334
+ : 'STRUCTURIZR_VALIDATION_FAILED',
335
+ );
336
+ }
337
+ }
338
+
339
+ async proxy(requestUrl, { method = 'GET', headers = {} } = {}) {
340
+ if (!this.isProxyPath(requestUrl.pathname)) {
341
+ return null;
342
+ }
343
+
344
+ const upstreamUrl = `${this.serverUrl}${requestUrl.pathname}${requestUrl.search}`;
345
+ let response;
346
+ try {
347
+ const proxyHeaders = {
348
+ Accept: headers.accept || '*/*',
349
+ };
350
+ if (headers.range) {
351
+ proxyHeaders.Range = headers.range;
352
+ }
353
+ response = await fetch(upstreamUrl, {
354
+ headers: proxyHeaders,
355
+ method,
356
+ redirect: 'follow',
357
+ signal: AbortSignal.timeout(SIDECAR_TIMEOUT_MS),
358
+ });
359
+ } catch (error) {
360
+ throw createServiceError(
361
+ `Structurizr renderer is unavailable: ${error.message}`,
362
+ 503,
363
+ 'STRUCTURIZR_UNAVAILABLE',
364
+ );
365
+ }
366
+
367
+ return {
368
+ body: Buffer.from(await response.arrayBuffer()),
369
+ contentType: response.headers.get('content-type') || 'application/octet-stream',
370
+ statusCode: response.status,
371
+ };
372
+ }
373
+ }
374
+
@@ -1,6 +1,10 @@
1
1
  import { WebSocketServer } from 'ws';
2
2
 
3
3
  import { normalizeHostedEmail } from '../../domain/hosted-workspace-contract.js';
4
+ import {
5
+ createRequestUrlWithPathname,
6
+ stripBasePath,
7
+ } from '../http/http-request-helpers.js';
4
8
  import { ClientSocketSession } from './client-socket-session.js';
5
9
 
6
10
  function rejectUpgrade(socket, statusCode, statusMessage, {
@@ -25,28 +29,6 @@ function extractRoomName(pathname, wsBasePath) {
25
29
  return decodeURIComponent(roomSegment || 'default');
26
30
  }
27
31
 
28
- function stripBasePath(pathname, basePath) {
29
- if (!basePath) {
30
- return pathname;
31
- }
32
-
33
- if (pathname === basePath) {
34
- return '/';
35
- }
36
-
37
- if (pathname.startsWith(`${basePath}/`)) {
38
- return pathname.slice(basePath.length) || '/';
39
- }
40
-
41
- return pathname;
42
- }
43
-
44
- function createRequestUrlWithPathname(requestUrl, pathname) {
45
- const nextUrl = new URL(requestUrl.toString());
46
- nextUrl.pathname = pathname || '/';
47
- return nextUrl;
48
- }
49
-
50
32
  export function attachCollaborationGateway({
51
33
  authService,
52
34
  basePath = '',
@@ -1 +0,0 @@
1
- import{i as e}from"./rolldown-runtime-Dd_uD5pT.js";import{A as t,F as n,J as r,L as i,P as a,V as o,X as s,c,lt as l,q as u,st as d,t as f}from"./dist-C11hHqv_.js";import"./chunk-SRAX5OIU-KFQikY-4.js";import{n as p,t as m}from"./jsx-runtime-BpzPEenQ.js";import{n as h,r as g,s as _,t as v}from"./dist-GrygD2zE.js";var y=e(p(),1),b=m(),x=c.define({token(e){return e.match(/^%%.*$/)?`comment`:e.match(/^"(?:[^"\\]|\\.)*"/)?`string`:e.match(/^(flowchart|graph|sequenceDiagram|classDiagram|stateDiagram|erDiagram|gantt|pie|mindmap|journey|gitGraph|timeline|quadrantChart|sankey|xychart)\b/i)||e.match(/^(TB|TD|BT|RL|LR)\b/)||e.match(/^(subgraph|end|participant|actor|loop|alt|else|opt|par|critical|break|rect|note|over|activate|deactivate|title|section|class|style|linkStyle|classDef|click)\b/i)?`keyword`:e.match(/^[-.=<>|ox]+>/)||e.match(/^<[-.=<>|ox]+/)||e.match(/^--+|\.\.+|==+/)?`operator`:e.match(/^[[\](){}|<>]/)?`bracket`:e.match(/^[A-Za-z_][A-Za-z0-9_]*/)?`variableName`:e.match(/^\d+(\.\d+)?/)?`number`:e.match(/^[,:;]/)?`punctuation`:(e.eatSpace()||e.next(),null)}});function S(){return x}var C=i.theme({"&":{backgroundColor:`#1e1e1e`,color:`#d4d4d4`},".cm-content":{caretColor:`#fff`},".cm-cursor":{borderLeftColor:`#fff`},".cm-gutters":{backgroundColor:`#1e1e1e`,color:`#858585`,border:`none`},".cm-activeLineGutter":{backgroundColor:`#2a2a2a`},".cm-activeLine":{backgroundColor:`#2a2a2a`},".cm-errorLine":{backgroundColor:`rgba(255, 0, 0, 0.15)`}},{dark:!0}),w=f.define([{tag:a.keyword,color:`#569cd6`},{tag:a.string,color:`#ce9178`},{tag:a.comment,color:`#6a9955`},{tag:a.number,color:`#b5cea8`},{tag:a.operator,color:`#d4d4d4`},{tag:a.punctuation,color:`#d4d4d4`},{tag:a.variableName,color:`#9cdcfe`},{tag:a.bracket,color:`#ffd700`}]),T=i.theme({"&":{backgroundColor:`#ffffff`,color:`#1e1e1e`},".cm-content":{caretColor:`#000`},".cm-cursor":{borderLeftColor:`#000`},".cm-gutters":{backgroundColor:`#fff`,color:`#999`,border:`none`},".cm-activeLineGutter":{backgroundColor:`#e8e8e8`},".cm-activeLine":{backgroundColor:`#e8e8e8`},".cm-errorLine":{backgroundColor:`rgba(255, 0, 0, 0.1)`}}),E=f.define([{tag:a.keyword,color:`#0000ff`},{tag:a.string,color:`#a31515`},{tag:a.comment,color:`#008000`},{tag:a.number,color:`#098658`},{tag:a.operator,color:`#1e1e1e`},{tag:a.punctuation,color:`#1e1e1e`},{tag:a.variableName,color:`#001080`},{tag:a.bracket,color:`#af00db`}]),D=n.line({class:`cm-errorLine`}),O=(e,t)=>{if(!e||e<1||e>t.lines)return i.decorations.of(n.none);let r=t.line(e);return i.decorations.of(n.set([D.range(r.from)]))},k=e=>e===`dark`?[C,t(w)]:[T,t(E)],A=({value:e,onChange:t,onKeyboardSubmit:n,placeholder:a,theme:c,errorLine:f})=>{let p=(0,y.useRef)(null),m=(0,y.useRef)(null),x=(0,y.useRef)(t),C=(0,y.useRef)(n),w=(0,y.useRef)(new d),T=(0,y.useRef)(new d);return x.current=t,C.current=n,(0,y.useEffect)(()=>{if(!p.current)return;let t=w.current,n=new i({state:l.create({doc:e,extensions:[u.of([{key:`Mod-Enter`,run:()=>(C.current?.(),!0)},{key:`Mod-Shift-z`,run:_,preventDefault:!0}]),i.updateListener.of(e=>{e.docChanged&&x.current(e.state.doc.toString())}),h(),u.of([...v,...g]),r(),i.lineWrapping,t.of(k(c)),T.current.of([]),S(),o({drawRangeCursor:!0}),...a?[s(a)]:[]]}),parent:p.current});return m.current=n,n.focus(),()=>{n.destroy(),m.current=null}},[]),(0,y.useEffect)(()=>{let e=m.current;e&&e.dispatch({effects:w.current.reconfigure(k(c))})},[c]),(0,y.useEffect)(()=>{let e=m.current;e&&e.dispatch({effects:T.current.reconfigure(O(f,e.state.doc))})},[f]),(0,y.useEffect)(()=>{let t=m.current;if(!t)return;let n=t.state.doc.toString();e!==n&&t.dispatch({changes:{from:0,to:n.length,insert:e}})},[e]),(0,b.jsx)(`div`,{ref:p,className:`ttd-dialog-input ttd-dialog-input--codemirror`})};export{A as default};
@@ -1 +0,0 @@
1
- function e(){return typeof window>`u`?globalThis:window}function t(t,n){let r=e();return typeof r.requestIdleCallback==`function`?r.requestIdleCallback(t,{timeout:n}):r.setTimeout(()=>t({didTimeout:!1,timeRemaining:()=>0}),1)}function n(t){if(t===null)return;let n=e();if(typeof n.cancelIdleCallback==`function`){n.cancelIdleCallback(t);return}n.clearTimeout(t)}function r(e,t,n){if(!e||!t)return!1;let r=t.getBoundingClientRect(),i=e.getBoundingClientRect();return i.bottom>=r.top-n&&i.top<=r.bottom+n}function i(t,n,{removeDelayMs:r=3e4,revokeDelayMs:i=r}={}){let a=e(),o=URL.createObjectURL(t),s=document.createElement(`a`);s.href=o,s.download=n,s.style&&(s.style.display=`none`),document.body.appendChild(s),s.click(),r>0?a.setTimeout(()=>s.remove(),r):s.remove(),a.setTimeout(()=>URL.revokeObjectURL(o),i)}function a(e=``,t=`download`){let n=String(e).match(/filename\*=UTF-8''([^;]+)/iu);if(n)try{return decodeURIComponent(n[1])}catch{return t}return String(e).match(/filename="([^"]+)"/iu)?.[1]||t}export{t as a,a as i,i as n,r,n as t};