@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,287 @@
1
+ # Deployment
2
+
3
+ How to run Jeeves Server in production.
4
+
5
+ > Jeeves Server runs on **Windows** and **Linux**. Both platforms are tested in CI.
6
+
7
+ ## Prerequisites
8
+
9
+ - **Node.js** ≥ 18
10
+ - **Chrome or Chromium** — for PDF/DOCX export via Puppeteer
11
+ - **A domain** with HTTPS — required for Google OAuth and secure sharing
12
+ - **A reverse proxy** — nginx, Caddy, or similar (recommended)
13
+
14
+ ### Optional Dependencies
15
+
16
+ - **Java** (JDK 11+) — required for local PlantUML rendering with `!include` support. Without Java, PlantUML falls back to the community server (no `!include` support).
17
+ - **PlantUML jar** — download from [plantuml.com/download](https://plantuml.com/download). Configure the path in `jeeves.config.ts` under `plantuml.jarPath`.
18
+ - **Mermaid CLI** — for server-side Mermaid diagram rendering. Install with `npm install @mermaid-js/mermaid-cli` and configure `mermaidCliPath` in `jeeves.config.ts`.
19
+
20
+ ## Running the Server
21
+
22
+ ### Direct
23
+
24
+ ```bash
25
+ node dist/server.js
26
+ ```
27
+
28
+ The server listens on the port configured in `jeeves.config.ts` (default: 1934) on all interfaces (`0.0.0.0`).
29
+
30
+ ### As a Windows Service (NSSM)
31
+
32
+ [NSSM](https://nssm.cc/) (Non-Sucking Service Manager) turns any executable into a Windows service:
33
+
34
+ ```bash
35
+ # Install the service
36
+ nssm install JeevesServer "C:\Program Files\nodejs\node.exe" "E:\jeeves-server\dist\server.js"
37
+
38
+ # Configure working directory
39
+ nssm set JeevesServer AppDirectory "E:\jeeves-server"
40
+
41
+ # Configure stdout/stderr logging
42
+ nssm set JeevesServer AppStdout "E:\jeeves-server\logs\service-stdout.log"
43
+ nssm set JeevesServer AppStderr "E:\jeeves-server\logs\service-stderr.log"
44
+
45
+ # Start the service
46
+ nssm start JeevesServer
47
+ ```
48
+
49
+ **Service management:**
50
+ ```bash
51
+ nssm start JeevesServer
52
+ nssm stop JeevesServer
53
+ nssm restart JeevesServer
54
+ nssm status JeevesServer
55
+ nssm remove JeevesServer confirm # Uninstall
56
+ ```
57
+
58
+ ### As a systemd Service (Linux)
59
+
60
+ ```ini
61
+ # /etc/systemd/system/jeeves-server.service
62
+ [Unit]
63
+ Description=Jeeves Server
64
+ After=network.target
65
+
66
+ [Service]
67
+ Type=simple
68
+ User=jeeves
69
+ WorkingDirectory=/opt/jeeves-server
70
+ ExecStart=/usr/bin/node /opt/jeeves-server/dist/server.js
71
+ Restart=on-failure
72
+ RestartSec=5
73
+ Environment=NODE_ENV=production
74
+
75
+ [Install]
76
+ WantedBy=multi-user.target
77
+ ```
78
+
79
+ ```bash
80
+ sudo systemctl enable jeeves-server
81
+ sudo systemctl start jeeves-server
82
+ sudo systemctl status jeeves-server
83
+ ```
84
+
85
+ ### Linux Quick Start (Ubuntu/Debian)
86
+
87
+ ```bash
88
+ # System packages
89
+ sudo apt-get update && sudo apt-get install -y curl git build-essential chromium-browser caddy
90
+
91
+ # Node.js 22
92
+ curl -fsSL https://deb.nodesource.com/setup_22.x | sudo bash -
93
+ sudo apt-get install -y nodejs
94
+
95
+ # Clone and build
96
+ cd /opt
97
+ sudo git clone https://github.com/karmaniverous/jeeves-server.git
98
+ cd jeeves-server
99
+ npm ci
100
+ cd client && npm ci && npx vite build --outDir ../dist/client && cd ..
101
+ npx tsc
102
+
103
+ # Configure
104
+ cp jeeves.config.template.ts jeeves.config.ts
105
+ # Edit jeeves.config.ts — set chromePath, roots, auth, keys, etc.
106
+ echo '{}' > state.json
107
+ ```
108
+
109
+ **Linux-specific config options:**
110
+
111
+ ```typescript
112
+ {
113
+ // Chromium path (required for PDF/DOCX export)
114
+ chromePath: '/usr/bin/chromium-browser',
115
+
116
+ // Filesystem roots for the file browser (replaces Windows drive letters)
117
+ roots: {
118
+ home: '/home',
119
+ projects: '/opt/projects',
120
+ },
121
+
122
+ // Mermaid CLI path (optional, for .mmd diagram rendering)
123
+ mermaidCliPath: '/opt/mermaid-cli',
124
+ }
125
+ ```
126
+
127
+ On Windows, `roots` is ignored — the file browser auto-discovers drive letters. On Linux, if `roots` is omitted, it defaults to `{ root: '/' }`.
128
+
129
+ **Puppeteer config** (for Chromium on Linux):
130
+
131
+ Create `puppeteer.json` in the server root:
132
+ ```json
133
+ {
134
+ "executablePath": "/usr/bin/chromium-browser",
135
+ "args": ["--no-sandbox", "--disable-setuid-sandbox"]
136
+ }
137
+ ```
138
+
139
+ ## Reverse Proxy
140
+
141
+ Running behind a reverse proxy is recommended for:
142
+ - **HTTPS termination** — required for Google OAuth and secure key transmission
143
+ - **Domain routing** — serve on a clean domain/subdomain
144
+ - **Rate limiting** and request filtering
145
+
146
+ ### Caddy (simplest)
147
+
148
+ ```
149
+ jeeves.example.com {
150
+ reverse_proxy localhost:1934
151
+ }
152
+ ```
153
+
154
+ Caddy automatically provisions and renews HTTPS certificates via Let's Encrypt.
155
+
156
+ ### nginx
157
+
158
+ ```nginx
159
+ server {
160
+ listen 443 ssl http2;
161
+ server_name jeeves.example.com;
162
+
163
+ ssl_certificate /etc/letsencrypt/live/jeeves.example.com/fullchain.pem;
164
+ ssl_certificate_key /etc/letsencrypt/live/jeeves.example.com/privkey.pem;
165
+
166
+ location / {
167
+ proxy_pass http://localhost:1934;
168
+ proxy_set_header Host $host;
169
+ proxy_set_header X-Real-IP $remote_addr;
170
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
171
+ proxy_set_header X-Forwarded-Proto $scheme;
172
+
173
+ # Large file uploads (for webhook bodies)
174
+ client_max_body_size 10M;
175
+
176
+ # WebSocket support (if needed in future)
177
+ proxy_http_version 1.1;
178
+ proxy_set_header Upgrade $http_upgrade;
179
+ proxy_set_header Connection "upgrade";
180
+ }
181
+ }
182
+
183
+ server {
184
+ listen 80;
185
+ server_name jeeves.example.com;
186
+ return 301 https://$host$request_uri;
187
+ }
188
+ ```
189
+
190
+ ## HTTPS
191
+
192
+ **HTTPS is required** when using Google OAuth — Google will not redirect to an HTTP callback URL (except `localhost` for development).
193
+
194
+ **HTTPS is strongly recommended** even with key-only auth, because keys appear in URL parameters. Without HTTPS, keys are visible to network observers.
195
+
196
+ ### Options
197
+
198
+ | Method | Effort | Best For |
199
+ |--------|--------|----------|
200
+ | **Caddy** | Minimal | Automatic HTTPS, zero config |
201
+ | **Let's Encrypt + nginx** | Moderate | Fine-grained control |
202
+ | **Cloudflare Tunnel** | Moderate | No port forwarding needed |
203
+
204
+ ## Google OAuth Setup for Production
205
+
206
+ When using Google OAuth in production:
207
+
208
+ 1. **Add your domain** to the Google Cloud Console OAuth consent screen
209
+ 2. **Set the redirect URI** to `https://your-domain.com/auth/google/callback`
210
+ 3. **Update `jeeves.config.ts`** with the production credentials
211
+
212
+ > **Dev vs Prod:** You can use different Google OAuth client IDs for development and production. Each `jeeves.config.ts` is gitignored and instance-specific.
213
+
214
+ ### Multiple environments
215
+
216
+ Run dev and prod on the same machine using different ports:
217
+
218
+ ```typescript
219
+ // Dev: jeeves.config.ts (port 3457)
220
+ port: 3457,
221
+ auth: {
222
+ google: {
223
+ clientId: 'dev-client-id.apps.googleusercontent.com',
224
+ clientSecret: 'dev-secret',
225
+ },
226
+ },
227
+
228
+ // Prod: jeeves.config.ts (port 1934)
229
+ port: 1934,
230
+ auth: {
231
+ google: {
232
+ clientId: 'prod-client-id.apps.googleusercontent.com',
233
+ clientSecret: 'prod-secret',
234
+ },
235
+ },
236
+ ```
237
+
238
+ Each needs its own Google OAuth redirect URI registered.
239
+
240
+ ## Health Checks
241
+
242
+ The `/health` endpoint requires no authentication:
243
+
244
+ ```bash
245
+ curl http://localhost:1934/health
246
+ # Returns 200 OK
247
+ ```
248
+
249
+ Use this for:
250
+ - Load balancer health checks
251
+ - Service monitoring (Uptime Kuma, Prometheus, etc.)
252
+ - NSSM/systemd restart triggers
253
+
254
+ ## Updating
255
+
256
+ ```bash
257
+ cd /path/to/jeeves-server
258
+ git pull
259
+
260
+ # Full rebuild
261
+ npm install
262
+ npm run build
263
+ cd client && npx vite build --outDir ../dist/client && cd ..
264
+
265
+ # Restart the service
266
+ nssm restart JeevesServer # Windows
267
+ sudo systemctl restart jeeves-server # Linux
268
+ ```
269
+
270
+ > ⚠️ Remember: `npm run build` deletes `dist/` entirely. Always rebuild the client after the server.
271
+
272
+ ## File Permissions
273
+
274
+ The server needs:
275
+ - **Read access** to any files you want to serve (drives, directories)
276
+ - **Write access** to its own directory for `state.json` and `logs/`
277
+ - **Execute access** to Chrome/Chromium for PDF export
278
+ - **Execute access** to event handler commands
279
+
280
+ ## Backups
281
+
282
+ Key files to back up:
283
+ - `jeeves.config.ts` — your configuration (secrets!)
284
+ - `state.json` — insider keys and rotation state
285
+ - `logs/event-queue.jsonl` + `logs/event-queue.cursor` — pending events
286
+
287
+ The server code itself is in git — no need to back up `dist/` or `node_modules/`.
@@ -0,0 +1,204 @@
1
+ # Event Gateway
2
+
3
+ Jeeves Server includes a webhook gateway that receives HTTP POST requests, validates them against JSON Schema rules, and dispatches matched events to shell commands via a durable queue.
4
+
5
+ ## Overview
6
+
7
+ ![Event Gateway Flow](event-gateway.svg)
8
+
9
+ ## Configuration
10
+
11
+ Events are defined in `jeeves.config.ts`:
12
+
13
+ ```typescript
14
+ events: {
15
+ 'notion-page-update': {
16
+ // JSON Schema matched against the incoming POST body
17
+ schema: {
18
+ type: 'object',
19
+ properties: {
20
+ type: { const: 'page.content_updated' },
21
+ },
22
+ required: ['type'],
23
+ },
24
+
25
+ // Shell command to execute when matched
26
+ cmd: 'node /path/to/handler.js',
27
+
28
+ // Optional: transform the body before passing to the command
29
+ map: {
30
+ pageId: {
31
+ '$': { method: '$.lib._.get', params: ['$.input', 'data.page_id'] },
32
+ },
33
+ type: {
34
+ '$': { method: '$.lib._.get', params: ['$.input', 'type'] },
35
+ },
36
+ },
37
+
38
+ // Optional: override default timeout (ms)
39
+ timeoutMs: 60000,
40
+ },
41
+ },
42
+ ```
43
+
44
+ ### Schema matching
45
+
46
+ Each event has a [JSON Schema](https://json-schema.org/) that's validated against the incoming request body using [ajv](https://ajv.js.org/). The **first matching** event wins — order matters if schemas could overlap.
47
+
48
+ Common patterns:
49
+
50
+ ```json
51
+ // Match a specific event type
52
+ { "type": "object", "properties": { "type": { "const": "page.content_updated" } }, "required": ["type"] }
53
+
54
+ // Match any object with an "action" field
55
+ { "type": "object", "required": ["action"] }
56
+
57
+ // Match based on nested field
58
+ { "type": "object", "properties": { "data": { "type": "object", "properties": { "status": { "const": "completed" } } } } }
59
+ ```
60
+
61
+ ### Body mapping with JsonMap
62
+
63
+ When an event config includes a `map` object, the incoming body is transformed via [@karmaniverous/jsonmap](https://github.com/karmaniverous/jsonmap) before being passed to the command. This extracts only the fields you need from potentially large webhook payloads.
64
+
65
+ The `lib` object available in mappings includes [`radash`](https://github.com/sodiray/radash) as `_`.
66
+
67
+ When `map` is omitted, the full webhook body is passed as-is.
68
+
69
+ **Example — Notion sends a large payload, we extract just two fields:**
70
+
71
+ ```typescript
72
+ map: {
73
+ pageId: {
74
+ '$': { method: '$.lib._.get', params: ['$.input', 'data.page_id'] },
75
+ },
76
+ type: {
77
+ '$': { method: '$.lib._.get', params: ['$.input', 'type'] },
78
+ },
79
+ },
80
+ ```
81
+
82
+ Input: `{ type: "page.content_updated", data: { page_id: "abc123", ... } }`
83
+ Output to command: `{ pageId: "abc123", type: "page.content_updated" }`
84
+
85
+ ## Authentication
86
+
87
+ Webhook callers must authenticate with a key that has scope access to `/event`:
88
+
89
+ ```typescript
90
+ keys: {
91
+ 'webhook-notion': {
92
+ key: 'random-seed-string',
93
+ scopes: ['/event'],
94
+ },
95
+ },
96
+ ```
97
+
98
+ Your config contains a **seed** — a secret string that never leaves the server. The actual URL key is **derived** from the seed by the server. To get it:
99
+
100
+ ```bash
101
+ curl -s "https://your-domain.com/insider-key" -H "X-API-Key: <your-seed>"
102
+ # Returns: { "key": "a1b2c3d4..." }
103
+ ```
104
+
105
+ Use the returned key in webhook URLs:
106
+
107
+ ```bash
108
+ curl -X POST "https://your-domain.com/event?key=<derived-key>" \
109
+ -H "Content-Type: application/json" \
110
+ -d '{"type": "page.content_updated", "data": {"page_id": "abc123"}}'
111
+ ```
112
+
113
+ Insiders with `/event` scope can also copy an authenticated event URL directly from the **Event link button** in the header bar — no command line needed.
114
+
115
+ See the [Insiders, Outsiders & Sharing](sharing.md) guide for full details on the key model.
116
+
117
+ ## Queue Processing
118
+
119
+ Events are processed through a **durable JSONL queue**:
120
+
121
+ 1. **Append** — Validated events are appended to `logs/event-queue.jsonl` with metadata
122
+ 2. **Drain** — A single-threaded processor reads entries sequentially
123
+ 3. **Execute** — For each entry, the `cmd` is spawned with the (optionally mapped) body piped as JSON to stdin
124
+ 4. **Timeout** — Commands are killed after `timeoutMs` (per-event or the global `eventTimeoutMs` default)
125
+ 5. **Errors logged** — The command is responsible for its own error handling; the queue processor logs and moves on
126
+ 6. **Cursor** — A cursor file (`logs/event-queue.cursor`) tracks the byte offset of the last processed entry, surviving restarts
127
+
128
+ ### Queue entry format
129
+
130
+ ```jsonl
131
+ {"ts":"2026-02-15T05:00:00Z","event":"notion-page-update","cmd":"node handler.js","body":{"pageId":"abc123"},"timeoutMs":60000}
132
+ ```
133
+
134
+ ### Durability
135
+
136
+ The queue survives server restarts. On startup, the processor reads the cursor file and resumes from where it left off. If the cursor file is missing, processing starts from the beginning of the queue.
137
+
138
+ ## Event Logging
139
+
140
+ All events — matched and unmatched — are logged to `logs/event-log.jsonl`:
141
+
142
+ ```jsonl
143
+ {"ts":"2026-02-15T05:00:00Z","event":"notion-page-update","matched":true,"exitCode":0,"durationMs":1234}
144
+ {"ts":"2026-02-15T05:00:01Z","event":null,"matched":false,"bodyPreview":"..."}
145
+ ```
146
+
147
+ ### Log purging
148
+
149
+ Each log write also purges entries older than `eventLogPurgeMs` (default: 30 days). This keeps the log file from growing unbounded.
150
+
151
+ ## Writing Event Handlers
152
+
153
+ Your command receives the (optionally mapped) body as JSON on **stdin**:
154
+
155
+ ```javascript
156
+ // handler.js
157
+ const chunks = [];
158
+ process.stdin.on('data', (chunk) => chunks.push(chunk));
159
+ process.stdin.on('end', () => {
160
+ const body = JSON.parse(Buffer.concat(chunks).toString());
161
+ console.log('Received:', body.pageId);
162
+ // Do your work here
163
+ });
164
+ ```
165
+
166
+ **Key points:**
167
+ - The command runs in the server's working directory
168
+ - stdout/stderr are captured for logging
169
+ - Exit code 0 = success, anything else = failure (logged but not retried)
170
+ - The command must complete within `timeoutMs` or it's killed
171
+
172
+ ## Global Settings
173
+
174
+ ```typescript
175
+ // Default timeout for all event commands (ms)
176
+ eventTimeoutMs: 30_000,
177
+
178
+ // Purge log entries older than this (ms). Default: 30 days
179
+ eventLogPurgeMs: 2_592_000_000,
180
+ ```
181
+
182
+ ## Monitoring
183
+
184
+ Check the event log for failures:
185
+
186
+ ```bash
187
+ # Recent failures
188
+ grep '"exitCode":' logs/event-log.jsonl | grep -v '"exitCode":0'
189
+
190
+ # Unmatched events (potential misconfiguration)
191
+ grep '"matched":false' logs/event-log.jsonl
192
+ ```
193
+
194
+ ## Example: Notion Webhook Integration
195
+
196
+ 1. **Configure the event** in `jeeves.config.ts` (see Configuration above)
197
+ 2. **Create a scoped key** for the webhook
198
+ 3. **Register the webhook URL** in Notion:
199
+ - Settings → Connections → Add a connection
200
+ - Webhook URL: `https://your-domain.com/event?key=<webhook-derived-key>`
201
+ 4. **Write your handler** to process the mapped body
202
+ 5. **Verify** by triggering a page update and checking `logs/event-log.jsonl`
203
+
204
+ > **Note:** Notion signs webhooks with HMAC. For production use, add signature verification in your handler before processing.
@@ -0,0 +1,17 @@
1
+ flowchart TD
2
+ A["POST /event?key=‹scoped-key›"] --> B["Authenticate key\n(check scope includes /event)"]
3
+ B --> C{"For each event\nin config.events"}
4
+ C --> D["Validate body against\nevent.schema (ajv)"]
5
+ D --> E{"Match?"}
6
+ E -- "First match wins" --> F{"event.map\ndefined?"}
7
+ F -- Yes --> G["Transform body\nvia JsonMap"]
8
+ F -- No --> H["Use full body"]
9
+ G --> I["Append to durable\nJSONL queue"]
10
+ H --> I
11
+ I --> J["Return 200\n{ matched: ‹event-name› }"]
12
+ E -- "No match" --> K["Log as unmatched"]
13
+ K --> L["Return 200\n{ matched: null }"]
14
+
15
+ style A fill:#4a9eff,color:#fff,stroke:#2d7dd2
16
+ style J fill:#22c55e,color:#fff,stroke:#16a34a
17
+ style L 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: 789.625px; background-color: white;" viewBox="0 0 789.625 1542.296875" 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="M394.813,86L394.813,90.167C394.813,94.333,394.813,102.667,394.813,110.333C394.813,118,394.813,125,394.813,128.5L394.813,132" 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="W3sieCI6Mzk0LjgxMjUsInkiOjg2fSx7IngiOjM5NC44MTI1LCJ5IjoxMTF9LHsieCI6Mzk0LjgxMjUsInkiOjEzNn1d" marker-end="url(#my-svg_flowchart-v2-pointEnd)"/><path d="M394.813,214L394.813,218.167C394.813,222.333,394.813,230.667,394.813,238.333C394.813,246,394.813,253,394.813,256.5L394.813,260" 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="W3sieCI6Mzk0LjgxMjUsInkiOjIxNH0seyJ4IjozOTQuODEyNSwieSI6MjM5fSx7IngiOjM5NC44MTI1LCJ5IjoyNjR9XQ==" marker-end="url(#my-svg_flowchart-v2-pointEnd)"/><path d="M394.813,542L394.813,546.167C394.813,550.333,394.813,558.667,394.813,566.333C394.813,574,394.813,581,394.813,584.5L394.813,588" 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="W3sieCI6Mzk0LjgxMjUsInkiOjU0Mn0seyJ4IjozOTQuODEyNSwieSI6NTY3fSx7IngiOjM5NC44MTI1LCJ5Ijo1OTJ9XQ==" marker-end="url(#my-svg_flowchart-v2-pointEnd)"/><path d="M394.813,694L394.813,698.167C394.813,702.333,394.813,710.667,394.813,718.333C394.813,726,394.813,733,394.813,736.5L394.813,740" id="L_D_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_D_E_0" data-points="W3sieCI6Mzk0LjgxMjUsInkiOjY5NH0seyJ4IjozOTQuODEyNSwieSI6NzE5fSx7IngiOjM5NC44MTI1LCJ5Ijo3NDR9XQ==" marker-end="url(#my-svg_flowchart-v2-pointEnd)"/><path d="M364.406,816.234L348.073,827.469C331.74,838.703,299.073,861.172,282.74,877.906C266.406,894.641,266.406,905.641,266.406,911.141L266.406,916.641" id="L_E_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_E_F_0" data-points="W3sieCI6MzY0LjQwNjIxODQ1ODIzODcsInkiOjgxNi4yMzQzNDM0NTgyMzg4fSx7IngiOjI2Ni40MDYyNSwieSI6ODgzLjY0MDYyNX0seyJ4IjoyNjYuNDA2MjUsInkiOjkyMC42NDA2MjV9XQ==" marker-end="url(#my-svg_flowchart-v2-pointEnd)"/><path d="M217.181,1077.072L203.985,1091.443C190.788,1105.814,164.394,1134.555,151.197,1154.426C138,1174.297,138,1185.297,138,1190.797L138,1196.297" 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="W3sieCI6MjE3LjE4MTQ4NTU1MzY3ODU2LCJ5IjoxMDc3LjA3MjExMDU1MzY3ODZ9LHsieCI6MTM4LCJ5IjoxMTYzLjI5Njg3NX0seyJ4IjoxMzgsInkiOjEyMDAuMjk2ODc1fV0=" marker-end="url(#my-svg_flowchart-v2-pointEnd)"/><path d="M315.631,1077.072L328.828,1091.443C342.025,1105.814,368.419,1134.555,381.616,1156.426C394.813,1178.297,394.813,1193.297,394.813,1200.797L394.813,1208.297" id="L_F_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_F_H_0" data-points="W3sieCI6MzE1LjYzMTAxNDQ0NjMyMTQ0LCJ5IjoxMDc3LjA3MjExMDU1MzY3ODZ9LHsieCI6Mzk0LjgxMjUsInkiOjExNjMuMjk2ODc1fSx7IngiOjM5NC44MTI1LCJ5IjoxMjEyLjI5Njg3NX1d" marker-end="url(#my-svg_flowchart-v2-pointEnd)"/><path d="M138,1278.297L138,1282.464C138,1286.63,138,1294.964,145.763,1302.999C153.526,1311.035,169.052,1318.774,176.816,1322.643L184.579,1326.513" 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="W3sieCI6MTM4LCJ5IjoxMjc4LjI5Njg3NX0seyJ4IjoxMzgsInkiOjEzMDMuMjk2ODc1fSx7IngiOjE4OC4xNTg2OTE0MDYyNSwieSI6MTMyOC4yOTY4NzV9XQ==" marker-end="url(#my-svg_flowchart-v2-pointEnd)"/><path d="M394.813,1266.297L394.813,1272.464C394.813,1278.63,394.813,1290.964,387.049,1300.999C379.286,1311.035,363.76,1318.774,355.997,1322.643L348.234,1326.513" id="L_H_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_H_I_0" data-points="W3sieCI6Mzk0LjgxMjUsInkiOjEyNjYuMjk2ODc1fSx7IngiOjM5NC44MTI1LCJ5IjoxMzAzLjI5Njg3NX0seyJ4IjozNDQuNjUzODA4NTkzNzUsInkiOjEzMjguMjk2ODc1fV0=" marker-end="url(#my-svg_flowchart-v2-pointEnd)"/><path d="M266.406,1406.297L266.406,1410.464C266.406,1414.63,266.406,1422.964,266.406,1430.63C266.406,1438.297,266.406,1445.297,266.406,1448.797L266.406,1452.297" 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="W3sieCI6MjY2LjQwNjI1LCJ5IjoxNDA2LjI5Njg3NX0seyJ4IjoyNjYuNDA2MjUsInkiOjE0MzEuMjk2ODc1fSx7IngiOjI2Ni40MDYyNSwieSI6MTQ1Ni4yOTY4NzV9XQ==" marker-end="url(#my-svg_flowchart-v2-pointEnd)"/><path d="M433,808.453L469.437,820.985C505.875,833.516,578.75,858.578,615.187,889.247C651.625,919.917,651.625,956.193,651.625,974.331L651.625,992.469" id="L_E_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_E_K_0" data-points="W3sieCI6NDMyLjk5OTgyMTczOTgxOTM3LCJ5Ijo4MDguNDUzMzAzMjYwMTgwNn0seyJ4Ijo2NTEuNjI1LCJ5Ijo4ODMuNjQwNjI1fSx7IngiOjY1MS42MjUsInkiOjk5Ni40Njg3NX1d" marker-end="url(#my-svg_flowchart-v2-pointEnd)"/><path d="M651.625,1050.469L651.625,1069.273C651.625,1088.078,651.625,1125.688,651.625,1149.992C651.625,1174.297,651.625,1185.297,651.625,1190.797L651.625,1196.297" id="L_K_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_K_L_0" data-points="W3sieCI6NjUxLjYyNSwieSI6MTA1MC40Njg3NX0seyJ4Ijo2NTEuNjI1LCJ5IjoxMTYzLjI5Njg3NX0seyJ4Ijo2NTEuNjI1LCJ5IjoxMjAwLjI5Njg3NX1d" 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"><g class="label" data-id="L_B_C_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"><g class="label" data-id="L_C_D_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"><g class="label" data-id="L_D_E_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(266.40625, 883.640625)"><g class="label" data-id="L_E_F_0" transform="translate(-59.015625, -12)"><foreignObject width="118.03125" 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>First match wins</p></span></div></foreignObject></g></g><g class="edgeLabel" transform="translate(138, 1163.296875)"><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(394.8125, 1163.296875)"><g class="label" data-id="L_F_H_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"><g class="label" data-id="L_G_I_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"><g class="label" data-id="L_H_I_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"><g class="label" data-id="L_I_J_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(651.625, 883.640625)"><g class="label" data-id="L_E_K_0" transform="translate(-34.15625, -12)"><foreignObject width="68.3125" 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 match</p></span></div></foreignObject></g></g><g class="edgeLabel"><g class="label" data-id="L_K_L_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><g class="nodes"><g class="node default" id="flowchart-A-0" transform="translate(394.8125, 47)"><rect class="basic label-container" style="fill:#4a9eff !important;stroke:#2d7dd2 !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>POST /event?key=‹scoped-key›</p></span></div></foreignObject></g></g><g class="node default" id="flowchart-B-1" transform="translate(394.8125, 175)"><rect class="basic label-container" style="" x="-130" y="-39" width="260" height="78"/><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>Authenticate key\n(check scope includes /event)</p></span></div></foreignObject></g></g><g class="node default" id="flowchart-C-3" transform="translate(394.8125, 403)"><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>For each event\nin config.events</p></span></div></foreignObject></g></g><g class="node default" id="flowchart-D-5" transform="translate(394.8125, 643)"><rect class="basic label-container" style="" x="-130" y="-51" width="260" height="102"/><g class="label" style="" transform="translate(-100, -36)"><rect/><foreignObject width="200" height="72"><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>Validate body against\nevent.schema (ajv)</p></span></div></foreignObject></g></g><g class="node default" id="flowchart-E-7" transform="translate(394.8125, 795.3203125)"><polygon points="51.3203125,0 102.640625,-51.3203125 51.3203125,-102.640625 0,-51.3203125" class="label-container" transform="translate(-50.8203125, 51.3203125)"/><g class="label" style="" transform="translate(-24.3203125, -12)"><rect/><foreignObject width="48.640625" 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>Match?</p></span></div></foreignObject></g></g><g class="node default" id="flowchart-F-9" transform="translate(266.40625, 1023.46875)"><polygon points="102.828125,0 205.65625,-102.828125 102.828125,-205.65625 0,-102.828125" class="label-container" transform="translate(-102.328125, 102.828125)"/><g class="label" style="" transform="translate(-75.828125, -12)"><rect/><foreignObject width="151.65625" 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>event.map\ndefined?</p></span></div></foreignObject></g></g><g class="node default" id="flowchart-G-11" transform="translate(138, 1239.296875)"><rect class="basic label-container" style="" x="-130" y="-39" width="260" height="78"/><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>Transform body\nvia JsonMap</p></span></div></foreignObject></g></g><g class="node default" id="flowchart-H-13" transform="translate(394.8125, 1239.296875)"><rect class="basic label-container" style="" x="-76.8125" y="-27" width="153.625" height="54"/><g class="label" style="" transform="translate(-46.8125, -12)"><rect/><foreignObject width="93.625" 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>Use full body</p></span></div></foreignObject></g></g><g class="node default" id="flowchart-I-15" transform="translate(266.40625, 1367.296875)"><rect class="basic label-container" style="" x="-130" y="-39" width="260" height="78"/><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>Append to durable\nJSONL queue</p></span></div></foreignObject></g></g><g class="node default" id="flowchart-J-19" transform="translate(266.40625, 1495.296875)"><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>Return 200\n{ matched: ‹event-name› }</p></span></div></foreignObject></g></g><g class="node default" id="flowchart-K-21" transform="translate(651.625, 1023.46875)"><rect class="basic label-container" style="" x="-94.53125" y="-27" width="189.0625" height="54"/><g class="label" style="" transform="translate(-64.53125, -12)"><rect/><foreignObject width="129.0625" 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>Log as unmatched</p></span></div></foreignObject></g></g><g class="node default" id="flowchart-L-23" transform="translate(651.625, 1239.296875)"><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>Return 200\n{ matched: null }</p></span></div></foreignObject></g></g></g></g></g></svg>