@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,83 @@
1
+ /**
2
+ * Tests for formatting utilities
3
+ */
4
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
5
+ import { formatRelativeTime, formatSize, nowIso } from './formatters.js';
6
+ describe('formatters', () => {
7
+ describe('formatSize', () => {
8
+ it('should format 0 bytes', () => {
9
+ expect(formatSize(0)).toBe('0 B');
10
+ });
11
+ it('should format bytes', () => {
12
+ expect(formatSize(500)).toBe('500 B');
13
+ expect(formatSize(1023)).toBe('1023 B');
14
+ });
15
+ it('should format kilobytes', () => {
16
+ expect(formatSize(1024)).toBe('1.0 KB');
17
+ expect(formatSize(1536)).toBe('1.5 KB');
18
+ expect(formatSize(10240)).toBe('10.0 KB');
19
+ });
20
+ it('should format megabytes', () => {
21
+ expect(formatSize(1048576)).toBe('1.0 MB');
22
+ expect(formatSize(1572864)).toBe('1.5 MB');
23
+ });
24
+ it('should format gigabytes', () => {
25
+ expect(formatSize(1073741824)).toBe('1.0 GB');
26
+ expect(formatSize(2147483648)).toBe('2.0 GB');
27
+ });
28
+ it('should format terabytes', () => {
29
+ expect(formatSize(1099511627776)).toBe('1.0 TB');
30
+ });
31
+ });
32
+ describe('formatRelativeTime', () => {
33
+ beforeEach(() => {
34
+ vi.useFakeTimers();
35
+ vi.setSystemTime(new Date('2026-02-15T12:00:00Z'));
36
+ });
37
+ it('should return null for null input', () => {
38
+ expect(formatRelativeTime(null)).toBeNull();
39
+ });
40
+ it('should return null for future timestamps', () => {
41
+ const future = new Date('2026-02-15T13:00:00Z').toISOString();
42
+ expect(formatRelativeTime(future)).toBeNull();
43
+ });
44
+ it('should return "just now" for very recent timestamps', () => {
45
+ const recent = new Date('2026-02-15T11:59:30Z').toISOString();
46
+ expect(formatRelativeTime(recent)).toBe('just now');
47
+ });
48
+ it('should return minutes ago', () => {
49
+ const mins5 = new Date('2026-02-15T11:55:00Z').toISOString();
50
+ expect(formatRelativeTime(mins5)).toBe('5m ago');
51
+ const mins45 = new Date('2026-02-15T11:15:00Z').toISOString();
52
+ expect(formatRelativeTime(mins45)).toBe('45m ago');
53
+ });
54
+ it('should return hours ago', () => {
55
+ const hours2 = new Date('2026-02-15T10:00:00Z').toISOString();
56
+ expect(formatRelativeTime(hours2)).toBe('2h ago');
57
+ const hours12 = new Date('2026-02-15T00:00:00Z').toISOString();
58
+ expect(formatRelativeTime(hours12)).toBe('12h ago');
59
+ });
60
+ it('should return days ago', () => {
61
+ const days1 = new Date('2026-02-14T12:00:00Z').toISOString();
62
+ expect(formatRelativeTime(days1)).toBe('1d ago');
63
+ const days7 = new Date('2026-02-08T12:00:00Z').toISOString();
64
+ expect(formatRelativeTime(days7)).toBe('7d ago');
65
+ });
66
+ it('should prioritize days over hours', () => {
67
+ const days1Hours5 = new Date('2026-02-14T07:00:00Z').toISOString();
68
+ expect(formatRelativeTime(days1Hours5)).toBe('1d ago');
69
+ });
70
+ });
71
+ describe('nowIso', () => {
72
+ beforeEach(() => {
73
+ vi.useFakeTimers();
74
+ vi.setSystemTime(new Date('2026-02-15T12:34:56.789Z'));
75
+ });
76
+ it('should return current time in ISO format', () => {
77
+ expect(nowIso()).toBe('2026-02-15T12:34:56.789Z');
78
+ });
79
+ it('should match ISO 8601 format', () => {
80
+ expect(nowIso()).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/);
81
+ });
82
+ });
83
+ });
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Resolve the service package version by walking up from the caller's directory.
3
+ */
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ function findPackageJson(startDir) {
8
+ let dir = startDir;
9
+ while (dir !== path.dirname(dir)) {
10
+ const candidate = path.join(dir, 'package.json');
11
+ if (fs.existsSync(candidate)) {
12
+ const pkg = JSON.parse(fs.readFileSync(candidate, 'utf8'));
13
+ // Find our package specifically, not the monorepo root
14
+ if (pkg.name === '@karmaniverous/jeeves-server')
15
+ return candidate;
16
+ }
17
+ dir = path.dirname(dir);
18
+ }
19
+ throw new Error('Could not find @karmaniverous/jeeves-server package.json');
20
+ }
21
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
22
+ const pkgPath = findPackageJson(__dirname);
23
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
24
+ /** The package version of the jeeves-server service package. */
25
+ export const packageVersion = pkg.version;
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Platform abstraction for filesystem operations.
3
+ *
4
+ * Handles the differences between Windows (drive letters, backslashes)
5
+ * and Linux (mount points, forward slashes) for URL path ↔ filesystem path conversion.
6
+ */
7
+ import fs from 'node:fs';
8
+ import path from 'node:path';
9
+ const IS_WINDOWS = process.platform === 'win32';
10
+ /**
11
+ * Discover available filesystem roots.
12
+ * Windows: enumerate accessible drive letters.
13
+ * Linux: return configured roots or default to '/'.
14
+ */
15
+ export function getRoots(configuredRoots) {
16
+ if (IS_WINDOWS) {
17
+ const roots = [];
18
+ for (let code = 65; code <= 90; code++) {
19
+ const letter = String.fromCharCode(code);
20
+ const drivePath = `${letter}:\\`;
21
+ try {
22
+ fs.accessSync(drivePath, fs.constants.R_OK);
23
+ roots.push({
24
+ id: letter.toLowerCase(),
25
+ label: `${letter}:`,
26
+ fsPath: drivePath,
27
+ });
28
+ }
29
+ catch {
30
+ // Drive not accessible
31
+ }
32
+ }
33
+ return roots;
34
+ }
35
+ // Linux: use configured roots or default to filesystem root
36
+ if (configuredRoots && Object.keys(configuredRoots).length > 0) {
37
+ return Object.entries(configuredRoots).map(([id, fsPath]) => ({
38
+ id,
39
+ label: fsPath,
40
+ fsPath: fsPath.endsWith('/') ? fsPath : fsPath + '/',
41
+ }));
42
+ }
43
+ return [{ id: 'root', label: '/', fsPath: '/' }];
44
+ }
45
+ /**
46
+ * Convert a URL path to a filesystem path.
47
+ *
48
+ * URL paths use forward slashes and start with the root id:
49
+ * Windows: /e/jeeves-server/README.md → E:\\jeeves-server\\README.md
50
+ * Linux: /home/user/docs/README.md → /home/user/docs/README.md
51
+ * /root/docs/README.md → /docs/README.md (if root id is "root" mapping to "/")
52
+ */
53
+ export function urlPathToFs(urlPath, roots) {
54
+ const normalized = urlPath.replace(/^\/+/, '');
55
+ if (!normalized)
56
+ return null;
57
+ if (IS_WINDOWS) {
58
+ // First segment is the drive letter
59
+ const slashIdx = normalized.indexOf('/');
60
+ const driveLetter = slashIdx >= 0 ? normalized.substring(0, slashIdx) : normalized;
61
+ const rest = slashIdx >= 0 ? normalized.substring(slashIdx + 1) : '';
62
+ if (driveLetter.length !== 1)
63
+ return null;
64
+ const fsPath = `${driveLetter.toUpperCase()}:\\${rest.replace(/\//g, '\\')}`;
65
+ return fsPath;
66
+ }
67
+ // Linux: match against configured roots
68
+ for (const root of roots) {
69
+ if (normalized.startsWith(root.id + '/') || normalized === root.id) {
70
+ const rest = normalized.substring(root.id.length);
71
+ // rest starts with '/' or is empty
72
+ const fsPath = root.fsPath.replace(/\/+$/, '') + (rest || '/');
73
+ return fsPath;
74
+ }
75
+ }
76
+ return null;
77
+ }
78
+ /**
79
+ * Convert a filesystem path to a URL path.
80
+ *
81
+ * Windows: E:\\jeeves-server\\README.md → /e/jeeves-server/README.md
82
+ * Linux: /home/user/docs/README.md → /home/user/docs/README.md (root="root" → "/")
83
+ */
84
+ export function fsPathToUrl(fsPath, roots) {
85
+ if (IS_WINDOWS) {
86
+ return ('/' +
87
+ fsPath
88
+ .replace(/\\/g, '/')
89
+ .replace(/^([A-Za-z]):/, (_, d) => d.toLowerCase()));
90
+ }
91
+ // Linux: find the matching root and prepend the root id
92
+ for (const root of roots) {
93
+ const rootFs = root.fsPath.replace(/\/+$/, '');
94
+ if (fsPath === rootFs || fsPath.startsWith(rootFs + '/')) {
95
+ const rest = fsPath.substring(rootFs.length); // starts with '/' or is empty
96
+ if (root.id === 'root' && root.fsPath === '/') {
97
+ // Default root — URL path is just the fs path
98
+ return rest || '/';
99
+ }
100
+ return '/' + root.id + rest;
101
+ }
102
+ }
103
+ // Fallback: return as-is with forward slashes
104
+ return fsPath.replace(/\\/g, '/');
105
+ }
106
+ /**
107
+ * Split a filesystem path into breadcrumb parts.
108
+ * Returns [\{label, urlPath\}] from root to leaf.
109
+ */
110
+ export function breadcrumbParts(fsPath, roots) {
111
+ const urlPath = fsPathToUrl(fsPath, roots);
112
+ const parts = urlPath
113
+ .replace(/^\/+/, '')
114
+ .split('/')
115
+ .filter((p) => p);
116
+ return parts.map((_part, i) => {
117
+ const accumulated = parts.slice(0, i + 1).join('/');
118
+ return { label: parts[i], path: accumulated };
119
+ });
120
+ }
121
+ /**
122
+ * Recursively calculate total size of a directory in bytes.
123
+ */
124
+ export function getDirSize(dirPath) {
125
+ let totalSize = 0;
126
+ try {
127
+ const entries = fs.readdirSync(dirPath, { withFileTypes: true });
128
+ for (const entry of entries) {
129
+ const entryPath = path.join(dirPath, entry.name);
130
+ try {
131
+ if (entry.isDirectory()) {
132
+ totalSize += getDirSize(entryPath);
133
+ }
134
+ else {
135
+ const s = fs.statSync(entryPath);
136
+ totalSize += s.size;
137
+ }
138
+ }
139
+ catch {
140
+ /* skip inaccessible */
141
+ }
142
+ }
143
+ }
144
+ catch {
145
+ /* skip inaccessible */
146
+ }
147
+ return totalSize;
148
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Server state management (key rotation tracking, etc.)
3
+ */
4
+ import fs from 'node:fs';
5
+ import { getConfig } from '../config/index.js';
6
+ /**
7
+ * Load state from file
8
+ */
9
+ export function loadState() {
10
+ const { stateFile } = getConfig();
11
+ try {
12
+ if (fs.existsSync(stateFile)) {
13
+ const content = fs.readFileSync(stateFile, 'utf8');
14
+ return JSON.parse(content);
15
+ }
16
+ }
17
+ catch {
18
+ // Ignore errors, return empty state
19
+ }
20
+ return {};
21
+ }
22
+ /**
23
+ * Save state to file
24
+ */
25
+ export function saveState(state) {
26
+ const { stateFile } = getConfig();
27
+ fs.writeFileSync(stateFile, JSON.stringify(state, null, 2), 'utf8');
28
+ }
29
+ /**
30
+ * Set key rotation timestamp
31
+ */
32
+ export function setKeyRotationTimestamp(timestamp) {
33
+ const state = loadState();
34
+ state.keyRotatedAt = timestamp;
35
+ saveState(state);
36
+ }
37
+ /**
38
+ * Set an insider's auto-generated key in state
39
+ */
40
+ export function setInsiderKey(email, seed, createdAt) {
41
+ const state = loadState();
42
+ if (!state.insiderKeys)
43
+ state.insiderKeys = {};
44
+ state.insiderKeys[email.toLowerCase()] = { seed, createdAt };
45
+ saveState(state);
46
+ }
@@ -0,0 +1,12 @@
1
+ import { defineConfig } from 'vitest/config';
2
+ export default defineConfig({
3
+ test: {
4
+ globals: true,
5
+ environment: 'happy-dom',
6
+ exclude: ['dist/**', '**/node_modules/**'],
7
+ coverage: {
8
+ provider: 'v8',
9
+ reporter: ['text', 'lcov'],
10
+ },
11
+ },
12
+ });
package/favicon.svg ADDED
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
2
+ <text y="0.9em" font-size="90">🎩</text>
3
+ </svg>
@@ -0,0 +1,24 @@
1
+ flowchart TD
2
+ A["Request arrives"] --> B{"Has session cookie?\n(Google OAuth)"}
3
+ B -- Yes --> C{"Email in\ninsiders map?"}
4
+ C -- Yes --> D["✅ INSIDER access\n(within scopes)"]
5
+ C -- No --> E["❌ DENIED"]
6
+
7
+ B -- No --> F{"Has ?key=\nparameter?"}
8
+ F -- Yes --> G{"Matches machine\ninsider key?"}
9
+ G -- Yes --> H["✅ INSIDER access\n(within scopes)"]
10
+ G -- No --> I{"Matches outsider key?\n(machine or insider seed)"}
11
+ I -- Yes --> J{"Path matches key\npath or ancestor?"}
12
+ J -- Yes --> K["✅ OUTSIDER access"]
13
+ J -- No --> L["❌ DENIED"]
14
+ I -- No --> M["❌ DENIED"]
15
+
16
+ F -- No --> N["❌ DENIED\n(or redirect to Google login)"]
17
+
18
+ style D fill:#22c55e,color:#fff,stroke:#16a34a
19
+ style H fill:#22c55e,color:#fff,stroke:#16a34a
20
+ style K fill:#22c55e,color:#fff,stroke:#16a34a
21
+ style E fill:#ef4444,color:#fff,stroke:#dc2626
22
+ style L fill:#ef4444,color:#fff,stroke:#dc2626
23
+ style M fill:#ef4444,color:#fff,stroke:#dc2626
24
+ style N fill:#f59e0b,color:#fff,stroke:#d97706
@@ -0,0 +1 @@
1
+ <svg id="my-svg" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" class="flowchart" style="max-width: 1102.22px; background-color: white;" viewBox="0 0 1102.21875 1878.078125" role="graphics-document document" aria-roledescription="flowchart-v2"><style>#my-svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#my-svg .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#my-svg .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#my-svg .error-icon{fill:#552222;}#my-svg .error-text{fill:#552222;stroke:#552222;}#my-svg .edge-thickness-normal{stroke-width:1px;}#my-svg .edge-thickness-thick{stroke-width:3.5px;}#my-svg .edge-pattern-solid{stroke-dasharray:0;}#my-svg .edge-thickness-invisible{stroke-width:0;fill:none;}#my-svg .edge-pattern-dashed{stroke-dasharray:3;}#my-svg .edge-pattern-dotted{stroke-dasharray:2;}#my-svg .marker{fill:#333333;stroke:#333333;}#my-svg .marker.cross{stroke:#333333;}#my-svg svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#my-svg p{margin:0;}#my-svg .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#my-svg .cluster-label text{fill:#333;}#my-svg .cluster-label span{color:#333;}#my-svg .cluster-label span p{background-color:transparent;}#my-svg .label text,#my-svg span{fill:#333;color:#333;}#my-svg .node rect,#my-svg .node circle,#my-svg .node ellipse,#my-svg .node polygon,#my-svg .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#my-svg .rough-node .label text,#my-svg .node .label text,#my-svg .image-shape .label,#my-svg .icon-shape .label{text-anchor:middle;}#my-svg .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#my-svg .rough-node .label,#my-svg .node .label,#my-svg .image-shape .label,#my-svg .icon-shape .label{text-align:center;}#my-svg .node.clickable{cursor:pointer;}#my-svg .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#my-svg .arrowheadPath{fill:#333333;}#my-svg .edgePath .path{stroke:#333333;stroke-width:2.0px;}#my-svg .flowchart-link{stroke:#333333;fill:none;}#my-svg .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#my-svg .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#my-svg .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#my-svg .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#my-svg .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#my-svg .cluster text{fill:#333;}#my-svg .cluster span{color:#333;}#my-svg div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#my-svg .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#my-svg rect.text{fill:none;stroke-width:0;}#my-svg .icon-shape,#my-svg .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#my-svg .icon-shape p,#my-svg .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#my-svg .icon-shape rect,#my-svg .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#my-svg .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#my-svg .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#my-svg :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><marker id="my-svg_flowchart-v2-pointEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 0 L 10 5 L 0 10 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="my-svg_flowchart-v2-pointStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="4.5" refY="5" markerUnits="userSpaceOnUse" markerWidth="8" markerHeight="8" orient="auto"><path d="M 0 5 L 10 10 L 10 0 z" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="my-svg_flowchart-v2-circleEnd" class="marker flowchart-v2" viewBox="0 0 10 10" refX="11" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="my-svg_flowchart-v2-circleStart" class="marker flowchart-v2" viewBox="0 0 10 10" refX="-1" refY="5" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><circle cx="5" cy="5" r="5" class="arrowMarkerPath" style="stroke-width: 1; stroke-dasharray: 1, 0;"/></marker><marker id="my-svg_flowchart-v2-crossEnd" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="12" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"/></marker><marker id="my-svg_flowchart-v2-crossStart" class="marker cross flowchart-v2" viewBox="0 0 11 11" refX="-1" refY="5.2" markerUnits="userSpaceOnUse" markerWidth="11" markerHeight="11" orient="auto"><path d="M 1,1 l 9,9 M 10,1 l -9,9" class="arrowMarkerPath" style="stroke-width: 2; stroke-dasharray: 1, 0;"/></marker><g class="root"><g class="clusters"/><g class="edgePaths"><path d="M551.109,62L551.109,66.167C551.109,70.333,551.109,78.667,551.109,86.333C551.109,94,551.109,101,551.109,104.5L551.109,108" id="L_A_B_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_A_B_0" data-points="W3sieCI6NTUxLjEwOTM3NSwieSI6NjJ9LHsieCI6NTUxLjEwOTM3NSwieSI6ODd9LHsieCI6NTUxLjEwOTM3NSwieSI6MTEyfV0=" marker-end="url(#my-svg_flowchart-v2-pointEnd)"/><path d="M464.771,303.661L431.068,324.218C397.365,344.774,329.96,385.887,296.257,411.944C262.555,438,262.555,449,262.555,454.5L262.555,460" id="L_B_C_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_B_C_0" data-points="W3sieCI6NDY0Ljc3MDU1OTI2NTg0NiwieSI6MzAzLjY2MTE4NDI2NTg0Nn0seyJ4IjoyNjIuNTU0Njg3NSwieSI6NDI3fSx7IngiOjI2Mi41NTQ2ODc1LCJ5Ijo0NjR9XQ==" marker-end="url(#my-svg_flowchart-v2-pointEnd)"/><path d="M211.818,635.342L199.515,649.964C187.212,664.587,162.606,693.833,150.303,730.622C138,767.411,138,811.745,138,833.911L138,856.078" id="L_C_D_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_C_D_0" data-points="W3sieCI6MjExLjgxODI1MDEwOTI2NTczLCJ5Ijo2MzUuMzQxNjg3NjA5MjY1N30seyJ4IjoxMzgsInkiOjcyMy4wNzgxMjV9LHsieCI6MTM4LCJ5Ijo4NjAuMDc4MTI1fV0=" marker-end="url(#my-svg_flowchart-v2-pointEnd)"/><path d="M313.291,635.342L325.594,649.964C337.897,664.587,362.503,693.833,374.806,732.622C387.109,771.411,387.109,819.745,387.109,843.911L387.109,868.078" id="L_C_E_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_C_E_0" data-points="W3sieCI6MzEzLjI5MTEyNDg5MDczNDI0LCJ5Ijo2MzUuMzQxNjg3NjA5MjY1N30seyJ4IjozODcuMTA5Mzc1LCJ5Ijo3MjMuMDc4MTI1fSx7IngiOjM4Ny4xMDkzNzUsInkiOjg3Mi4wNzgxMjV9XQ==" marker-end="url(#my-svg_flowchart-v2-pointEnd)"/><path d="M633.165,307.945L661.757,327.787C690.349,347.63,747.534,387.315,776.126,412.941C804.719,438.568,804.719,450.135,804.719,455.919L804.719,461.703" id="L_B_F_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_B_F_0" data-points="W3sieCI6NjMzLjE2NDYyMTQwODQzNzksInkiOjMwNy45NDQ3NTM1OTE1NjIxfSx7IngiOjgwNC43MTg3NSwieSI6NDI3fSx7IngiOjgwNC43MTg3NSwieSI6NDY1LjcwMzEyNX1d" marker-end="url(#my-svg_flowchart-v2-pointEnd)"/><path d="M748.013,627.67L730.881,643.571C713.749,659.473,679.484,691.275,662.351,712.677C645.219,734.078,645.219,745.078,645.219,750.578L645.219,756.078" id="L_F_G_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_F_G_0" data-points="W3sieCI6NzQ4LjAxMzQ5MTUyMTY1NjMsInkiOjYyNy42Njk3NDE1MjE2NTYzfSx7IngiOjY0NS4yMTg3NSwieSI6NzIzLjA3ODEyNX0seyJ4Ijo2NDUuMjE4NzUsInkiOjc2MC4wNzgxMjV9XQ==" marker-end="url(#my-svg_flowchart-v2-pointEnd)"/><path d="M579.137,971.996L563.567,989.176C547.997,1006.357,516.858,1040.717,501.288,1080.064C485.719,1119.411,485.719,1163.745,485.719,1185.911L485.719,1208.078" id="L_G_H_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_G_H_0" data-points="W3sieCI6NTc5LjEzNjc4Mjc4Njg4NTIsInkiOjk3MS45OTYxNTc3ODY4ODUyfSx7IngiOjQ4NS43MTg3NSwieSI6MTA3NS4wNzgxMjV9LHsieCI6NDg1LjcxODc1LCJ5IjoxMjEyLjA3ODEyNX1d" marker-end="url(#my-svg_flowchart-v2-pointEnd)"/><path d="M711.301,971.996L726.87,989.176C742.44,1006.357,773.579,1040.717,789.149,1063.398C804.719,1086.078,804.719,1097.078,804.719,1102.578L804.719,1108.078" id="L_G_I_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_G_I_0" data-points="W3sieCI6NzExLjMwMDcxNzIxMzExNDgsInkiOjk3MS45OTYxNTc3ODY4ODUyfSx7IngiOjgwNC43MTg3NSwieSI6MTA3NS4wNzgxMjV9LHsieCI6ODA0LjcxODc1LCJ5IjoxMTEyLjA3ODEyNX1d" marker-end="url(#my-svg_flowchart-v2-pointEnd)"/><path d="M745.914,1331.274L734.206,1347.241C722.497,1363.208,699.081,1395.143,687.372,1416.611C675.664,1438.078,675.664,1449.078,675.664,1454.578L675.664,1460.078" id="L_I_J_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_I_J_0" data-points="W3sieCI6NzQ1LjkxNDIwNjc1NzI0MTIsInkiOjEzMzEuMjczNTgxNzU3MjQxM30seyJ4Ijo2NzUuNjY0MDYyNSwieSI6MTQyNy4wNzgxMjV9LHsieCI6Njc1LjY2NDA2MjUsInkiOjE0NjQuMDc4MTI1fV0=" marker-end="url(#my-svg_flowchart-v2-pointEnd)"/><path d="M621.753,1688.167L612.153,1703.319C602.553,1718.471,583.352,1748.774,573.752,1769.426C564.152,1790.078,564.152,1801.078,564.152,1806.578L564.152,1812.078" id="L_J_K_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_J_K_0" data-points="W3sieCI6NjIxLjc1Mjc2ODEyMzQxMjEsInkiOjE2ODguMTY2ODMwNjIzNDEyMX0seyJ4Ijo1NjQuMTUyMzQzNzUsInkiOjE3NzkuMDc4MTI1fSx7IngiOjU2NC4xNTIzNDM3NSwieSI6MTgxNi4wNzgxMjV9XQ==" marker-end="url(#my-svg_flowchart-v2-pointEnd)"/><path d="M729.575,1688.167L739.175,1703.319C748.775,1718.471,767.976,1748.774,777.576,1769.426C787.176,1790.078,787.176,1801.078,787.176,1806.578L787.176,1812.078" id="L_J_L_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_J_L_0" data-points="W3sieCI6NzI5LjU3NTM1Njg3NjU4NzksInkiOjE2ODguMTY2ODMwNjIzNDEyMX0seyJ4Ijo3ODcuMTc1NzgxMjUsInkiOjE3NzkuMDc4MTI1fSx7IngiOjc4Ny4xNzU3ODEyNSwieSI6MTgxNi4wNzgxMjV9XQ==" marker-end="url(#my-svg_flowchart-v2-pointEnd)"/><path d="M863.523,1331.274L875.232,1347.241C886.94,1363.208,910.357,1395.143,922.065,1435.277C933.773,1475.411,933.773,1523.745,933.773,1547.911L933.773,1572.078" id="L_I_M_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_I_M_0" data-points="W3sieCI6ODYzLjUyMzI5MzI0Mjc1ODgsInkiOjEzMzEuMjczNTgxNzU3MjQxM30seyJ4Ijo5MzMuNzczNDM3NSwieSI6MTQyNy4wNzgxMjV9LHsieCI6OTMzLjc3MzQzNzUsInkiOjE1NzYuMDc4MTI1fV0=" marker-end="url(#my-svg_flowchart-v2-pointEnd)"/><path d="M861.424,627.67L878.556,643.571C895.689,659.473,929.954,691.275,947.086,729.343C964.219,767.411,964.219,811.745,964.219,833.911L964.219,856.078" id="L_F_N_0" class="edge-thickness-normal edge-pattern-solid edge-thickness-normal edge-pattern-solid flowchart-link" style=";" data-edge="true" data-et="edge" data-id="L_F_N_0" data-points="W3sieCI6ODYxLjQyNDAwODQ3ODM0MzcsInkiOjYyNy42Njk3NDE1MjE2NTYzfSx7IngiOjk2NC4yMTg3NSwieSI6NzIzLjA3ODEyNX0seyJ4Ijo5NjQuMjE4NzUsInkiOjg2MC4wNzgxMjV9XQ==" marker-end="url(#my-svg_flowchart-v2-pointEnd)"/></g><g class="edgeLabels"><g class="edgeLabel"><g class="label" data-id="L_A_B_0" transform="translate(0, 0)"><foreignObject width="0" height="0"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"></span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(262.5546875, 427)"><g class="label" data-id="L_B_C_0" transform="translate(-11.328125, -12)"><foreignObject width="22.65625" height="24"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"><p>Yes</p></span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(138, 723.078125)"><g class="label" data-id="L_C_D_0" transform="translate(-11.328125, -12)"><foreignObject width="22.65625" height="24"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"><p>Yes</p></span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(387.109375, 723.078125)"><g class="label" data-id="L_C_E_0" transform="translate(-9.3984375, -12)"><foreignObject width="18.796875" height="24"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"><p>No</p></span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(804.71875, 427)"><g class="label" data-id="L_B_F_0" transform="translate(-9.3984375, -12)"><foreignObject width="18.796875" height="24"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"><p>No</p></span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(645.21875, 723.078125)"><g class="label" data-id="L_F_G_0" transform="translate(-11.328125, -12)"><foreignObject width="22.65625" height="24"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"><p>Yes</p></span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(485.71875, 1075.078125)"><g class="label" data-id="L_G_H_0" transform="translate(-11.328125, -12)"><foreignObject width="22.65625" height="24"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"><p>Yes</p></span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(804.71875, 1075.078125)"><g class="label" data-id="L_G_I_0" transform="translate(-9.3984375, -12)"><foreignObject width="18.796875" height="24"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"><p>No</p></span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(675.6640625, 1427.078125)"><g class="label" data-id="L_I_J_0" transform="translate(-11.328125, -12)"><foreignObject width="22.65625" height="24"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"><p>Yes</p></span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(564.15234375, 1779.078125)"><g class="label" data-id="L_J_K_0" transform="translate(-11.328125, -12)"><foreignObject width="22.65625" height="24"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"><p>Yes</p></span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(787.17578125, 1779.078125)"><g class="label" data-id="L_J_L_0" transform="translate(-9.3984375, -12)"><foreignObject width="18.796875" height="24"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"><p>No</p></span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(933.7734375, 1427.078125)"><g class="label" data-id="L_I_M_0" transform="translate(-9.3984375, -12)"><foreignObject width="18.796875" height="24"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"><p>No</p></span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(964.21875, 723.078125)"><g class="label" data-id="L_F_N_0" transform="translate(-9.3984375, -12)"><foreignObject width="18.796875" height="24"><div xmlns="http://www.w3.org/1999/xhtml" class="labelBkg" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="edgeLabel"><p>No</p></span></div></foreignObject></g></g></g><g class="nodes"><g class="node default" id="flowchart-A-0" transform="translate(551.109375, 35)"><rect class="basic label-container" style="" x="-84.9296875" y="-27" width="169.859375" height="54"/><g class="label" style="" transform="translate(-54.9296875, -12)"><rect/><foreignObject width="109.859375" height="24"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Request arrives</p></span></div></foreignObject></g></g><g class="node default" id="flowchart-B-1" transform="translate(551.109375, 251)"><polygon points="139,0 278,-139 139,-278 0,-139" class="label-container" transform="translate(-138.5, 139)"/><g class="label" style="" transform="translate(-100, -24)"><rect/><foreignObject width="200" height="48"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table; white-space: break-spaces; line-height: 1.5; max-width: 200px; text-align: center; width: 200px;"><span class="nodeLabel"><p>Has session cookie?\n(Google OAuth)</p></span></div></foreignObject></g></g><g class="node default" id="flowchart-C-3" transform="translate(262.5546875, 575.0390625)"><polygon points="111.0390625,0 222.078125,-111.0390625 111.0390625,-222.078125 0,-111.0390625" class="label-container" transform="translate(-110.5390625, 111.0390625)"/><g class="label" style="" transform="translate(-84.0390625, -12)"><rect/><foreignObject width="168.078125" height="24"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Email in\ninsiders map?</p></span></div></foreignObject></g></g><g class="node default" id="flowchart-D-5" transform="translate(138, 899.078125)"><rect class="basic label-container" style="fill:#22c55e !important;stroke:#16a34a !important" x="-130" y="-39" width="260" height="78"/><g class="label" style="color:#fff !important" transform="translate(-100, -24)"><rect/><foreignObject width="200" height="48"><div style="color: rgb(255, 255, 255) !important; display: table; white-space: break-spaces; line-height: 1.5; max-width: 200px; text-align: center; width: 200px;" xmlns="http://www.w3.org/1999/xhtml"><span style="color:#fff !important" class="nodeLabel"><p>✅ INSIDER access\n(within scopes)</p></span></div></foreignObject></g></g><g class="node default" id="flowchart-E-7" transform="translate(387.109375, 899.078125)"><rect class="basic label-container" style="fill:#ef4444 !important;stroke:#dc2626 !important" x="-69.109375" y="-27" width="138.21875" height="54"/><g class="label" style="color:#fff !important" transform="translate(-39.109375, -12)"><rect/><foreignObject width="78.21875" height="24"><div style="color: rgb(255, 255, 255) !important; display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;" xmlns="http://www.w3.org/1999/xhtml"><span style="color:#fff !important" class="nodeLabel"><p>❌ DENIED</p></span></div></foreignObject></g></g><g class="node default" id="flowchart-F-9" transform="translate(804.71875, 575.0390625)"><polygon points="109.3359375,0 218.671875,-109.3359375 109.3359375,-218.671875 0,-109.3359375" class="label-container" transform="translate(-108.8359375, 109.3359375)"/><g class="label" style="" transform="translate(-82.3359375, -12)"><rect/><foreignObject width="164.671875" height="24"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;"><span class="nodeLabel"><p>Has ?key=\nparameter?</p></span></div></foreignObject></g></g><g class="node default" id="flowchart-G-11" transform="translate(645.21875, 899.078125)"><polygon points="139,0 278,-139 139,-278 0,-139" class="label-container" transform="translate(-138.5, 139)"/><g class="label" style="" transform="translate(-100, -24)"><rect/><foreignObject width="200" height="48"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table; white-space: break-spaces; line-height: 1.5; max-width: 200px; text-align: center; width: 200px;"><span class="nodeLabel"><p>Matches machine\ninsider key?</p></span></div></foreignObject></g></g><g class="node default" id="flowchart-H-13" transform="translate(485.71875, 1251.078125)"><rect class="basic label-container" style="fill:#22c55e !important;stroke:#16a34a !important" x="-130" y="-39" width="260" height="78"/><g class="label" style="color:#fff !important" transform="translate(-100, -24)"><rect/><foreignObject width="200" height="48"><div style="color: rgb(255, 255, 255) !important; display: table; white-space: break-spaces; line-height: 1.5; max-width: 200px; text-align: center; width: 200px;" xmlns="http://www.w3.org/1999/xhtml"><span style="color:#fff !important" class="nodeLabel"><p>✅ INSIDER access\n(within scopes)</p></span></div></foreignObject></g></g><g class="node default" id="flowchart-I-15" transform="translate(804.71875, 1251.078125)"><polygon points="139,0 278,-139 139,-278 0,-139" class="label-container" transform="translate(-138.5, 139)"/><g class="label" style="" transform="translate(-100, -24)"><rect/><foreignObject width="200" height="48"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table; white-space: break-spaces; line-height: 1.5; max-width: 200px; text-align: center; width: 200px;"><span class="nodeLabel"><p>Matches outsider key?\n(machine or insider seed)</p></span></div></foreignObject></g></g><g class="node default" id="flowchart-J-17" transform="translate(675.6640625, 1603.078125)"><polygon points="139,0 278,-139 139,-278 0,-139" class="label-container" transform="translate(-138.5, 139)"/><g class="label" style="" transform="translate(-100, -24)"><rect/><foreignObject width="200" height="48"><div xmlns="http://www.w3.org/1999/xhtml" style="display: table; white-space: break-spaces; line-height: 1.5; max-width: 200px; text-align: center; width: 200px;"><span class="nodeLabel"><p>Path matches key\npath or ancestor?</p></span></div></foreignObject></g></g><g class="node default" id="flowchart-K-19" transform="translate(564.15234375, 1843.078125)"><rect class="basic label-container" style="fill:#22c55e !important;stroke:#16a34a !important" x="-103.9140625" y="-27" width="207.828125" height="54"/><g class="label" style="color:#fff !important" transform="translate(-73.9140625, -12)"><rect/><foreignObject width="147.828125" height="24"><div style="color: rgb(255, 255, 255) !important; display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;" xmlns="http://www.w3.org/1999/xhtml"><span style="color:#fff !important" class="nodeLabel"><p>✅ OUTSIDER access</p></span></div></foreignObject></g></g><g class="node default" id="flowchart-L-21" transform="translate(787.17578125, 1843.078125)"><rect class="basic label-container" style="fill:#ef4444 !important;stroke:#dc2626 !important" x="-69.109375" y="-27" width="138.21875" height="54"/><g class="label" style="color:#fff !important" transform="translate(-39.109375, -12)"><rect/><foreignObject width="78.21875" height="24"><div style="color: rgb(255, 255, 255) !important; display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;" xmlns="http://www.w3.org/1999/xhtml"><span style="color:#fff !important" class="nodeLabel"><p>❌ DENIED</p></span></div></foreignObject></g></g><g class="node default" id="flowchart-M-23" transform="translate(933.7734375, 1603.078125)"><rect class="basic label-container" style="fill:#ef4444 !important;stroke:#dc2626 !important" x="-69.109375" y="-27" width="138.21875" height="54"/><g class="label" style="color:#fff !important" transform="translate(-39.109375, -12)"><rect/><foreignObject width="78.21875" height="24"><div style="color: rgb(255, 255, 255) !important; display: table-cell; white-space: nowrap; line-height: 1.5; max-width: 200px; text-align: center;" xmlns="http://www.w3.org/1999/xhtml"><span style="color:#fff !important" class="nodeLabel"><p>❌ DENIED</p></span></div></foreignObject></g></g><g class="node default" id="flowchart-N-25" transform="translate(964.21875, 899.078125)"><rect class="basic label-container" style="fill:#f59e0b !important;stroke:#d97706 !important" x="-130" y="-39" width="260" height="78"/><g class="label" style="color:#fff !important" transform="translate(-100, -24)"><rect/><foreignObject width="200" height="48"><div style="color: rgb(255, 255, 255) !important; display: table; white-space: break-spaces; line-height: 1.5; max-width: 200px; text-align: center; width: 200px;" xmlns="http://www.w3.org/1999/xhtml"><span style="color:#fff !important" class="nodeLabel"><p>❌ DENIED\n(or redirect to Google login)</p></span></div></foreignObject></g></g></g></g></g></svg>
@@ -0,0 +1,236 @@
1
+ # API & Integration Guide
2
+
3
+ How to interact with Jeeves Server programmatically — for scripts, bots, AI assistants, and CI/CD pipelines.
4
+
5
+ ## Authentication for API Access
6
+
7
+ All API requests authenticate via `?key=<insider-key>` URL parameter or session cookie. For programmatic access, use a key:
8
+
9
+ ```typescript
10
+ // In jeeves.config.ts
11
+ keys: {
12
+ 'ci-bot': 'random-seed-string',
13
+ // Scoped key for webhooks only:
14
+ 'webhook': { key: 'another-seed', scopes: ['/event'] },
15
+ },
16
+ ```
17
+
18
+ ### Getting the derived key
19
+
20
+ The config contains **seeds**. The actual URL key is derived via HMAC:
21
+
22
+ ```bash
23
+ # Get the insider key from a seed
24
+ curl -s "http://localhost:1934/insider-key" -H "X-API-Key: <seed>"
25
+ # Returns: { "key": "a1b2c3d4..." }
26
+ ```
27
+
28
+ Or compute it yourself:
29
+
30
+ ```javascript
31
+ const crypto = require('crypto');
32
+ function insiderKey(seed) {
33
+ return crypto.createHmac('sha256', seed).update('insider').digest('hex').substring(0, 32);
34
+ }
35
+ ```
36
+
37
+ ## API Endpoints
38
+
39
+ ### File Access
40
+
41
+ ```bash
42
+ # Get file content (rendered)
43
+ GET /api/file/d/docs/design.md?key=<key>
44
+ # Returns: { type: "markdown", html: "...", headings: [...], content: "...", fileName: "..." }
45
+
46
+ # Get file content (raw text)
47
+ GET /api/file/d/docs/design.md?key=<key>&mode=raw
48
+ # Returns: { type: "text", content: "...", fileName: "..." }
49
+
50
+ # Get raw file bytes
51
+ GET /path/d/docs/design.md?key=<key>&raw=1
52
+ # Returns: file content with appropriate Content-Type
53
+
54
+ # Export as PDF
55
+ GET /path/d/docs/design.md?key=<key>&export=pdf
56
+ # Returns: application/pdf
57
+
58
+ # Export as DOCX
59
+ GET /path/d/docs/design.md?key=<key>&export=docx
60
+ # Returns: application/vnd.openxmlformats-officedocument.wordprocessingml.document
61
+ ```
62
+
63
+ ### Directory Listing
64
+
65
+ ```bash
66
+ # List drives
67
+ GET /api/drives?key=<key>
68
+ # Returns: { drives: [{ letter: "C", label: "System", ... }] }
69
+
70
+ # List directory
71
+ GET /api/directory/d/docs?key=<key>
72
+ # Returns: { path: "d/docs", entries: [{ name: "...", type: "file"|"directory", ... }] }
73
+ ```
74
+
75
+ ### Authentication
76
+
77
+ ```bash
78
+ # Check auth status
79
+ GET /api/auth/status?key=<key>
80
+ # Returns: { authenticated: true, email: "...", isInsider: true, mode: "key" }
81
+ ```
82
+
83
+ ### Sharing
84
+
85
+ ```bash
86
+ # Get insider key (requires X-API-Key header with seed)
87
+ GET /insider-key
88
+ # Headers: X-API-Key: <seed>
89
+ # Returns: { key: "a1b2c3d4..." }
90
+
91
+ # Compute outsider key for a path
92
+ GET /key?path=/d/docs/design.md
93
+ # Headers: X-API-Key: <seed>
94
+ # Returns: { key: "e5f6a7b8..." }
95
+
96
+ # Rotate a key
97
+ POST /rotate-key
98
+ # Body: { key: "<current-insider-key>" }
99
+ ```
100
+
101
+ ### Event Gateway
102
+
103
+ ```bash
104
+ # Send a webhook
105
+ POST /event?key=<webhook-key>
106
+ Content-Type: application/json
107
+ Body: { "type": "page.content_updated", "data": { "page_id": "abc123" } }
108
+ # Returns: { matched: "notion-page-update" } or { matched: null }
109
+ ```
110
+
111
+ ### Health
112
+
113
+ ```bash
114
+ GET /health
115
+ # Returns: 200 OK (no auth required)
116
+ ```
117
+
118
+ ## Converting Windows Paths to URLs
119
+
120
+ Jeeves Server maps Windows filesystem paths to URL paths:
121
+
122
+ ```
123
+ D:\docs\design.md → /d/docs/design.md
124
+ E:\projects\foo → /e/projects/foo
125
+ ```
126
+
127
+ **Conversion formula:**
128
+ 1. Replace backslashes with forward slashes
129
+ 2. Replace the drive letter + colon with lowercase letter
130
+ 3. Prepend the route prefix (`/path/` for legacy, `/browse/` for SPA, `/api/file/` for API)
131
+
132
+ ```javascript
133
+ function winPathToUrl(winPath, prefix = '/path/') {
134
+ const urlPath = winPath
135
+ .replace(/\\/g, '/')
136
+ .replace(/^([A-Z]):/, (_, d) => d.toLowerCase());
137
+ return `${prefix}${urlPath}`;
138
+ }
139
+
140
+ // D:\docs\design.md → /path/d/docs/design.md
141
+ // D:\docs\design.md → /browse/d/docs/design.md
142
+ // D:\docs\design.md → /api/file/d/docs/design.md
143
+ ```
144
+
145
+ ```powershell
146
+ # PowerShell equivalent
147
+ function Convert-ToJeevesUrl {
148
+ param([string]$Path, [string]$Prefix = '/path/')
149
+ $urlPath = $Path -replace '\\','/' -replace '^([A-Z]):',{ $_.Groups[1].Value.ToLower() }
150
+ return "${Prefix}${urlPath}"
151
+ }
152
+ ```
153
+
154
+ ## Generating Share Links
155
+
156
+ ### Insider links
157
+
158
+ ```javascript
159
+ const insiderKey = computeInsiderKey(seed);
160
+ const url = `https://jeeves.example.com/browse/d/docs/design.md?key=${insiderKey}`;
161
+ ```
162
+
163
+ ### Outsider links (path-scoped)
164
+
165
+ ```javascript
166
+ const crypto = require('crypto');
167
+
168
+ function outsiderKey(seed, path) {
169
+ const normalized = path.toLowerCase().replace(/^\/+|\/+$/g, '');
170
+ return crypto.createHmac('sha256', seed).update(normalized).digest('hex').substring(0, 32);
171
+ }
172
+
173
+ function outsiderKeyWithExpiry(seed, path, expiryMs) {
174
+ const normalized = path.toLowerCase().replace(/^\/+|\/+$/g, '');
175
+ const data = `${normalized}|${expiryMs}`;
176
+ return crypto.createHmac('sha256', seed).update(data).digest('hex').substring(0, 32);
177
+ }
178
+
179
+ // Non-expiring outsider link
180
+ const key = outsiderKey(seed, 'd/docs/design.md');
181
+ const url = `https://jeeves.example.com/browse/d/docs/design.md?key=${key}`;
182
+
183
+ // Expiring outsider link (1 week)
184
+ const expiry = Date.now() + 7 * 24 * 60 * 60 * 1000;
185
+ const key = outsiderKeyWithExpiry(seed, 'd/docs/design.md', expiry);
186
+ const url = `https://jeeves.example.com/browse/d/docs/design.md?key=${key}&exp=${expiry}`;
187
+ ```
188
+
189
+ ### Directory links
190
+
191
+ Outsider keys for directories grant access to all descendants:
192
+
193
+ ```javascript
194
+ // Share an entire directory
195
+ const key = outsiderKey(seed, 'd/projects/client-x');
196
+ const url = `https://jeeves.example.com/browse/d/projects/client-x?key=${key}`;
197
+ // Grants access to all files under D:\projects\client-x\
198
+ ```
199
+
200
+ ## For AI Assistants
201
+
202
+ If you're an AI assistant working with Jeeves Server, here's what you need to know:
203
+
204
+ ### Generating links to share with humans
205
+
206
+ When your human asks you to share a document:
207
+
208
+ 1. **Convert the Windows path** to a URL path (see above)
209
+ 2. **Use the insider key** for team members, or generate an outsider key for external recipients
210
+ 3. **Choose the right route**: `/browse/` for browser viewing, `/path/` with `?export=pdf` for direct PDF download
211
+
212
+ ### Authoring documents
213
+
214
+ Write Markdown files to the server's filesystem. Jeeves Server will render them beautifully. You can:
215
+ - Embed Mermaid diagrams (rendered inline)
216
+ - Embed SVG files (rendered with pan/zoom)
217
+ - Use code blocks with language hints (syntax highlighted)
218
+ - Reference other local files with relative paths
219
+
220
+ ### Checking server status
221
+
222
+ ```bash
223
+ curl -s http://localhost:1934/health
224
+ ```
225
+
226
+ ### Triggering webhooks
227
+
228
+ If you need to trigger an action via the event gateway:
229
+
230
+ ```bash
231
+ curl -X POST "http://localhost:1934/event?key=<webhook-key>" \
232
+ -H "Content-Type: application/json" \
233
+ -d '{"action": "rebuild", "target": "docs"}'
234
+ ```
235
+
236
+ Match this against a configured event schema to dispatch your handler.