@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,204 @@
1
+ # Insiders, Outsiders & Sharing
2
+
3
+ Jeeves Server has a clear access model built around two roles: **insiders** and **outsiders**. Understanding the difference is key to using sharing effectively.
4
+
5
+ ## Insiders
6
+
7
+ An insider is an **authenticated user** on the server. Depending on their configuration, they may have full access or be scoped to specific paths. Within their scope, they can navigate directories, view files, and generate share links for others.
8
+
9
+ ### Who is an insider?
10
+
11
+ Anyone listed in the `insiders` map in your config:
12
+
13
+ ```typescript
14
+ insiders: {
15
+ 'alice@example.com': {},
16
+ 'bob@example.com': { scopes: ['/d/projects/*'] },
17
+ },
18
+ ```
19
+
20
+ ### How insiders authenticate
21
+
22
+ Depends on which auth modes are active:
23
+
24
+ - **Google OAuth** (`'google'` mode) — Insider logs in with Google. The server checks their email against the `insiders` map. On first login, a key seed is auto-generated and stored in `state.json`.
25
+ - **Key auth** (`'keys'` mode) — Insider uses a derived URL key (`?key=<insider-key>`). The key is derived from a configured seed via HMAC-SHA256.
26
+
27
+ ### What insiders can do
28
+
29
+ - Browse all drives and directories (within their scopes)
30
+ - View rendered Markdown, code, SVG, Mermaid diagrams, and images
31
+ - Switch between Rendered and Raw views
32
+ - Export files as PDF, DOCX, or ZIP
33
+ - Copy insider links (for other insiders)
34
+ - Generate outsider links (for external sharing)
35
+ - Rotate their key (invalidating all their outsider links)
36
+
37
+ ### Scoped insiders
38
+
39
+ Scopes restrict which paths an insider can access. Three formats are supported:
40
+
41
+ **Allow-only** (string array — backward compatible):
42
+ ```typescript
43
+ 'contractor@example.com': {
44
+ scopes: ['/d/projects/client-x/*'],
45
+ },
46
+ ```
47
+
48
+ **Allow with deny** (broad access with cutouts):
49
+ ```typescript
50
+ 'team-member@example.com': {
51
+ scopes: {
52
+ allow: ['/d/*'],
53
+ deny: ['/d/secrets/*', '/d/.private/*'],
54
+ },
55
+ },
56
+ ```
57
+
58
+ **Deny-only** (everything except exclusions):
59
+ ```typescript
60
+ 'almost-full@example.com': {
61
+ scopes: {
62
+ deny: ['/d/hr/*', '/d/finance/*'],
63
+ },
64
+ },
65
+ ```
66
+
67
+ **Semantics:**
68
+ - A path must match at least one allow rule **and** not match any deny rule
69
+ - Omitting `allow` = implicit `['/**']` (allow everything)
70
+ - Omitting `deny` = no exclusions
71
+ - Omitting scopes entirely = **full access** (unchanged)
72
+
73
+ ---
74
+
75
+ ## Outsiders
76
+
77
+ An outsider is someone viewing a **specific file or directory** via a share link. They can see exactly what was shared — nothing more.
78
+
79
+ ### What outsiders can do
80
+
81
+ - View the shared file (rendered Markdown, code, images, etc.)
82
+ - Download the file (raw, PDF, or DOCX)
83
+ - Navigate within a shared directory (if a directory link was shared)
84
+
85
+ ### What outsiders cannot do
86
+
87
+ - Browse to other files or directories
88
+ - See the drive listing or parent directories
89
+ - Generate their own share links
90
+ - Rotate keys
91
+
92
+ ---
93
+
94
+ ## How Sharing Works
95
+
96
+ ### The key model
97
+
98
+ Every insider has a **seed** — a secret string (either configured manually or auto-generated on Google login). From this seed, the server derives two types of keys:
99
+
100
+ | Key type | Derivation | Grants |
101
+ |----------|-----------|--------|
102
+ | **Insider key** | `HMAC-SHA256(seed, "insider")` | Full browsing access (within scopes) |
103
+ | **Outsider key** | `HMAC-SHA256(seed, normalized_path)` | Access to one specific path |
104
+ | **Expiring outsider key** | `HMAC-SHA256(seed, path + "\|" + expiry)` | Access to one path, until expiry |
105
+
106
+ All keys are truncated to 32 hex characters. Verification uses timing-safe comparison.
107
+
108
+ ### Generating a share link
109
+
110
+ In the header of any file or directory view, insiders see sharing controls:
111
+
112
+ 1. **Link dropdown** (🔗) — Copy a share link
113
+ 2. **Expiry selector** — Choose how long the link is valid: never, 1 hour, 1 day, 1 week, 1 month, or 1 year
114
+
115
+ The generated URL includes the outsider key as a `?key=` parameter and (if expiring) an `&exp=` parameter with the expiration timestamp.
116
+
117
+ **Example insider link:**
118
+ ```
119
+ https://jeeves.example.com/browse/d/docs/design.md?key=a1b2c3d4...
120
+ ```
121
+
122
+ **Example outsider link (expiring):**
123
+ ```
124
+ https://jeeves.example.com/browse/d/docs/design.md?key=e5f6a7b8...&exp=1771340000000
125
+ ```
126
+
127
+ ### Directory sharing
128
+
129
+ When you share a directory link, the outsider can:
130
+ - See the directory listing
131
+ - Navigate into subdirectories
132
+ - View any file within that directory tree
133
+
134
+ The server checks the outsider key against the requested path **and all ancestor paths**, so a key generated for `/d/docs/` also grants access to `/d/docs/report.md` and `/d/docs/specs/api.md`.
135
+
136
+ ### Link expiration
137
+
138
+ - **"Never"** links work until the insider rotates their key
139
+ - **Timed** links expire at the specified time — after that, the key is cryptographically invalid
140
+ - Expiry is embedded in the key derivation itself, not stored server-side — there's nothing to clean up
141
+
142
+ ### Key rotation
143
+
144
+ The 🔑 button in the header rotates the insider's key seed. This:
145
+
146
+ - **Invalidates all outsider links** that insider has ever generated
147
+ - **Generates a new insider key** for the insider
148
+ - Is **irreversible** — old links cannot be restored
149
+
150
+ Use rotation when you need to revoke all shared links at once (e.g., a contractor's access ended, a link was shared too broadly).
151
+
152
+ ---
153
+
154
+ ## Machine Keys
155
+
156
+ In addition to insider-generated keys, the server supports **named machine keys** for programmatic access:
157
+
158
+ ```typescript
159
+ keys: {
160
+ primary: 'random-seed-string',
161
+ 'webhook-notion': { key: 'another-seed', scopes: ['/event'] },
162
+ _internal: 'internal-seed',
163
+ },
164
+ ```
165
+
166
+ Machine keys follow the same derivation model:
167
+ - The **insider key** derived from an unscoped machine seed grants full access
168
+ - Machine seeds can also generate **outsider keys** for specific paths
169
+ - **Scoped** machine keys (like `webhook-notion` above) can only access matching paths
170
+
171
+ ### The `_internal` key
172
+
173
+ Reserved for server-side operations. Puppeteer uses this key when rendering PDFs and DOCX files (it loads the page in headless Chrome and needs to authenticate). It must be unscoped — the schema enforces this.
174
+
175
+ ---
176
+
177
+ ## Access Decision Flow
178
+
179
+ When a request arrives, the server determines access as follows:
180
+
181
+ ![Access Decision Flow](access-decision-flow.svg)
182
+
183
+ ### Insider vs outsider UI
184
+
185
+ The server renders different UI based on access mode:
186
+
187
+ | Feature | Insider | Outsider |
188
+ |---------|---------|----------|
189
+ | Drive/directory browsing | ✅ | Only shared path |
190
+ | File viewing | ✅ | ✅ |
191
+ | PDF/DOCX export | ✅ | ✅ |
192
+ | Share link generation | ✅ | ❌ |
193
+ | Key rotation | ✅ | ❌ |
194
+ | Download dropdown | Full options | File download only |
195
+
196
+ ---
197
+
198
+ ## Security Notes
199
+
200
+ - **All keys are derived** — seeds are never exposed in URLs. Even if someone captures an outsider key, they can't derive the insider key or access other paths.
201
+ - **Timing-safe comparison** — key verification uses constant-time comparison to prevent timing attacks.
202
+ - **No server-side link storage** — outsider keys are computed on-the-fly from the seed + path (+ optional expiry). There's no database of active links to breach.
203
+ - **Scopes are enforced at verification time** — even if a valid key is presented, it's rejected if the path doesn't match the key's scopes.
204
+ - **HTTPS recommended** — keys are in URL parameters. Use HTTPS in production to prevent interception.
@@ -0,0 +1,25 @@
1
+ {
2
+ "$schema": "./jeeves-server.schema.json",
3
+ "port": 1934,
4
+ "chromePath": "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
5
+ "auth": {
6
+ "modes": ["google", "keys"],
7
+ "google": {
8
+ "clientId": "your-google-client-id.apps.googleusercontent.com",
9
+ "clientSecret": "${GOOGLE_CLIENT_SECRET}"
10
+ },
11
+ "sessionSecret": "${SESSION_SECRET}"
12
+ },
13
+ "insiders": {
14
+ "user@example.com": {
15
+ "scopes": ["/**"]
16
+ }
17
+ },
18
+ "keys": {
19
+ "primary": "${KEY_PRIMARY}",
20
+ "_internal": "${KEY_INTERNAL}"
21
+ },
22
+ "events": {},
23
+ "eventTimeoutMs": 30000,
24
+ "eventLogPurgeMs": 2592000000
25
+ }
package/package.json ADDED
@@ -0,0 +1,124 @@
1
+ {
2
+ "name": "@karmaniverous/jeeves-server",
3
+ "version": "3.0.0-0",
4
+ "description": "Secure file browser, markdown viewer, and webhook gateway with PDF/DOCX export and expiring share links",
5
+ "keywords": [
6
+ "fastify",
7
+ "react",
8
+ "markdown",
9
+ "file-server",
10
+ "file-browser",
11
+ "pdf-export",
12
+ "docx-export",
13
+ "mermaid",
14
+ "typescript",
15
+ "webhook",
16
+ "document-viewer",
17
+ "self-hosted",
18
+ "share-links",
19
+ "hmac",
20
+ "google-oauth"
21
+ ],
22
+ "type": "module",
23
+ "scripts": {
24
+ "start": "node dist/src/server.js",
25
+ "dev": "tsx watch src/server.ts",
26
+ "prebuild": "npx rimraf dist .tsbuildinfo",
27
+ "build": "tsc",
28
+ "postbuild": "cd client && npm run build",
29
+ "typecheck": "cross-env NODE_OPTIONS=--max-old-space-size=4096 tsc --noEmit",
30
+ "test": "vitest run",
31
+ "changelog": "auto-changelog",
32
+ "release": "dotenvx run -f .env.local -- release-it",
33
+ "postinstall": "node scripts/download-plantuml.js",
34
+ "lint": "eslint .",
35
+ "lint:fix": "eslint --fix .",
36
+ "knip": "knip",
37
+ "release:pre": "dotenvx run -f .env.local -- release-it --no-git.requireBranch --github.prerelease --preRelease"
38
+ },
39
+ "publishConfig": {
40
+ "access": "public"
41
+ },
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "git+https://github.com/karmaniverous/jeeves-server.git"
45
+ },
46
+ "author": "Jeeves",
47
+ "license": "MIT",
48
+ "dependencies": {
49
+ "@commander-js/extra-typings": "^14.0.0",
50
+ "@fastify/cookie": "^11.0.2",
51
+ "@fastify/static": "^8.3.0",
52
+ "@karmaniverous/jsonmap": "^0.3.1",
53
+ "@mermaid-js/mermaid-cli": "^11.12.0",
54
+ "@turbodocx/html-to-docx": "^1.1.0",
55
+ "ajv": "^8.17.1",
56
+ "archiver": "^7.0.1",
57
+ "cheerio": "^1.2.0",
58
+ "cosmiconfig": "^9.0.1",
59
+ "fastify": "^5.2.3",
60
+ "lz-string": "^1.5.0",
61
+ "marked": "^17.0.1",
62
+ "mime-types": "^3.0.2",
63
+ "picomatch": "^4.0.3",
64
+ "plantuml-encoder": "^1.4.0",
65
+ "puppeteer": "^23.11.1",
66
+ "puppeteer-core": "^23.11.1",
67
+ "radash": "^12.1.1",
68
+ "zod": "^4.3.6"
69
+ },
70
+ "devDependencies": {
71
+ "@dotenvx/dotenvx": "^1.54.1",
72
+ "@types/archiver": "^7.0.0",
73
+ "@types/mime-types": "^3.0.1",
74
+ "@types/node": "^22.0.0",
75
+ "@types/picomatch": "^4.0.2",
76
+ "@vitest/coverage-v8": "^4.0.16",
77
+ "auto-changelog": "^2.5.0",
78
+ "cross-env": "^10.1.0",
79
+ "happy-dom": "^20.0.11",
80
+ "release-it": "^19.2.4",
81
+ "rimraf": "^6.0.1",
82
+ "vitest": "^4.0.16"
83
+ },
84
+ "auto-changelog": {
85
+ "output": "CHANGELOG.md",
86
+ "unreleased": true,
87
+ "commitLimit": false,
88
+ "hideCredit": true
89
+ },
90
+ "release-it": {
91
+ "git": {
92
+ "changelog": "npx auto-changelog --unreleased-only --stdout --template https://raw.githubusercontent.com/release-it/release-it/main/templates/changelog-compact.hbs",
93
+ "commitMessage": "chore: release @karmaniverous/jeeves-server v${version}",
94
+ "tagName": "service/${version}",
95
+ "requireBranch": "main"
96
+ },
97
+ "github": {
98
+ "release": true
99
+ },
100
+ "hooks": {
101
+ "after:init": [
102
+ "npm run lint",
103
+ "npm run typecheck",
104
+ "npm run test",
105
+ "npm run build"
106
+ ],
107
+ "before:npm:release": [
108
+ "npx auto-changelog -p",
109
+ "git add -A"
110
+ ],
111
+ "after:release": [
112
+ "git switch -c release/service/${version}",
113
+ "git push -u origin release/service/${version}",
114
+ "git switch ${branchName}"
115
+ ]
116
+ },
117
+ "npm": {
118
+ "publish": true
119
+ }
120
+ },
121
+ "bin": {
122
+ "jeeves-server": "./dist/src/cli/index.js"
123
+ }
124
+ }
@@ -0,0 +1,70 @@
1
+ /* eslint-disable no-undef */
2
+ /* eslint-env node */
3
+
4
+ /**
5
+ * Downloads a pinned PlantUML jar to vendor/plantuml.jar.
6
+ * Run automatically via postinstall, or manually: node scripts/download-plantuml.js
7
+ *
8
+ * The jar is gitignored and server-side only (never touches the client build).
9
+ * To upgrade PlantUML: bump PLANTUML_VERSION below and run npm install.
10
+ */
11
+
12
+ import fs from 'node:fs';
13
+ import https from 'node:https';
14
+ import path from 'node:path';
15
+ import { fileURLToPath } from 'node:url';
16
+
17
+ const PLANTUML_VERSION = 'v1.2026.2';
18
+ const PLANTUML_URL = `https://github.com/plantuml/plantuml/releases/download/${PLANTUML_VERSION}/plantuml-${PLANTUML_VERSION.slice(1)}.jar`;
19
+
20
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
21
+ const vendorDir = path.resolve(__dirname, '..', 'vendor');
22
+ const jarPath = path.join(vendorDir, 'plantuml.jar');
23
+ const versionFile = path.join(vendorDir, '.plantuml-version');
24
+
25
+ // Skip if already downloaded at this version
26
+ if (fs.existsSync(jarPath) && fs.existsSync(versionFile)) {
27
+ const currentVersion = fs.readFileSync(versionFile, 'utf8').trim();
28
+ if (currentVersion === PLANTUML_VERSION) {
29
+ console.log(`PlantUML ${PLANTUML_VERSION} already present, skipping download.`);
30
+ process.exit(0);
31
+ }
32
+ }
33
+
34
+ fs.mkdirSync(vendorDir, { recursive: true });
35
+
36
+ console.log(`Downloading PlantUML ${PLANTUML_VERSION}...`);
37
+
38
+ function download(url, dest) {
39
+ return new Promise((resolve, reject) => {
40
+ const follow = (url) => {
41
+ https.get(url, (res) => {
42
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
43
+ follow(res.headers.location);
44
+ return;
45
+ }
46
+ if (res.statusCode !== 200) {
47
+ reject(new Error(`HTTP ${String(res.statusCode)} downloading PlantUML`));
48
+ return;
49
+ }
50
+ const file = fs.createWriteStream(dest);
51
+ res.pipe(file);
52
+ file.on('finish', () => { file.close(); resolve(); });
53
+ file.on('error', reject);
54
+ }).on('error', reject);
55
+ };
56
+ follow(url);
57
+ });
58
+ }
59
+
60
+ try {
61
+ await download(PLANTUML_URL, jarPath);
62
+ fs.writeFileSync(versionFile, PLANTUML_VERSION);
63
+ const stats = fs.statSync(jarPath);
64
+ console.log(`PlantUML ${PLANTUML_VERSION} downloaded (${(stats.size / 1024 / 1024).toFixed(1)} MB)`);
65
+ } catch (err) {
66
+ console.warn(`Warning: Failed to download PlantUML: ${err.message}`);
67
+ console.warn('PlantUML local rendering will fall back to server-based rendering.');
68
+ // Don't fail the install — PlantUML is optional
69
+ process.exit(0);
70
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Google OAuth 2.0 helpers.
3
+ * Uses native fetch — no googleapis SDK needed.
4
+ */
5
+
6
+ const GOOGLE_AUTH_URL = 'https://accounts.google.com/o/oauth2/v2/auth';
7
+ const GOOGLE_TOKEN_URL = 'https://oauth2.googleapis.com/token';
8
+ const GOOGLE_USERINFO_URL = 'https://openidconnect.googleapis.com/v1/userinfo';
9
+
10
+ export interface GoogleTokens {
11
+ access_token: string;
12
+ id_token?: string;
13
+ refresh_token?: string;
14
+ expires_in: number;
15
+ token_type: string;
16
+ }
17
+
18
+ export interface GoogleUserInfo {
19
+ sub: string;
20
+ email: string;
21
+ email_verified: boolean;
22
+ name?: string;
23
+ picture?: string;
24
+ }
25
+
26
+ /**
27
+ * Build the Google OAuth consent URL.
28
+ */
29
+ export function buildAuthUrl(
30
+ clientId: string,
31
+ redirectUri: string,
32
+ state?: string,
33
+ ): string {
34
+ const params = new URLSearchParams({
35
+ client_id: clientId,
36
+ redirect_uri: redirectUri,
37
+ response_type: 'code',
38
+ scope: 'openid email profile',
39
+ access_type: 'offline',
40
+ prompt: 'consent',
41
+ });
42
+ if (state) params.set('state', state);
43
+ return `${GOOGLE_AUTH_URL}?${params.toString()}`;
44
+ }
45
+
46
+ /**
47
+ * Exchange authorization code for tokens.
48
+ */
49
+ export async function exchangeCode(
50
+ clientId: string,
51
+ clientSecret: string,
52
+ redirectUri: string,
53
+ code: string,
54
+ ): Promise<GoogleTokens> {
55
+ const resp = await fetch(GOOGLE_TOKEN_URL, {
56
+ method: 'POST',
57
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
58
+ body: new URLSearchParams({
59
+ client_id: clientId,
60
+ client_secret: clientSecret,
61
+ redirect_uri: redirectUri,
62
+ code,
63
+ grant_type: 'authorization_code',
64
+ }),
65
+ });
66
+
67
+ if (!resp.ok) {
68
+ const text = await resp.text();
69
+ throw new Error(
70
+ `Google token exchange failed: ${String(resp.status)} ${text}`,
71
+ );
72
+ }
73
+
74
+ return (await resp.json()) as GoogleTokens;
75
+ }
76
+
77
+ /**
78
+ * Fetch user info using an access token.
79
+ */
80
+ export async function getUserInfo(
81
+ accessToken: string,
82
+ ): Promise<GoogleUserInfo> {
83
+ const resp = await fetch(GOOGLE_USERINFO_URL, {
84
+ headers: { Authorization: `Bearer ${accessToken}` },
85
+ });
86
+
87
+ if (!resp.ok) {
88
+ const text = await resp.text();
89
+ throw new Error(`Google userinfo failed: ${String(resp.status)} ${text}`);
90
+ }
91
+
92
+ return (await resp.json()) as GoogleUserInfo;
93
+ }