@karmaniverous/jeeves-server 3.0.0-0

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 (260) hide show
  1. package/.env.local +13 -0
  2. package/.env.local.template +13 -0
  3. package/.tsbuildinfo +1 -0
  4. package/CHANGELOG.md +450 -0
  5. package/about.md +82 -0
  6. package/client/README.md +73 -0
  7. package/client/eslint.config.js +23 -0
  8. package/client/index.html +14 -0
  9. package/client/package-lock.json +5181 -0
  10. package/client/package.json +60 -0
  11. package/client/public/vite.svg +1 -0
  12. package/client/src/App.tsx +22 -0
  13. package/client/src/components/AccountMenu.tsx +167 -0
  14. package/client/src/components/ActionDropdown.tsx +120 -0
  15. package/client/src/components/CodeEditor.tsx +143 -0
  16. package/client/src/components/CodeViewer.tsx +113 -0
  17. package/client/src/components/ConfirmDialog.tsx +32 -0
  18. package/client/src/components/DirectoryRow.tsx +62 -0
  19. package/client/src/components/DirectoryTable.tsx +42 -0
  20. package/client/src/components/DownloadDropdown.tsx +116 -0
  21. package/client/src/components/DriveList.tsx +54 -0
  22. package/client/src/components/EmbeddedDiagramPanzoom.ts +28 -0
  23. package/client/src/components/FileContentView.tsx +155 -0
  24. package/client/src/components/InlineSvgPanzoom.ts +60 -0
  25. package/client/src/components/LazyDiagram.ts +93 -0
  26. package/client/src/components/LinkDropdown.tsx +134 -0
  27. package/client/src/components/MarkdownView.tsx +115 -0
  28. package/client/src/components/MermaidViewer.tsx +21 -0
  29. package/client/src/components/PlantUmlViewer.tsx +21 -0
  30. package/client/src/components/SearchModal.tsx +424 -0
  31. package/client/src/components/SvgViewer.tsx +107 -0
  32. package/client/src/components/TabBar.tsx +96 -0
  33. package/client/src/components/layout/Header.tsx +270 -0
  34. package/client/src/components/panzoom.ts +203 -0
  35. package/client/src/components/renderableUtils.ts +15 -0
  36. package/client/src/components/runner/JobTable.tsx +153 -0
  37. package/client/src/components/runner/RunHistory.tsx +140 -0
  38. package/client/src/components/runner/StatsBar.tsx +43 -0
  39. package/client/src/components/runner/StatusPill.tsx +27 -0
  40. package/client/src/components/runner/jobTableUtils.ts +65 -0
  41. package/client/src/components/scrollUtils.ts +39 -0
  42. package/client/src/components/ui/alert-dialog.tsx +107 -0
  43. package/client/src/components/ui/button.tsx +40 -0
  44. package/client/src/components/ui/dropdown-menu.tsx +79 -0
  45. package/client/src/components/ui/input.tsx +26 -0
  46. package/client/src/components/useActionState.ts +43 -0
  47. package/client/src/hooks/useFileBrowser.ts +102 -0
  48. package/client/src/hooks/useFileData.ts +78 -0
  49. package/client/src/hooks/useScrollAnchor.ts +70 -0
  50. package/client/src/hooks/useShareSettings.ts +22 -0
  51. package/client/src/hooks/useTopBar.ts +27 -0
  52. package/client/src/index.css +281 -0
  53. package/client/src/lib/AuthContext.ts +27 -0
  54. package/client/src/lib/api.ts +239 -0
  55. package/client/src/lib/auth.tsx +50 -0
  56. package/client/src/lib/codeBlockCm6.ts +129 -0
  57. package/client/src/lib/codeBlockCopy.ts +43 -0
  58. package/client/src/lib/codemirror.ts +77 -0
  59. package/client/src/lib/runner-api.ts +172 -0
  60. package/client/src/lib/svg.ts +50 -0
  61. package/client/src/lib/theme.ts +34 -0
  62. package/client/src/lib/utils.ts +6 -0
  63. package/client/src/main.tsx +11 -0
  64. package/client/src/pages/FileBrowser.tsx +135 -0
  65. package/client/src/pages/Home.tsx +46 -0
  66. package/client/src/pages/Runner.tsx +151 -0
  67. package/client/src/pages/RunnerJob.tsx +170 -0
  68. package/client/tsconfig.app.json +32 -0
  69. package/client/tsconfig.json +7 -0
  70. package/client/tsconfig.node.json +26 -0
  71. package/client/vite.config.ts +35 -0
  72. package/content/privacy.md +61 -0
  73. package/content/terms.md +41 -0
  74. package/dist/client/assets/CodeEditor-0XHVI8Nu.js +1 -0
  75. package/dist/client/assets/CodeViewer-CykMVsfX.js +1 -0
  76. package/dist/client/assets/index--MBieNJA.js +1 -0
  77. package/dist/client/assets/index-BENeXQI_.js +1 -0
  78. package/dist/client/assets/index-BbBpoOxz.js +1 -0
  79. package/dist/client/assets/index-BdV9g5AM.js +6 -0
  80. package/dist/client/assets/index-BjAilRri.js +2 -0
  81. package/dist/client/assets/index-BqbhWo2I.js +3 -0
  82. package/dist/client/assets/index-CVbycZ0H.js +1 -0
  83. package/dist/client/assets/index-Cs5oz2oJ.js +5 -0
  84. package/dist/client/assets/index-D8KZVveX.js +1 -0
  85. package/dist/client/assets/index-DC4HMHxY.js +13 -0
  86. package/dist/client/assets/index-DbMebkkd.css +1 -0
  87. package/dist/client/assets/index-DcY2RXqX.js +1 -0
  88. package/dist/client/assets/index-Duy-tZYV.js +1 -0
  89. package/dist/client/assets/index-Dw7rDFmE.js +7 -0
  90. package/dist/client/assets/index-FlCUvrjv.js +2 -0
  91. package/dist/client/assets/index-K6OVmfhg.js +1 -0
  92. package/dist/client/assets/index-LjwgzZ7F.js +62 -0
  93. package/dist/client/assets/index-MLwyFRN0.js +1 -0
  94. package/dist/client/assets/index-OpqBpSjn.js +1 -0
  95. package/dist/client/assets/index-SsHei0HE.js +1 -0
  96. package/dist/client/assets/index-uQa2yckk.js +1 -0
  97. package/dist/client/assets/index-udkXoIER.js +1 -0
  98. package/dist/client/index.html +15 -0
  99. package/dist/client/vite.svg +1 -0
  100. package/dist/src/auth/google.js +57 -0
  101. package/dist/src/auth/keys.js +185 -0
  102. package/dist/src/auth/resolve.js +102 -0
  103. package/dist/src/auth/session.js +57 -0
  104. package/dist/src/cli/commands/config.js +100 -0
  105. package/dist/src/cli/commands/config.test.js +84 -0
  106. package/dist/src/cli/commands/service.js +93 -0
  107. package/dist/src/cli/commands/start.js +24 -0
  108. package/dist/src/cli/index.js +20 -0
  109. package/dist/src/config/index.js +90 -0
  110. package/dist/src/config/loadConfig.test.js +127 -0
  111. package/dist/src/config/resolve.js +134 -0
  112. package/dist/src/config/resolve.test.js +148 -0
  113. package/dist/src/config/schema.js +159 -0
  114. package/dist/src/config/substituteEnvVars.js +45 -0
  115. package/dist/src/config/substituteEnvVars.test.js +51 -0
  116. package/dist/src/config/types.js +5 -0
  117. package/dist/src/routes/api/auth-status.js +56 -0
  118. package/dist/src/routes/api/diagrams.js +35 -0
  119. package/dist/src/routes/api/directory.js +93 -0
  120. package/dist/src/routes/api/drives.js +15 -0
  121. package/dist/src/routes/api/export.js +218 -0
  122. package/dist/src/routes/api/fileContent.js +286 -0
  123. package/dist/src/routes/api/index.js +33 -0
  124. package/dist/src/routes/api/linkInfo.js +71 -0
  125. package/dist/src/routes/api/linkInfo.test.js +104 -0
  126. package/dist/src/routes/api/middleware.js +117 -0
  127. package/dist/src/routes/api/raw.js +38 -0
  128. package/dist/src/routes/api/runner.js +59 -0
  129. package/dist/src/routes/api/search.js +236 -0
  130. package/dist/src/routes/api/sharing.js +203 -0
  131. package/dist/src/routes/api/status.js +68 -0
  132. package/dist/src/routes/api/status.test.js +62 -0
  133. package/dist/src/routes/auth.js +99 -0
  134. package/dist/src/routes/event.js +77 -0
  135. package/dist/src/routes/event.test.js +206 -0
  136. package/dist/src/routes/health.js +10 -0
  137. package/dist/src/routes/keys.js +129 -0
  138. package/dist/src/routes/path/index.js +17 -0
  139. package/dist/src/routes/static.js +30 -0
  140. package/dist/src/server.js +90 -0
  141. package/dist/src/services/deepShareLinks.js +163 -0
  142. package/dist/src/services/diagramCache.js +104 -0
  143. package/dist/src/services/embeddedDiagrams.js +136 -0
  144. package/dist/src/services/eventLog.js +55 -0
  145. package/dist/src/services/eventLog.test.js +113 -0
  146. package/dist/src/services/eventQueue.js +154 -0
  147. package/dist/src/services/eventQueue.test.js +104 -0
  148. package/dist/src/services/export.js +220 -0
  149. package/dist/src/services/exportCache.js +196 -0
  150. package/dist/src/services/markdown.js +147 -0
  151. package/dist/src/services/mermaid.js +97 -0
  152. package/dist/src/services/plantuml.js +145 -0
  153. package/dist/src/services/puppeteer.js +156 -0
  154. package/dist/src/util/breadcrumbs.js +22 -0
  155. package/dist/src/util/crypto.js +56 -0
  156. package/dist/src/util/crypto.test.js +99 -0
  157. package/dist/src/util/fileDetection.js +66 -0
  158. package/dist/src/util/fileDetection.test.js +89 -0
  159. package/dist/src/util/formatters.js +43 -0
  160. package/dist/src/util/formatters.test.js +83 -0
  161. package/dist/src/util/packageVersion.js +25 -0
  162. package/dist/src/util/platform.js +148 -0
  163. package/dist/src/util/state.js +46 -0
  164. package/dist/vitest.config.js +12 -0
  165. package/favicon.svg +3 -0
  166. package/guides/access-decision-flow.mmd +24 -0
  167. package/guides/access-decision-flow.svg +1 -0
  168. package/guides/api-integration.md +236 -0
  169. package/guides/deployment.md +287 -0
  170. package/guides/event-gateway.md +204 -0
  171. package/guides/event-gateway.mmd +17 -0
  172. package/guides/event-gateway.svg +1 -0
  173. package/guides/exports.md +239 -0
  174. package/guides/setup.md +313 -0
  175. package/guides/sharing.md +204 -0
  176. package/jeeves-server.config.template.json +25 -0
  177. package/package.json +124 -0
  178. package/scripts/download-plantuml.js +70 -0
  179. package/src/auth/google.ts +93 -0
  180. package/src/auth/keys.ts +252 -0
  181. package/src/auth/resolve.ts +157 -0
  182. package/src/auth/session.ts +77 -0
  183. package/src/cli/commands/config.test.ts +107 -0
  184. package/src/cli/commands/config.ts +113 -0
  185. package/src/cli/commands/service.ts +129 -0
  186. package/src/cli/commands/start.ts +27 -0
  187. package/src/cli/index.ts +25 -0
  188. package/src/config/index.ts +113 -0
  189. package/src/config/loadConfig.test.ts +155 -0
  190. package/src/config/resolve.test.ts +192 -0
  191. package/src/config/resolve.ts +173 -0
  192. package/src/config/schema.ts +179 -0
  193. package/src/config/substituteEnvVars.test.ts +64 -0
  194. package/src/config/substituteEnvVars.ts +52 -0
  195. package/src/config/types.ts +129 -0
  196. package/src/routes/api/auth-status.ts +85 -0
  197. package/src/routes/api/diagrams.ts +53 -0
  198. package/src/routes/api/directory.ts +123 -0
  199. package/src/routes/api/drives.ts +23 -0
  200. package/src/routes/api/export.ts +314 -0
  201. package/src/routes/api/fileContent.ts +414 -0
  202. package/src/routes/api/index.ts +37 -0
  203. package/src/routes/api/linkInfo.test.ts +132 -0
  204. package/src/routes/api/linkInfo.ts +83 -0
  205. package/src/routes/api/middleware.ts +156 -0
  206. package/src/routes/api/raw.ts +54 -0
  207. package/src/routes/api/runner.ts +107 -0
  208. package/src/routes/api/search.ts +321 -0
  209. package/src/routes/api/sharing.ts +259 -0
  210. package/src/routes/api/status.test.ts +72 -0
  211. package/src/routes/api/status.ts +82 -0
  212. package/src/routes/auth.ts +143 -0
  213. package/src/routes/event.test.ts +248 -0
  214. package/src/routes/event.ts +109 -0
  215. package/src/routes/health.ts +13 -0
  216. package/src/routes/keys.ts +192 -0
  217. package/src/routes/path/index.ts +24 -0
  218. package/src/routes/static.ts +54 -0
  219. package/src/server.ts +104 -0
  220. package/src/services/deepShareLinks.ts +203 -0
  221. package/src/services/diagramCache.ts +128 -0
  222. package/src/services/embeddedDiagrams.ts +168 -0
  223. package/src/services/eventLog.test.ts +144 -0
  224. package/src/services/eventLog.ts +68 -0
  225. package/src/services/eventQueue.test.ts +127 -0
  226. package/src/services/eventQueue.ts +196 -0
  227. package/src/services/export.ts +267 -0
  228. package/src/services/exportCache.ts +216 -0
  229. package/src/services/markdown.ts +189 -0
  230. package/src/services/mermaid.ts +113 -0
  231. package/src/services/plantuml.ts +172 -0
  232. package/src/services/puppeteer.ts +188 -0
  233. package/src/types/fastify.d.ts +13 -0
  234. package/src/types/jsonmap.d.ts +10 -0
  235. package/src/types/plantuml-encoder.d.ts +4 -0
  236. package/src/util/breadcrumbs.ts +33 -0
  237. package/src/util/crypto.test.ts +132 -0
  238. package/src/util/crypto.ts +79 -0
  239. package/src/util/fileDetection.test.ts +115 -0
  240. package/src/util/fileDetection.ts +70 -0
  241. package/src/util/formatters.test.ts +105 -0
  242. package/src/util/formatters.ts +44 -0
  243. package/src/util/packageVersion.ts +30 -0
  244. package/src/util/platform.ts +178 -0
  245. package/src/util/state.ts +55 -0
  246. package/test-docs/diagram-retry-test.md +18 -0
  247. package/test-docs/embedded-diagrams.md +52 -0
  248. package/test-docs/lazy-diagrams-test.md +333 -0
  249. package/test-docs/page-a.md +7 -0
  250. package/test-docs/page-b.md +7 -0
  251. package/test-docs/page-c.md +7 -0
  252. package/test-docs/sub/page-d.md +7 -0
  253. package/test-docs/test-diagram.puml +13 -0
  254. package/test-docs/validate-deep-share.js +318 -0
  255. package/tsconfig.json +37 -0
  256. package/tsdoc.json +13 -0
  257. package/vendor/.plantuml-version +1 -0
  258. package/vendor/plantuml.jar +0 -0
  259. package/vitest.config.js +12 -0
  260. package/vitest.config.ts +13 -0
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Mermaid diagram rendering service.
3
+ *
4
+ * Uses \@mermaid-js/mermaid-cli programmatic API with the server's configured Chrome.
5
+ * Falls back to execSync shell-out if the programmatic API fails to load.
6
+ */
7
+
8
+ import fs from 'node:fs';
9
+ import os from 'node:os';
10
+ import path from 'node:path';
11
+
12
+ import { getConfig } from '../config/index.js';
13
+
14
+ /**
15
+ * Render Mermaid source string to SVG via the mermaid-cli programmatic API.
16
+ *
17
+ * Uses the server's configured chromePath via puppeteerConfig.executablePath,
18
+ * avoiding a separate Chromium download.
19
+ *
20
+ * Returns null on failure.
21
+ */
22
+ export async function renderMermaidFromSource(
23
+ source: string,
24
+ ): Promise<string | null> {
25
+ const config = getConfig();
26
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mmd-'));
27
+ const inFile = path.join(tmpDir, 'diagram.mmd');
28
+ const outFile = path.join(tmpDir, 'diagram.svg');
29
+
30
+ try {
31
+ fs.writeFileSync(inFile, source, 'utf8');
32
+
33
+ // Dynamic import to avoid loading puppeteer at module parse time
34
+ const { run } = await import('@mermaid-js/mermaid-cli');
35
+
36
+ await run(inFile, outFile as `${string}.svg`, {
37
+ quiet: true,
38
+ outputFormat: 'svg',
39
+ puppeteerConfig: {
40
+ executablePath: config.chromePath,
41
+ headless: 'shell',
42
+ args: ['--no-sandbox', '--disable-setuid-sandbox'],
43
+ },
44
+ parseMMDOptions: {
45
+ backgroundColor: 'white',
46
+ viewport: { width: 1600, height: 1200, deviceScaleFactor: 2 },
47
+ },
48
+ });
49
+
50
+ if (!fs.existsSync(outFile)) return null;
51
+ return fs.readFileSync(outFile, 'utf8');
52
+ } catch (err) {
53
+ console.error('[mermaid] render failed:', (err as Error).message);
54
+ return null;
55
+ } finally {
56
+ try {
57
+ fs.rmSync(tmpDir, { recursive: true, force: true });
58
+ } catch {
59
+ /* ignore */
60
+ }
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Render a .mmd file to SVG string.
66
+ * Returns null on failure.
67
+ */
68
+ export async function renderMermaidSvg(
69
+ inputPath: string,
70
+ ): Promise<string | null> {
71
+ const source = fs.readFileSync(inputPath, 'utf8');
72
+ return renderMermaidFromSource(source);
73
+ }
74
+
75
+ /**
76
+ * Render a .mmd file to the specified format and return the output file path.
77
+ * The caller is responsible for reading and cleaning up the output file.
78
+ * Returns null on failure.
79
+ */
80
+ export async function renderMermaidToFile(
81
+ inputPath: string,
82
+ format: string,
83
+ ): Promise<string | null> {
84
+ const config = getConfig();
85
+ const resolved = path.resolve(inputPath);
86
+ const outFile = path.join(
87
+ path.dirname(resolved),
88
+ `${path.basename(resolved, '.mmd')}.${format}`,
89
+ );
90
+
91
+ try {
92
+ const { run } = await import('@mermaid-js/mermaid-cli');
93
+
94
+ await run(resolved, outFile as `${string}.svg`, {
95
+ quiet: true,
96
+ outputFormat: format as 'svg' | 'png' | 'pdf',
97
+ puppeteerConfig: {
98
+ executablePath: config.chromePath,
99
+ headless: 'shell',
100
+ args: ['--no-sandbox', '--disable-setuid-sandbox'],
101
+ },
102
+ parseMMDOptions: {
103
+ backgroundColor: 'white',
104
+ viewport: { width: 1600, height: 1200, deviceScaleFactor: 2 },
105
+ },
106
+ });
107
+
108
+ if (fs.existsSync(outFile)) return outFile;
109
+ } catch (err) {
110
+ console.error('[mermaid] render to file failed:', (err as Error).message);
111
+ }
112
+ return null;
113
+ }
@@ -0,0 +1,172 @@
1
+ /**
2
+ * PlantUML rendering with fallback pipeline:
3
+ * 1. Local Java jar (if configured) — supports !include directives
4
+ * 2. Configured PlantUML servers (in order)
5
+ * 3. Public community server (always last resort)
6
+ */
7
+
8
+ import { execSync } from 'node:child_process';
9
+ import fs from 'node:fs';
10
+ import os from 'node:os';
11
+ import path from 'node:path';
12
+
13
+ import plantumlEncoder from 'plantuml-encoder';
14
+
15
+ import { getConfig } from '../config/index.js';
16
+
17
+ /**
18
+ * Try rendering via local PlantUML jar.
19
+ * Output goes to a temp directory to avoid writing in the source tree.
20
+ * Returns the output buffer, or null on failure.
21
+ */
22
+ function renderViaJar(filePath: string, format: string): Buffer | null {
23
+ const { plantuml } = getConfig();
24
+ if (!plantuml.jarPath) return null;
25
+
26
+ const dir = path.dirname(filePath);
27
+ const ext = path.extname(filePath);
28
+ const base = path.basename(filePath, ext);
29
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'puml-'));
30
+ const outFile = path.join(tmpDir, `${base}.${format}`);
31
+
32
+ try {
33
+ const java = plantuml.javaPath ?? 'java';
34
+ // UNSECURE profile allows remote !include directives (e.g. AWS PuML from GitHub)
35
+ execSync(
36
+ `"${java}" -DPLANTUML_SECURITY_PROFILE=UNSECURE -jar "${plantuml.jarPath}" -t${format} -o "${tmpDir}" "${filePath}"`,
37
+ { timeout: 60_000, stdio: 'pipe', cwd: dir },
38
+ );
39
+ if (!fs.existsSync(outFile)) return null;
40
+ const buffer = fs.readFileSync(outFile);
41
+ return buffer;
42
+ } catch (err) {
43
+ // PlantUML may produce output even on non-zero exit (e.g. partial render with warnings).
44
+ // Check for output before giving up.
45
+ if (fs.existsSync(outFile)) {
46
+ console.warn('[PlantUML jar] render completed with warnings');
47
+ if (err && typeof err === 'object' && 'stderr' in err) {
48
+ console.warn(
49
+ '[PlantUML jar] stderr:',
50
+ String((err as { stderr: unknown }).stderr),
51
+ );
52
+ }
53
+ return fs.readFileSync(outFile);
54
+ }
55
+ console.error('[PlantUML jar] render failed:', (err as Error).message);
56
+ if (err && typeof err === 'object' && 'stderr' in err) {
57
+ console.error(
58
+ '[PlantUML jar] stderr:',
59
+ String((err as { stderr: unknown }).stderr),
60
+ );
61
+ }
62
+ return null;
63
+ } finally {
64
+ // Clean up temp dir
65
+ try {
66
+ fs.rmSync(tmpDir, { recursive: true, force: true });
67
+ } catch {
68
+ /* ignore */
69
+ }
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Try rendering via a PlantUML server endpoint.
75
+ * Returns the output buffer, or null on failure.
76
+ */
77
+ async function renderViaServer(
78
+ source: string,
79
+ format: string,
80
+ serverUrl: string,
81
+ ): Promise<Buffer | null> {
82
+ try {
83
+ const encoded = plantumlEncoder.encode(source);
84
+ const url = `${serverUrl.replace(/\/+$/, '')}/${format}/${encoded}`;
85
+ const resp = await fetch(url, { signal: AbortSignal.timeout(15_000) });
86
+ if (!resp.ok) return null;
87
+ return Buffer.from(await resp.arrayBuffer());
88
+ } catch {
89
+ return null;
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Render a PlantUML file with the full fallback pipeline.
95
+ * Returns \{ buffer, method \} or null if all methods fail.
96
+ */
97
+ export async function renderPlantUml(
98
+ filePath: string,
99
+ format: string = 'svg',
100
+ ): Promise<{ buffer: Buffer; method: string } | null> {
101
+ // 1. Try local jar (supports includes)
102
+ const jarResult = renderViaJar(filePath, format);
103
+ if (jarResult) return { buffer: jarResult, method: 'jar' };
104
+
105
+ // 2. Try configured servers in order
106
+ const { plantuml } = getConfig();
107
+ const source = fs.readFileSync(filePath, 'utf8');
108
+
109
+ for (const server of plantuml.servers) {
110
+ const serverResult = await renderViaServer(source, format, server);
111
+ if (serverResult) return { buffer: serverResult, method: server };
112
+ }
113
+
114
+ return null;
115
+ }
116
+
117
+ /**
118
+ * Render PlantUML source string to SVG.
119
+ * Writes to a temp file, renders via the full pipeline, cleans up.
120
+ */
121
+ export async function renderPlantUmlFromSource(
122
+ source: string,
123
+ ): Promise<string | null> {
124
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'puml-src-'));
125
+ const inFile = path.join(tmpDir, 'diagram.puml');
126
+
127
+ try {
128
+ fs.writeFileSync(inFile, source, 'utf8');
129
+ return await renderPlantUmlSvg(inFile);
130
+ } finally {
131
+ try {
132
+ fs.rmSync(tmpDir, { recursive: true, force: true });
133
+ } catch {
134
+ /* ignore */
135
+ }
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Render PlantUML to SVG string (convenience for file API).
141
+ */
142
+ export async function renderPlantUmlSvg(
143
+ filePath: string,
144
+ ): Promise<string | null> {
145
+ const result = await renderPlantUml(filePath, 'svg');
146
+ if (!result) return null;
147
+ return result.buffer.toString('utf8');
148
+ }
149
+
150
+ /**
151
+ * Render PlantUML to buffer for export.
152
+ */
153
+ export async function renderPlantUmlToBuffer(
154
+ filePath: string,
155
+ format: string,
156
+ ): Promise<Buffer | null> {
157
+ const result = await renderPlantUml(filePath, format);
158
+ return result?.buffer ?? null;
159
+ }
160
+
161
+ /**
162
+ * Get all export formats supported by PlantUML.
163
+ */
164
+ export function getPlantUmlFormats(): string[] {
165
+ // PlantUML jar supports all these; server supports svg/png/txt
166
+ const { plantuml } = getConfig();
167
+ if (plantuml.jarPath) {
168
+ return ['svg', 'png', 'pdf', 'eps'];
169
+ }
170
+ // Server-only: limited formats
171
+ return ['svg', 'png'];
172
+ }
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Puppeteer browser management and page preparation utilities.
3
+ * Shared by PDF and DOCX export paths.
4
+ */
5
+
6
+ import type { Browser, Page } from 'puppeteer-core';
7
+ import puppeteer from 'puppeteer-core';
8
+
9
+ import { getConfig } from '../config/index.js';
10
+
11
+ /**
12
+ * Launch Puppeteer browser with configured Chrome path.
13
+ */
14
+ export async function launchBrowser(): Promise<Browser> {
15
+ const { chromePath } = getConfig();
16
+ return await puppeteer.launch({
17
+ executablePath: chromePath,
18
+ headless: true,
19
+ args: ['--no-sandbox', '--disable-setuid-sandbox'],
20
+ });
21
+ }
22
+
23
+ /** Print CSS — hides chrome, normalizes layout for clean export. */
24
+ const PRINT_CSS = `
25
+ /* Hide chrome — works for both legacy server-rendered and SPA */
26
+ .header, .header-actions, .panzoom-container, .panzoom-hint,
27
+ header, nav, .toc-sidebar, [class*="sticky"], [class*="fixed"] { display: none !important; }
28
+ .toc { position: static !important; height: auto !important; page-break-after: always; }
29
+ .toc-spacer { display: none !important; }
30
+ .layout { display: block !important; }
31
+ body { background: #fff !important; font-size: 10pt !important; line-height: 1.5 !important; color: #000 !important; }
32
+ /* SPA layout: remove scroll containers so body grows to full content height */
33
+ html, body, #root { height: auto !important; overflow: visible !important; }
34
+ main, [class*="overflow-y"] { overflow: visible !important; height: auto !important; }
35
+ /* SPA article.prose */
36
+ article.prose { max-width: none !important; border: none !important; padding: 0 !important; }
37
+ .content, article.prose { font-size: 10pt !important; }
38
+ /* Hide tab bar and other SPA controls */
39
+ [role="tablist"], button { display: none !important; }
40
+ main { padding-top: 0 !important; }
41
+ /* Inline SVG Panzoom & Embedded Diagrams: strip container chrome, show SVGs cleanly */
42
+ .inline-svg-panzoom, .embedded-diagram-panzoom, .embedded-diagram-rendered {
43
+ position: static !important;
44
+ background: white !important;
45
+ border: none !important;
46
+ overflow: visible !important;
47
+ cursor: default !important;
48
+ padding: 0 !important;
49
+ margin: 1em 0 !important;
50
+ }
51
+ .inline-svg-panzoom button, .embedded-diagram-panzoom button { display: none !important; }
52
+ .inline-svg-panzoom .text-xs, .embedded-diagram-panzoom .text-xs { display: none !important; }
53
+ .inline-svg-panzoom svg, .embedded-diagram-panzoom svg, .embedded-diagram-rendered svg {
54
+ max-width: 190mm !important;
55
+ max-height: 250mm !important;
56
+ width: auto !important;
57
+ height: auto !important;
58
+ display: block !important;
59
+ page-break-inside: avoid !important;
60
+ }
61
+ h1 { font-size: 18pt !important; }
62
+ h2 { font-size: 14pt !important; }
63
+ h3 { font-size: 12pt !important; }
64
+ h4, h5, h6 { font-size: 10pt !important; }
65
+ code { font-size: 9pt !important; }
66
+ pre { font-size: 8pt !important; background: #f5f5f5 !important; border: 1px solid #e0e0e0 !important; border-radius: 4px !important; padding: 8px !important; overflow-wrap: break-word !important; white-space: pre-wrap !important; }
67
+ pre code { font-size: 8pt !important; }
68
+ table { font-size: 10pt !important; }
69
+ a.anchor { display: none !important; }
70
+ img, svg, .svg-container, .zoomable-svg {
71
+ max-width: 190mm !important;
72
+ max-height: 250mm !important;
73
+ width: auto !important;
74
+ height: auto !important;
75
+ display: block !important;
76
+ page-break-inside: avoid !important;
77
+ }
78
+ img { object-fit: contain !important; }
79
+ `;
80
+
81
+ /**
82
+ * Add print styles to a Puppeteer page.
83
+ */
84
+ export async function addPrintStyles(page: Page): Promise<void> {
85
+ await page.addStyleTag({ content: PRINT_CSS });
86
+ }
87
+
88
+ /**
89
+ * Wait for SPA content to fully render, including async SVG fetches.
90
+ */
91
+ export async function waitForSpaContent(page: Page): Promise<void> {
92
+ await page
93
+ .waitForSelector('article.prose', { timeout: 15_000 })
94
+ .catch(() => {});
95
+ await page
96
+ .waitForFunction(
97
+ () => {
98
+ const containers = document.querySelectorAll('.inline-svg-panzoom');
99
+ if (containers.length === 0) return true;
100
+ return Array.from(containers).every(
101
+ (c) => !c.textContent.includes('Loading SVG'),
102
+ );
103
+ },
104
+ { timeout: 15_000 },
105
+ )
106
+ .catch(() => {});
107
+ // Wait for CM6 code blocks to mount
108
+ await page
109
+ .waitForFunction(
110
+ () => {
111
+ const article = document.querySelector('article.prose');
112
+ if (!article) return true;
113
+ // If there are no code blocks, or CM6 has signaled ready
114
+ const codeBlocks = article.querySelectorAll('.cm6-embedded-code');
115
+ if (codeBlocks.length === 0) return true;
116
+ return article.getAttribute('data-cm6-ready') === 'true';
117
+ },
118
+ { timeout: 15_000 },
119
+ )
120
+ .catch(() => {});
121
+ await new Promise((r) => setTimeout(r, 1000));
122
+ }
123
+
124
+ /** SVG container selectors used across export paths. */
125
+ export const SVG_CONTAINER_SELECTORS =
126
+ '.svg-container, .zoomable-svg, .inline-svg-panzoom, .embedded-diagram-panzoom, .embedded-diagram-rendered';
127
+
128
+ /**
129
+ * Capture each SVG in a page as a high-quality PNG screenshot.
130
+ * Returns array of \{ index, dataUrl, width, height \}.
131
+ */
132
+ export async function captureSvgsAsPng(
133
+ browser: Browser,
134
+ page: Page,
135
+ ): Promise<
136
+ { index: number; dataUrl: string; width: number; height: number }[]
137
+ > {
138
+ const svgContents = await page.evaluate((selectors: string) => {
139
+ const containers = document.querySelectorAll(selectors);
140
+ return Array.from(containers).map((container, i) => {
141
+ const svg = container.querySelector('svg');
142
+ return { index: i, svgHtml: svg ? svg.outerHTML : null };
143
+ });
144
+ }, SVG_CONTAINER_SELECTORS);
145
+
146
+ const results: {
147
+ index: number;
148
+ dataUrl: string;
149
+ width: number;
150
+ height: number;
151
+ }[] = [];
152
+
153
+ for (const { index, svgHtml } of svgContents) {
154
+ if (!svgHtml) continue;
155
+
156
+ const svgPage = await browser.newPage();
157
+ await svgPage.setViewport({
158
+ width: 1200,
159
+ height: 2000,
160
+ deviceScaleFactor: 2,
161
+ });
162
+ await svgPage.setContent(
163
+ `<!DOCTYPE html>
164
+ <html><head><style>
165
+ body { margin: 0; padding: 0; background: #fff; }
166
+ svg { width: 1152px; height: auto; display: block; }
167
+ </style></head><body>${svgHtml}</body></html>`,
168
+ { waitUntil: 'networkidle0' },
169
+ );
170
+
171
+ const svgHandle = await svgPage.$('svg');
172
+ if (svgHandle) {
173
+ const screenshot = await svgHandle.screenshot({ type: 'png' });
174
+ const box = await svgHandle.boundingBox();
175
+ if (box && box.width > 0 && box.height > 0) {
176
+ results.push({
177
+ index,
178
+ dataUrl: `data:image/png;base64,${Buffer.from(screenshot).toString('base64')}`,
179
+ width: Math.ceil(box.width),
180
+ height: Math.ceil(box.height),
181
+ });
182
+ }
183
+ }
184
+ await svgPage.close();
185
+ }
186
+
187
+ return results;
188
+ }
@@ -0,0 +1,13 @@
1
+ import type { AccessMode, NormalizedScopes } from '../config/types.js';
2
+
3
+ declare module 'fastify' {
4
+ interface FastifyRequest {
5
+ accessMode?: AccessMode;
6
+ authSeed?: string;
7
+ insiderScopes?: NormalizedScopes | null;
8
+ insiderEmail?: string;
9
+ keyAge?: string | null;
10
+ deepShareParams?: { d: string; dirs: string; s: string };
11
+ authMatchedPath?: string | null;
12
+ }
13
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Type declarations for karmaniverous/jsonmap package
3
+ */
4
+
5
+ declare module '@karmaniverous/jsonmap' {
6
+ export class JsonMap {
7
+ constructor(map?: object, lib?: Record<string, unknown>, ignore?: string);
8
+ transform(input: unknown): Promise<unknown>;
9
+ }
10
+ }
@@ -0,0 +1,4 @@
1
+ declare module 'plantuml-encoder' {
2
+ export function encode(source: string): string;
3
+ export function decode(encoded: string): string;
4
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Breadcrumb filtering utilities.
3
+ */
4
+
5
+ export interface Breadcrumb {
6
+ label: string;
7
+ path: string;
8
+ }
9
+
10
+ /**
11
+ * Filter breadcrumbs for outsiders:
12
+ * - File shares: no breadcrumbs (the page stands alone)
13
+ * - Directory shares: trim to the share root (matchedPath)
14
+ */
15
+ export function filterBreadcrumbsForOutsider(
16
+ breadcrumbs: Breadcrumb[],
17
+ isInsider: boolean,
18
+ matchedPath: string | null,
19
+ isDirectoryView: boolean,
20
+ ): Breadcrumb[] {
21
+ if (isInsider) return breadcrumbs;
22
+ if (!isDirectoryView)
23
+ return breadcrumbs.length > 0 ? [breadcrumbs[breadcrumbs.length - 1]] : [];
24
+ // For directory views, trim breadcrumbs to the matched (shared) path root
25
+ if (matchedPath) {
26
+ const normalizedMatch = matchedPath.replace(/^\/+|\/+$/g, '').toLowerCase();
27
+ const matchIdx = breadcrumbs.findIndex(
28
+ (b) => b.path.replace(/^\/+|\/+$/g, '').toLowerCase() === normalizedMatch,
29
+ );
30
+ if (matchIdx >= 0) return breadcrumbs.slice(matchIdx);
31
+ }
32
+ return breadcrumbs;
33
+ }
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Tests for cryptographic utilities
3
+ */
4
+
5
+ import { describe, expect, it } from 'vitest';
6
+
7
+ import {
8
+ computeInsiderKey,
9
+ computeOutsiderKeyWithExpiry,
10
+ computePathKey,
11
+ timingSafeEqual,
12
+ } from './crypto.js';
13
+
14
+ describe('crypto utilities', () => {
15
+ const testApiKey = 'test-api-key-12345';
16
+
17
+ describe('computePathKey', () => {
18
+ it('should compute a 32-character hex key', () => {
19
+ const key = computePathKey(testApiKey, '/foo/bar');
20
+ expect(key).toMatch(/^[a-f0-9]{32}$/);
21
+ });
22
+
23
+ it('should normalize paths (case-insensitive)', () => {
24
+ const key1 = computePathKey(testApiKey, '/Foo/Bar');
25
+ const key2 = computePathKey(testApiKey, '/foo/bar');
26
+ expect(key1).toBe(key2);
27
+ });
28
+
29
+ it('should normalize paths (trim slashes)', () => {
30
+ const key1 = computePathKey(testApiKey, '/foo/bar/');
31
+ const key2 = computePathKey(testApiKey, 'foo/bar');
32
+ expect(key1).toBe(key2);
33
+ });
34
+
35
+ it('should produce different keys for different paths', () => {
36
+ const key1 = computePathKey(testApiKey, '/foo/bar');
37
+ const key2 = computePathKey(testApiKey, '/baz/qux');
38
+ expect(key1).not.toBe(key2);
39
+ });
40
+
41
+ it('should produce different keys for different API keys', () => {
42
+ const key1 = computePathKey('api-key-1', '/foo/bar');
43
+ const key2 = computePathKey('api-key-2', '/foo/bar');
44
+ expect(key1).not.toBe(key2);
45
+ });
46
+ });
47
+
48
+ describe('computeInsiderKey', () => {
49
+ it('should compute a 32-character hex key', () => {
50
+ const key = computeInsiderKey(testApiKey);
51
+ expect(key).toMatch(/^[a-f0-9]{32}$/);
52
+ });
53
+
54
+ it('should be deterministic (same API key = same insider key)', () => {
55
+ const key1 = computeInsiderKey(testApiKey);
56
+ const key2 = computeInsiderKey(testApiKey);
57
+ expect(key1).toBe(key2);
58
+ });
59
+
60
+ it('should produce different keys for different API keys', () => {
61
+ const key1 = computeInsiderKey('api-key-1');
62
+ const key2 = computeInsiderKey('api-key-2');
63
+ expect(key1).not.toBe(key2);
64
+ });
65
+ });
66
+
67
+ describe('computeOutsiderKeyWithExpiry', () => {
68
+ it('should compute a 32-character hex key', () => {
69
+ const expiry = Date.now() + 3600000;
70
+ const key = computeOutsiderKeyWithExpiry(testApiKey, '/foo/bar', expiry);
71
+ expect(key).toMatch(/^[a-f0-9]{32}$/);
72
+ });
73
+
74
+ it('should normalize paths (case-insensitive)', () => {
75
+ const expiry = Date.now() + 3600000;
76
+ const key1 = computeOutsiderKeyWithExpiry(testApiKey, '/Foo/Bar', expiry);
77
+ const key2 = computeOutsiderKeyWithExpiry(testApiKey, '/foo/bar', expiry);
78
+ expect(key1).toBe(key2);
79
+ });
80
+
81
+ it('should produce different keys for different expiries', () => {
82
+ const expiry1 = Date.now() + 3600000;
83
+ const expiry2 = Date.now() + 7200000;
84
+ const key1 = computeOutsiderKeyWithExpiry(
85
+ testApiKey,
86
+ '/foo/bar',
87
+ expiry1,
88
+ );
89
+ const key2 = computeOutsiderKeyWithExpiry(
90
+ testApiKey,
91
+ '/foo/bar',
92
+ expiry2,
93
+ );
94
+ expect(key1).not.toBe(key2);
95
+ });
96
+
97
+ it('should accept string expiry', () => {
98
+ const expiry = String(Date.now() + 3600000);
99
+ const key = computeOutsiderKeyWithExpiry(testApiKey, '/foo/bar', expiry);
100
+ expect(key).toMatch(/^[a-f0-9]{32}$/);
101
+ });
102
+ });
103
+
104
+ describe('timingSafeEqual', () => {
105
+ it('should return true for equal strings', () => {
106
+ const a = 'abc123';
107
+ const b = 'abc123';
108
+ expect(timingSafeEqual(a, b)).toBe(true);
109
+ });
110
+
111
+ it('should return false for different strings', () => {
112
+ const a = 'abc123';
113
+ const b = 'def456';
114
+ expect(timingSafeEqual(a, b)).toBe(false);
115
+ });
116
+
117
+ it('should return false for strings of different lengths', () => {
118
+ const a = 'abc';
119
+ const b = 'abcdef';
120
+ expect(timingSafeEqual(a, b)).toBe(false);
121
+ });
122
+
123
+ it('should return false for empty strings vs non-empty', () => {
124
+ expect(timingSafeEqual('', 'abc')).toBe(false);
125
+ expect(timingSafeEqual('abc', '')).toBe(false);
126
+ });
127
+
128
+ it('should return true for empty strings', () => {
129
+ expect(timingSafeEqual('', '')).toBe(true);
130
+ });
131
+ });
132
+ });