@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,318 @@
1
+ /**
2
+ * Validate deep share link behavior end-to-end.
3
+ *
4
+ * Tests:
5
+ * 1. Internal links work up to depth limit and not beyond
6
+ * 2. Directory option limits directory access and includes dirs in depth
7
+ * 3. External links always work (never rewritten)
8
+ *
9
+ * Usage: node test-docs/validate-deep-share.js
10
+ * Requires dev server running on port 3457.
11
+ */
12
+
13
+ const BASE = 'http://localhost:3457';
14
+ const INSIDER_KEY = '90db6073c9ee033c10745aa794f70fc5';
15
+
16
+ let pass = 0;
17
+ let fail = 0;
18
+
19
+ function assert(condition, label) {
20
+ if (condition) {
21
+ console.log(` ✅ ${label}`);
22
+ pass++;
23
+ } else {
24
+ console.log(` ❌ ${label}`);
25
+ fail++;
26
+ }
27
+ }
28
+
29
+ async function fetchJson(url) {
30
+ const res = await fetch(url, { redirect: 'manual' });
31
+ return { status: res.status, data: res.ok ? await res.json() : null, headers: res.headers };
32
+ }
33
+
34
+ async function getShareLink(path, depth, dirs) {
35
+ const res = await fetch(`${BASE}/api/share?key=${INSIDER_KEY}`, {
36
+ method: 'POST',
37
+ headers: { 'Content-Type': 'application/json' },
38
+ body: JSON.stringify({ path, depth, dirs }),
39
+ });
40
+ return res.json();
41
+ }
42
+
43
+ /** Parse a share URL into its components */
44
+ function parseShareUrl(url) {
45
+ const u = new URL(url, BASE);
46
+ return {
47
+ path: u.pathname.replace('/browse', ''),
48
+ key: u.searchParams.get('key'),
49
+ d: u.searchParams.get('d'),
50
+ dirs: u.searchParams.get('dirs'),
51
+ s: u.searchParams.get('s'),
52
+ exp: u.searchParams.get('exp'),
53
+ };
54
+ }
55
+
56
+ /** Build API file URL from share params */
57
+ function apiFileUrl(params) {
58
+ let url = `${BASE}/api/file${params.path}?key=${params.key}`;
59
+ if (params.d !== null) url += `&d=${params.d}`;
60
+ if (params.dirs !== null) url += `&dirs=${params.dirs}`;
61
+ if (params.s !== null) url += `&s=${params.s}`;
62
+ if (params.exp !== null) url += `&exp=${params.exp}`;
63
+ return url;
64
+ }
65
+
66
+ /** Build API path (directory) URL from share params */
67
+ function apiDirUrl(dirPath, params) {
68
+ let url = `${BASE}/api/path${dirPath}?key=${params.key}`;
69
+ if (params.d !== null) url += `&d=${params.d}`;
70
+ if (params.dirs !== null) url += `&dirs=${params.dirs}`;
71
+ if (params.s !== null) url += `&s=${params.s}`;
72
+ if (params.exp !== null) url += `&exp=${params.exp}`;
73
+ return url;
74
+ }
75
+
76
+ /** Extract internal links (rewritten /browse/ links) from HTML */
77
+ function extractInternalLinks(html) {
78
+ const links = [];
79
+ const re = /<a\s+[^>]*?href="(\/browse\/[^"]*)"[^>]*>([\s\S]*?)<\/a>/gi;
80
+ let m;
81
+ while ((m = re.exec(html)) !== null) {
82
+ links.push({ url: m[1], text: m[2] });
83
+ }
84
+ return links;
85
+ }
86
+
87
+ /** Extract external links from HTML */
88
+ function extractExternalLinks(html) {
89
+ const links = [];
90
+ const re = /<a\s+[^>]*?href="(https?:\/\/[^"]*)"[^>]*>([\s\S]*?)<\/a>/gi;
91
+ let m;
92
+ while ((m = re.exec(html)) !== null) {
93
+ links.push({ url: m[1], text: m[2] });
94
+ }
95
+ return links;
96
+ }
97
+
98
+ /** Extract dead (stripped) link texts — text that was a link but is now plain */
99
+ function extractDeadLinkTexts(html) {
100
+ // Dead links become plain text (no <a> wrapper). We look for known link texts
101
+ // that are NOT inside <a> tags.
102
+ return html;
103
+ }
104
+
105
+ async function runTests() {
106
+ console.log('\n🔗 Deep Share Link Validation\n');
107
+
108
+ // ================================================================
109
+ // TEST 1: Depth 0 — no internal links should work
110
+ // ================================================================
111
+ console.log('━━━ Test 1: Depth 0 (no link following) ━━━');
112
+ {
113
+ const share = await getShareLink('/e/dev/karmaniverous/jeeves-server/test-docs/page-a.md', 0, false);
114
+ const params = parseShareUrl(share.url);
115
+
116
+ // Page A should load
117
+ const fileRes = await fetchJson(apiFileUrl(params));
118
+ assert(fileRes.status === 200, 'Page A loads with depth-0 key');
119
+ assert(fileRes.data?.isInsider === false, 'Page A shows as outsider');
120
+
121
+ // HTML should have NO internal links (all stripped)
122
+ const html = fileRes.data?.html || '';
123
+ const internalLinks = extractInternalLinks(html);
124
+ assert(internalLinks.length === 0, `No internal links at depth 0 (found ${internalLinks.length})`);
125
+
126
+ // External links should still be present
127
+ const externalLinks = extractExternalLinks(html);
128
+ assert(externalLinks.length > 0, `External links preserved (found ${externalLinks.length})`);
129
+ assert(externalLinks.some(l => l.url.includes('google.com')), 'Google link preserved');
130
+ }
131
+
132
+ // ================================================================
133
+ // TEST 2: Depth 1 — one hop of internal links
134
+ // ================================================================
135
+ console.log('\n━━━ Test 2: Depth 1 (one hop) ━━━');
136
+ {
137
+ const share = await getShareLink('/e/dev/karmaniverous/jeeves-server/test-docs/page-a.md', 1, false);
138
+ const params = parseShareUrl(share.url);
139
+
140
+ // Page A should load
141
+ const fileRes = await fetchJson(apiFileUrl(params));
142
+ assert(fileRes.status === 200, 'Page A loads with depth-1 key');
143
+
144
+ const html = fileRes.data?.html || '';
145
+ const internalLinks = extractInternalLinks(html);
146
+
147
+ // Page B link should be live (depth 1)
148
+ const pageBLink = internalLinks.find(l => l.text.includes('Page B'));
149
+ assert(!!pageBLink, 'Page B link is live at depth 1');
150
+
151
+ // Sub Page (page-d in subdirectory) should be dead if dirs=false
152
+ const subPageLink = internalLinks.find(l => l.text.includes('Sub Page'));
153
+ // Actually sub/page-d.md is a file, not a directory — it should be live
154
+ // The dirs flag controls directory listings, not files in subdirectories
155
+ // Let's check what we get
156
+ console.log(` ℹ️ Sub Page link present: ${!!subPageLink}`);
157
+
158
+ // External links always present
159
+ const externalLinks = extractExternalLinks(html);
160
+ assert(externalLinks.some(l => l.url.includes('google.com')), 'External links preserved');
161
+
162
+ // Follow Page B link — should work
163
+ if (pageBLink) {
164
+ const pageBParams = parseShareUrl(pageBLink.url);
165
+ const pageBRes = await fetchJson(apiFileUrl(pageBParams));
166
+ assert(pageBRes.status === 200, 'Page B loads via depth-1 link');
167
+
168
+ // Page B's internal links should be dead (depth exhausted)
169
+ const pageBHtml = pageBRes.data?.html || '';
170
+ const pageBInternalLinks = extractInternalLinks(pageBHtml);
171
+ assert(pageBInternalLinks.length === 0, `Page B has no live internal links (depth exhausted, found ${pageBInternalLinks.length})`);
172
+
173
+ // Page B's external links should still work
174
+ const pageBExternalLinks = extractExternalLinks(pageBHtml);
175
+ assert(pageBExternalLinks.some(l => l.url.includes('github.com')), 'Page B external links preserved');
176
+ }
177
+ }
178
+
179
+ // ================================================================
180
+ // TEST 3: Depth 2 — two hops
181
+ // ================================================================
182
+ console.log('\n━━━ Test 3: Depth 2 (two hops) ━━━');
183
+ {
184
+ const share = await getShareLink('/e/dev/karmaniverous/jeeves-server/test-docs/page-a.md', 2, false);
185
+ const params = parseShareUrl(share.url);
186
+
187
+ const fileRes = await fetchJson(apiFileUrl(params));
188
+ assert(fileRes.status === 200, 'Page A loads with depth-2 key');
189
+
190
+ const html = fileRes.data?.html || '';
191
+ const internalLinks = extractInternalLinks(html);
192
+ const pageBLink = internalLinks.find(l => l.text.includes('Page B'));
193
+ assert(!!pageBLink, 'Page B link is live at depth 2');
194
+
195
+ // Follow to Page B
196
+ if (pageBLink) {
197
+ const pageBParams = parseShareUrl(pageBLink.url);
198
+ const pageBRes = await fetchJson(apiFileUrl(pageBParams));
199
+ assert(pageBRes.status === 200, 'Page B loads (hop 1)');
200
+
201
+ const pageBHtml = pageBRes.data?.html || '';
202
+ const pageBLinks = extractInternalLinks(pageBHtml);
203
+ const pageCLink = pageBLinks.find(l => l.text.includes('Page C'));
204
+ assert(!!pageCLink, 'Page C link is live from Page B (hop 2)');
205
+
206
+ // Follow to Page C
207
+ if (pageCLink) {
208
+ const pageCParams = parseShareUrl(pageCLink.url);
209
+ const pageCRes = await fetchJson(apiFileUrl(pageCParams));
210
+ assert(pageCRes.status === 200, 'Page C loads (hop 2)');
211
+
212
+ // Page C's internal links should be dead (depth exhausted)
213
+ const pageCHtml = pageCRes.data?.html || '';
214
+ const pageCLinks = extractInternalLinks(pageCHtml);
215
+ assert(pageCLinks.length === 0, `Page C has no live internal links (depth exhausted, found ${pageCLinks.length})`);
216
+
217
+ // External links on Page C
218
+ const pageCExternalLinks = extractExternalLinks(pageCHtml);
219
+ assert(pageCExternalLinks.some(l => l.url.includes('wikipedia.org')), 'Page C external links preserved');
220
+ }
221
+ }
222
+ }
223
+
224
+ // ================================================================
225
+ // TEST 4: Depth 1, dirs=false — directory listing blocked
226
+ // ================================================================
227
+ console.log('\n━━━ Test 4: Depth 1, dirs=false (directory access blocked) ━━━');
228
+ {
229
+ const share = await getShareLink('/e/dev/karmaniverous/jeeves-server/test-docs/page-a.md', 1, false);
230
+ const params = parseShareUrl(share.url);
231
+
232
+ // Try to access the directory listing
233
+ const dirRes = await fetchJson(apiDirUrl('/e/dev/karmaniverous/jeeves-server/test-docs/', params));
234
+ assert(dirRes.status === 401, `Directory listing blocked with dirs=false (got ${dirRes.status})`);
235
+ }
236
+
237
+ // ================================================================
238
+ // TEST 5: Depth 1, dirs=true — directory listing allowed
239
+ // ================================================================
240
+ console.log('\n━━━ Test 5: Depth 1, dirs=true (directory access) ━━━');
241
+ {
242
+ const share = await getShareLink('/e/dev/karmaniverous/jeeves-server/test-docs/page-a.md', 1, true);
243
+ const params = parseShareUrl(share.url);
244
+
245
+ // Page A should load
246
+ const fileRes = await fetchJson(apiFileUrl(params));
247
+ assert(fileRes.status === 200, 'Page A loads with dirs=true');
248
+
249
+ // Internal file links should still be live
250
+ const html = fileRes.data?.html || '';
251
+ const internalLinks = extractInternalLinks(html);
252
+ const pageBLink = internalLinks.find(l => l.text.includes('Page B'));
253
+ assert(!!pageBLink, 'Page B link is live with dirs=true');
254
+
255
+ // Parent directory listing should work
256
+ const parentDirRes = await fetchJson(apiDirUrl('/e/dev/karmaniverous/jeeves-server/test-docs', params));
257
+ assert(parentDirRes.status === 200, 'Parent directory listing works with dirs=true');
258
+
259
+ // Subdirectory listing should work
260
+ const subDirRes = await fetchJson(apiDirUrl('/e/dev/karmaniverous/jeeves-server/test-docs/sub', params));
261
+ assert(subDirRes.status === 200, 'Subdirectory listing works with dirs=true');
262
+
263
+ // Any directory accessible (scoped only by sharer's access)
264
+ const ancestorDirRes = await fetchJson(apiDirUrl('/e/dev', params));
265
+ assert(ancestorDirRes.status === 200, `Ancestor dir accessible with dirs=true (got ${ancestorDirRes.status})`);
266
+
267
+ // Sibling directory accessible too
268
+ const siblingDirRes = await fetchJson(apiDirUrl('/e/dev/karmaniverous/jeeves-server/src', params));
269
+ assert(siblingDirRes.status === 200, `Sibling dir accessible with dirs=true (got ${siblingDirRes.status})`);
270
+ }
271
+
272
+ // ================================================================
273
+ // TEST 6: Backward compatibility — no depth/dirs params
274
+ // ================================================================
275
+ console.log('\n━━━ Test 6: Backward compat (no depth/dirs) ━━━');
276
+ {
277
+ const share = await getShareLink('/e/dev/karmaniverous/jeeves-server/test-docs/page-a.md', 0, false);
278
+ const params = parseShareUrl(share.url);
279
+
280
+ // Should be a legacy share link (no d/dirs/s params)
281
+ assert(params.d === null, 'No d param in legacy share');
282
+ assert(params.s === null, 'No s param in legacy share');
283
+
284
+ const fileRes = await fetchJson(apiFileUrl(params));
285
+ assert(fileRes.status === 200, 'Legacy share link works');
286
+ }
287
+
288
+ // ================================================================
289
+ // TEST 7: Cross-page key isolation — key from Page A can't access Page B directly
290
+ // ================================================================
291
+ console.log('\n━━━ Test 7: Key isolation (depth-0 key can\'t access other pages) ━━━');
292
+ {
293
+ const share = await getShareLink('/e/dev/karmaniverous/jeeves-server/test-docs/page-a.md', 0, false);
294
+ const params = parseShareUrl(share.url);
295
+
296
+ // Try to use Page A's key to access Page B directly
297
+ const pageBUrl = `${BASE}/api/file/e/dev/karmaniverous/jeeves-server/test-docs/page-b.md?key=${params.key}`;
298
+ const pageBRes = await fetchJson(pageBUrl);
299
+ assert(pageBRes.status === 401, `Page A's depth-0 key can't access Page B (got ${pageBRes.status})`);
300
+ }
301
+
302
+ // ================================================================
303
+ // Summary
304
+ // ================================================================
305
+ console.log(`\n${'━'.repeat(50)}`);
306
+ console.log(`Results: ${pass} passed, ${fail} failed out of ${pass + fail} total`);
307
+ if (fail > 0) {
308
+ console.log('⚠️ Some tests failed!');
309
+ process.exit(1);
310
+ } else {
311
+ console.log('✅ All tests passed!');
312
+ }
313
+ }
314
+
315
+ runTests().catch((err) => {
316
+ console.error('Fatal error:', err);
317
+ process.exit(1);
318
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "compilerOptions": {
3
+ "esModuleInterop": true,
4
+ "forceConsistentCasingInFileNames": true,
5
+ "incremental": true,
6
+ "isolatedModules": true,
7
+ "lib": [
8
+ "ES2022",
9
+ "DOM"
10
+ ],
11
+ "module": "ESNext",
12
+ "moduleResolution": "Bundler",
13
+ "outDir": "dist",
14
+ "resolveJsonModule": true,
15
+ "skipLibCheck": true,
16
+ "strict": true,
17
+ "target": "ES2022",
18
+ "tsBuildInfoFile": "./.tsbuildinfo",
19
+ "types": [
20
+ "node",
21
+ "vitest/globals"
22
+ ],
23
+ "rootDir": "."
24
+ },
25
+ "exclude": [
26
+ "coverage",
27
+ "dist",
28
+ "node_modules",
29
+ "logs",
30
+ "server.js",
31
+ "jeeves.config.template.ts"
32
+ ],
33
+ "include": [
34
+ "src/**/*",
35
+ "*.config.ts"
36
+ ]
37
+ }
package/tsdoc.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "$schema": "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json",
3
+ "tagDefinitions": [
4
+ {
5
+ "tagName": "@module",
6
+ "syntaxKind": "modifier"
7
+ },
8
+ {
9
+ "tagName": "@yields",
10
+ "syntaxKind": "block"
11
+ }
12
+ ]
13
+ }
@@ -0,0 +1 @@
1
+ v1.2026.2
Binary file
@@ -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
+ });
@@ -0,0 +1,13 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ globals: true,
6
+ environment: 'happy-dom',
7
+ exclude: ['dist/**', '**/node_modules/**'],
8
+ coverage: {
9
+ provider: 'v8',
10
+ reporter: ['text', 'lcov'],
11
+ },
12
+ },
13
+ });