@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.
- package/.env.local +13 -0
- package/.env.local.template +13 -0
- package/.tsbuildinfo +1 -0
- package/CHANGELOG.md +450 -0
- package/about.md +82 -0
- package/client/README.md +73 -0
- package/client/eslint.config.js +23 -0
- package/client/index.html +14 -0
- package/client/package-lock.json +5181 -0
- package/client/package.json +60 -0
- package/client/public/vite.svg +1 -0
- package/client/src/App.tsx +22 -0
- package/client/src/components/AccountMenu.tsx +167 -0
- package/client/src/components/ActionDropdown.tsx +120 -0
- package/client/src/components/CodeEditor.tsx +143 -0
- package/client/src/components/CodeViewer.tsx +113 -0
- package/client/src/components/ConfirmDialog.tsx +32 -0
- package/client/src/components/DirectoryRow.tsx +62 -0
- package/client/src/components/DirectoryTable.tsx +42 -0
- package/client/src/components/DownloadDropdown.tsx +116 -0
- package/client/src/components/DriveList.tsx +54 -0
- package/client/src/components/EmbeddedDiagramPanzoom.ts +28 -0
- package/client/src/components/FileContentView.tsx +155 -0
- package/client/src/components/InlineSvgPanzoom.ts +60 -0
- package/client/src/components/LazyDiagram.ts +93 -0
- package/client/src/components/LinkDropdown.tsx +134 -0
- package/client/src/components/MarkdownView.tsx +115 -0
- package/client/src/components/MermaidViewer.tsx +21 -0
- package/client/src/components/PlantUmlViewer.tsx +21 -0
- package/client/src/components/SearchModal.tsx +424 -0
- package/client/src/components/SvgViewer.tsx +107 -0
- package/client/src/components/TabBar.tsx +96 -0
- package/client/src/components/layout/Header.tsx +270 -0
- package/client/src/components/panzoom.ts +203 -0
- package/client/src/components/renderableUtils.ts +15 -0
- package/client/src/components/runner/JobTable.tsx +153 -0
- package/client/src/components/runner/RunHistory.tsx +140 -0
- package/client/src/components/runner/StatsBar.tsx +43 -0
- package/client/src/components/runner/StatusPill.tsx +27 -0
- package/client/src/components/runner/jobTableUtils.ts +65 -0
- package/client/src/components/scrollUtils.ts +39 -0
- package/client/src/components/ui/alert-dialog.tsx +107 -0
- package/client/src/components/ui/button.tsx +40 -0
- package/client/src/components/ui/dropdown-menu.tsx +79 -0
- package/client/src/components/ui/input.tsx +26 -0
- package/client/src/components/useActionState.ts +43 -0
- package/client/src/hooks/useFileBrowser.ts +102 -0
- package/client/src/hooks/useFileData.ts +78 -0
- package/client/src/hooks/useScrollAnchor.ts +70 -0
- package/client/src/hooks/useShareSettings.ts +22 -0
- package/client/src/hooks/useTopBar.ts +27 -0
- package/client/src/index.css +281 -0
- package/client/src/lib/AuthContext.ts +27 -0
- package/client/src/lib/api.ts +239 -0
- package/client/src/lib/auth.tsx +50 -0
- package/client/src/lib/codeBlockCm6.ts +129 -0
- package/client/src/lib/codeBlockCopy.ts +43 -0
- package/client/src/lib/codemirror.ts +77 -0
- package/client/src/lib/runner-api.ts +172 -0
- package/client/src/lib/svg.ts +50 -0
- package/client/src/lib/theme.ts +34 -0
- package/client/src/lib/utils.ts +6 -0
- package/client/src/main.tsx +11 -0
- package/client/src/pages/FileBrowser.tsx +135 -0
- package/client/src/pages/Home.tsx +46 -0
- package/client/src/pages/Runner.tsx +151 -0
- package/client/src/pages/RunnerJob.tsx +170 -0
- package/client/tsconfig.app.json +32 -0
- package/client/tsconfig.json +7 -0
- package/client/tsconfig.node.json +26 -0
- package/client/vite.config.ts +35 -0
- package/content/privacy.md +61 -0
- package/content/terms.md +41 -0
- package/dist/client/assets/CodeEditor-0XHVI8Nu.js +1 -0
- package/dist/client/assets/CodeViewer-CykMVsfX.js +1 -0
- package/dist/client/assets/index--MBieNJA.js +1 -0
- package/dist/client/assets/index-BENeXQI_.js +1 -0
- package/dist/client/assets/index-BbBpoOxz.js +1 -0
- package/dist/client/assets/index-BdV9g5AM.js +6 -0
- package/dist/client/assets/index-BjAilRri.js +2 -0
- package/dist/client/assets/index-BqbhWo2I.js +3 -0
- package/dist/client/assets/index-CVbycZ0H.js +1 -0
- package/dist/client/assets/index-Cs5oz2oJ.js +5 -0
- package/dist/client/assets/index-D8KZVveX.js +1 -0
- package/dist/client/assets/index-DC4HMHxY.js +13 -0
- package/dist/client/assets/index-DbMebkkd.css +1 -0
- package/dist/client/assets/index-DcY2RXqX.js +1 -0
- package/dist/client/assets/index-Duy-tZYV.js +1 -0
- package/dist/client/assets/index-Dw7rDFmE.js +7 -0
- package/dist/client/assets/index-FlCUvrjv.js +2 -0
- package/dist/client/assets/index-K6OVmfhg.js +1 -0
- package/dist/client/assets/index-LjwgzZ7F.js +62 -0
- package/dist/client/assets/index-MLwyFRN0.js +1 -0
- package/dist/client/assets/index-OpqBpSjn.js +1 -0
- package/dist/client/assets/index-SsHei0HE.js +1 -0
- package/dist/client/assets/index-uQa2yckk.js +1 -0
- package/dist/client/assets/index-udkXoIER.js +1 -0
- package/dist/client/index.html +15 -0
- package/dist/client/vite.svg +1 -0
- package/dist/src/auth/google.js +57 -0
- package/dist/src/auth/keys.js +185 -0
- package/dist/src/auth/resolve.js +102 -0
- package/dist/src/auth/session.js +57 -0
- package/dist/src/cli/commands/config.js +100 -0
- package/dist/src/cli/commands/config.test.js +84 -0
- package/dist/src/cli/commands/service.js +93 -0
- package/dist/src/cli/commands/start.js +24 -0
- package/dist/src/cli/index.js +20 -0
- package/dist/src/config/index.js +90 -0
- package/dist/src/config/loadConfig.test.js +127 -0
- package/dist/src/config/resolve.js +134 -0
- package/dist/src/config/resolve.test.js +148 -0
- package/dist/src/config/schema.js +159 -0
- package/dist/src/config/substituteEnvVars.js +45 -0
- package/dist/src/config/substituteEnvVars.test.js +51 -0
- package/dist/src/config/types.js +5 -0
- package/dist/src/routes/api/auth-status.js +56 -0
- package/dist/src/routes/api/diagrams.js +35 -0
- package/dist/src/routes/api/directory.js +93 -0
- package/dist/src/routes/api/drives.js +15 -0
- package/dist/src/routes/api/export.js +218 -0
- package/dist/src/routes/api/fileContent.js +286 -0
- package/dist/src/routes/api/index.js +33 -0
- package/dist/src/routes/api/linkInfo.js +71 -0
- package/dist/src/routes/api/linkInfo.test.js +104 -0
- package/dist/src/routes/api/middleware.js +117 -0
- package/dist/src/routes/api/raw.js +38 -0
- package/dist/src/routes/api/runner.js +59 -0
- package/dist/src/routes/api/search.js +236 -0
- package/dist/src/routes/api/sharing.js +203 -0
- package/dist/src/routes/api/status.js +68 -0
- package/dist/src/routes/api/status.test.js +62 -0
- package/dist/src/routes/auth.js +99 -0
- package/dist/src/routes/event.js +77 -0
- package/dist/src/routes/event.test.js +206 -0
- package/dist/src/routes/health.js +10 -0
- package/dist/src/routes/keys.js +129 -0
- package/dist/src/routes/path/index.js +17 -0
- package/dist/src/routes/static.js +30 -0
- package/dist/src/server.js +90 -0
- package/dist/src/services/deepShareLinks.js +163 -0
- package/dist/src/services/diagramCache.js +104 -0
- package/dist/src/services/embeddedDiagrams.js +136 -0
- package/dist/src/services/eventLog.js +55 -0
- package/dist/src/services/eventLog.test.js +113 -0
- package/dist/src/services/eventQueue.js +154 -0
- package/dist/src/services/eventQueue.test.js +104 -0
- package/dist/src/services/export.js +220 -0
- package/dist/src/services/exportCache.js +196 -0
- package/dist/src/services/markdown.js +147 -0
- package/dist/src/services/mermaid.js +97 -0
- package/dist/src/services/plantuml.js +145 -0
- package/dist/src/services/puppeteer.js +156 -0
- package/dist/src/util/breadcrumbs.js +22 -0
- package/dist/src/util/crypto.js +56 -0
- package/dist/src/util/crypto.test.js +99 -0
- package/dist/src/util/fileDetection.js +66 -0
- package/dist/src/util/fileDetection.test.js +89 -0
- package/dist/src/util/formatters.js +43 -0
- package/dist/src/util/formatters.test.js +83 -0
- package/dist/src/util/packageVersion.js +25 -0
- package/dist/src/util/platform.js +148 -0
- package/dist/src/util/state.js +46 -0
- package/dist/vitest.config.js +12 -0
- package/favicon.svg +3 -0
- package/guides/access-decision-flow.mmd +24 -0
- package/guides/access-decision-flow.svg +1 -0
- package/guides/api-integration.md +236 -0
- package/guides/deployment.md +287 -0
- package/guides/event-gateway.md +204 -0
- package/guides/event-gateway.mmd +17 -0
- package/guides/event-gateway.svg +1 -0
- package/guides/exports.md +239 -0
- package/guides/setup.md +313 -0
- package/guides/sharing.md +204 -0
- package/jeeves-server.config.template.json +25 -0
- package/package.json +124 -0
- package/scripts/download-plantuml.js +70 -0
- package/src/auth/google.ts +93 -0
- package/src/auth/keys.ts +252 -0
- package/src/auth/resolve.ts +157 -0
- package/src/auth/session.ts +77 -0
- package/src/cli/commands/config.test.ts +107 -0
- package/src/cli/commands/config.ts +113 -0
- package/src/cli/commands/service.ts +129 -0
- package/src/cli/commands/start.ts +27 -0
- package/src/cli/index.ts +25 -0
- package/src/config/index.ts +113 -0
- package/src/config/loadConfig.test.ts +155 -0
- package/src/config/resolve.test.ts +192 -0
- package/src/config/resolve.ts +173 -0
- package/src/config/schema.ts +179 -0
- package/src/config/substituteEnvVars.test.ts +64 -0
- package/src/config/substituteEnvVars.ts +52 -0
- package/src/config/types.ts +129 -0
- package/src/routes/api/auth-status.ts +85 -0
- package/src/routes/api/diagrams.ts +53 -0
- package/src/routes/api/directory.ts +123 -0
- package/src/routes/api/drives.ts +23 -0
- package/src/routes/api/export.ts +314 -0
- package/src/routes/api/fileContent.ts +414 -0
- package/src/routes/api/index.ts +37 -0
- package/src/routes/api/linkInfo.test.ts +132 -0
- package/src/routes/api/linkInfo.ts +83 -0
- package/src/routes/api/middleware.ts +156 -0
- package/src/routes/api/raw.ts +54 -0
- package/src/routes/api/runner.ts +107 -0
- package/src/routes/api/search.ts +321 -0
- package/src/routes/api/sharing.ts +259 -0
- package/src/routes/api/status.test.ts +72 -0
- package/src/routes/api/status.ts +82 -0
- package/src/routes/auth.ts +143 -0
- package/src/routes/event.test.ts +248 -0
- package/src/routes/event.ts +109 -0
- package/src/routes/health.ts +13 -0
- package/src/routes/keys.ts +192 -0
- package/src/routes/path/index.ts +24 -0
- package/src/routes/static.ts +54 -0
- package/src/server.ts +104 -0
- package/src/services/deepShareLinks.ts +203 -0
- package/src/services/diagramCache.ts +128 -0
- package/src/services/embeddedDiagrams.ts +168 -0
- package/src/services/eventLog.test.ts +144 -0
- package/src/services/eventLog.ts +68 -0
- package/src/services/eventQueue.test.ts +127 -0
- package/src/services/eventQueue.ts +196 -0
- package/src/services/export.ts +267 -0
- package/src/services/exportCache.ts +216 -0
- package/src/services/markdown.ts +189 -0
- package/src/services/mermaid.ts +113 -0
- package/src/services/plantuml.ts +172 -0
- package/src/services/puppeteer.ts +188 -0
- package/src/types/fastify.d.ts +13 -0
- package/src/types/jsonmap.d.ts +10 -0
- package/src/types/plantuml-encoder.d.ts +4 -0
- package/src/util/breadcrumbs.ts +33 -0
- package/src/util/crypto.test.ts +132 -0
- package/src/util/crypto.ts +79 -0
- package/src/util/fileDetection.test.ts +115 -0
- package/src/util/fileDetection.ts +70 -0
- package/src/util/formatters.test.ts +105 -0
- package/src/util/formatters.ts +44 -0
- package/src/util/packageVersion.ts +30 -0
- package/src/util/platform.ts +178 -0
- package/src/util/state.ts +55 -0
- package/test-docs/diagram-retry-test.md +18 -0
- package/test-docs/embedded-diagrams.md +52 -0
- package/test-docs/lazy-diagrams-test.md +333 -0
- package/test-docs/page-a.md +7 -0
- package/test-docs/page-b.md +7 -0
- package/test-docs/page-c.md +7 -0
- package/test-docs/sub/page-d.md +7 -0
- package/test-docs/test-diagram.puml +13 -0
- package/test-docs/validate-deep-share.js +318 -0
- package/tsconfig.json +37 -0
- package/tsdoc.json +13 -0
- package/vendor/.plantuml-version +1 -0
- package/vendor/plantuml.jar +0 -0
- package/vitest.config.js +12 -0
- 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
|
+

|
|
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>
|