@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,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @packageDocumentation
|
|
3
|
+
*
|
|
4
|
+
* CLI commands: service install, service uninstall, service start/stop/restart.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Command } from '@commander-js/extra-typings';
|
|
8
|
+
|
|
9
|
+
const DEFAULT_SERVICE_NAME = 'JeevesServer';
|
|
10
|
+
const LINUX_SERVICE_NAME = 'jeeves-server';
|
|
11
|
+
|
|
12
|
+
export function registerServiceCommand(cli: Command): void {
|
|
13
|
+
const service = cli
|
|
14
|
+
.command('service')
|
|
15
|
+
.description('System service management');
|
|
16
|
+
|
|
17
|
+
service.addCommand(
|
|
18
|
+
new Command('install')
|
|
19
|
+
.description('Print install instructions for a system service')
|
|
20
|
+
.option('-c, --config <path>', 'Path to configuration file')
|
|
21
|
+
.option(
|
|
22
|
+
'-n, --name <name>',
|
|
23
|
+
'Service name',
|
|
24
|
+
process.platform === 'win32'
|
|
25
|
+
? DEFAULT_SERVICE_NAME
|
|
26
|
+
: LINUX_SERVICE_NAME,
|
|
27
|
+
)
|
|
28
|
+
.action((options) => {
|
|
29
|
+
const name = options.name;
|
|
30
|
+
const configArg = options.config ? ` --config "${options.config}"` : '';
|
|
31
|
+
|
|
32
|
+
if (process.platform === 'win32') {
|
|
33
|
+
console.log('# NSSM install commands:');
|
|
34
|
+
console.log(
|
|
35
|
+
`nssm install ${name} node "%CD%\\node_modules\\@karmaniverous\\jeeves-server\\dist\\src\\cli\\index.js" start${configArg}`,
|
|
36
|
+
);
|
|
37
|
+
console.log(`nssm set ${name} AppDirectory "%CD%"`);
|
|
38
|
+
console.log(`nssm set ${name} AppStdout "%CD%\\logs\\service.log"`);
|
|
39
|
+
console.log(
|
|
40
|
+
`nssm set ${name} AppStderr "%CD%\\logs\\service-error.log"`,
|
|
41
|
+
);
|
|
42
|
+
console.log(`nssm set ${name} Start SERVICE_AUTO_START`);
|
|
43
|
+
console.log(`nssm start ${name}`);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const unit = [
|
|
48
|
+
'[Unit]',
|
|
49
|
+
'Description=Jeeves Server',
|
|
50
|
+
'After=network.target',
|
|
51
|
+
'',
|
|
52
|
+
'[Service]',
|
|
53
|
+
'Type=simple',
|
|
54
|
+
'WorkingDirectory=%h',
|
|
55
|
+
`ExecStart=/usr/bin/env jeeves-server start${configArg}`,
|
|
56
|
+
'Restart=on-failure',
|
|
57
|
+
'',
|
|
58
|
+
'[Install]',
|
|
59
|
+
'WantedBy=default.target',
|
|
60
|
+
].join('\n');
|
|
61
|
+
|
|
62
|
+
console.log('# systemd unit file');
|
|
63
|
+
console.log(`# Save to: ~/.config/systemd/user/${name}.service`);
|
|
64
|
+
console.log('');
|
|
65
|
+
console.log(unit);
|
|
66
|
+
console.log('');
|
|
67
|
+
console.log('# Then run:');
|
|
68
|
+
console.log('systemctl --user daemon-reload');
|
|
69
|
+
console.log(`systemctl --user enable --now ${name}.service`);
|
|
70
|
+
}),
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
service.addCommand(
|
|
74
|
+
new Command('uninstall')
|
|
75
|
+
.description('Print uninstall instructions for a system service')
|
|
76
|
+
.option(
|
|
77
|
+
'-n, --name <name>',
|
|
78
|
+
'Service name',
|
|
79
|
+
process.platform === 'win32'
|
|
80
|
+
? DEFAULT_SERVICE_NAME
|
|
81
|
+
: LINUX_SERVICE_NAME,
|
|
82
|
+
)
|
|
83
|
+
.action((options) => {
|
|
84
|
+
const name = options.name;
|
|
85
|
+
|
|
86
|
+
if (process.platform === 'win32') {
|
|
87
|
+
console.log('# NSSM uninstall commands:');
|
|
88
|
+
console.log(`nssm stop ${name}`);
|
|
89
|
+
console.log(`nssm remove ${name} confirm`);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
console.log('# systemd uninstall:');
|
|
94
|
+
console.log(`systemctl --user disable --now ${name}.service`);
|
|
95
|
+
console.log(`rm ~/.config/systemd/user/${name}.service`);
|
|
96
|
+
console.log('systemctl --user daemon-reload');
|
|
97
|
+
}),
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
for (const action of ['start', 'stop', 'restart'] as const) {
|
|
101
|
+
service.addCommand(
|
|
102
|
+
new Command(action)
|
|
103
|
+
.description(
|
|
104
|
+
`${action.charAt(0).toUpperCase() + action.slice(1)} the system service`,
|
|
105
|
+
)
|
|
106
|
+
.option(
|
|
107
|
+
'-n, --name <name>',
|
|
108
|
+
'Service name',
|
|
109
|
+
process.platform === 'win32'
|
|
110
|
+
? DEFAULT_SERVICE_NAME
|
|
111
|
+
: LINUX_SERVICE_NAME,
|
|
112
|
+
)
|
|
113
|
+
.action((options) => {
|
|
114
|
+
const name = options.name;
|
|
115
|
+
|
|
116
|
+
if (process.platform === 'win32') {
|
|
117
|
+
if (action === 'restart') {
|
|
118
|
+
console.log(`nssm restart ${name}`);
|
|
119
|
+
} else {
|
|
120
|
+
console.log(`nssm ${action} ${name}`);
|
|
121
|
+
}
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
console.log(`systemctl --user ${action} ${name}.service`);
|
|
126
|
+
}),
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @packageDocumentation
|
|
3
|
+
*
|
|
4
|
+
* CLI command: start — launches the Fastify server.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Command } from '@commander-js/extra-typings';
|
|
8
|
+
|
|
9
|
+
export function registerStartCommand(cli: Command): void {
|
|
10
|
+
cli
|
|
11
|
+
.command('start')
|
|
12
|
+
.description('Start the jeeves-server')
|
|
13
|
+
.option('-c, --config <path>', 'Path to configuration file')
|
|
14
|
+
.action(async (options) => {
|
|
15
|
+
try {
|
|
16
|
+
// Dynamic import to avoid loading server code at CLI parse time
|
|
17
|
+
const { initConfig } = await import('../../config/index.js');
|
|
18
|
+
await initConfig(options.config);
|
|
19
|
+
|
|
20
|
+
// Import server after config is initialized
|
|
21
|
+
await import('../../server.js');
|
|
22
|
+
} catch (error) {
|
|
23
|
+
console.error('Failed to start:', error);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
}
|
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @packageDocumentation
|
|
4
|
+
*
|
|
5
|
+
* jeeves-server CLI entrypoint.
|
|
6
|
+
* Commands: start, config validate, config show, service install/uninstall.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { Command } from '@commander-js/extra-typings';
|
|
10
|
+
|
|
11
|
+
import { packageVersion } from '../util/packageVersion.js';
|
|
12
|
+
import { registerConfigCommand } from './commands/config.js';
|
|
13
|
+
import { registerServiceCommand } from './commands/service.js';
|
|
14
|
+
import { registerStartCommand } from './commands/start.js';
|
|
15
|
+
|
|
16
|
+
const cli = new Command()
|
|
17
|
+
.name('jeeves-server')
|
|
18
|
+
.description('Self-hosted file browser, document server, and webhook gateway')
|
|
19
|
+
.version(packageVersion);
|
|
20
|
+
|
|
21
|
+
registerStartCommand(cli);
|
|
22
|
+
registerConfigCommand(cli);
|
|
23
|
+
registerServiceCommand(cli);
|
|
24
|
+
|
|
25
|
+
cli.parse();
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @packageDocumentation
|
|
3
|
+
*
|
|
4
|
+
* Config loading and singleton management.
|
|
5
|
+
* Loads config via cosmiconfig, validates with Zod, applies env var substitution,
|
|
6
|
+
* resolves runtime types via resolve.ts, and exposes getConfig()/resetConfig().
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
|
|
12
|
+
import { cosmiconfig } from 'cosmiconfig';
|
|
13
|
+
|
|
14
|
+
import { buildRuntimeConfig } from './resolve.js';
|
|
15
|
+
import { jeevesConfigSchema } from './schema.js';
|
|
16
|
+
import { substituteEnvVars } from './substituteEnvVars.js';
|
|
17
|
+
import type { RuntimeConfig } from './types.js';
|
|
18
|
+
|
|
19
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const rootDir = path.resolve(__dirname, '../../..');
|
|
21
|
+
|
|
22
|
+
const MODULE_NAME = 'jeeves-server';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Load and validate jeeves-server configuration via cosmiconfig.
|
|
26
|
+
*
|
|
27
|
+
* Searches for `jeeves-server.config.{json,yaml,yml,js,ts,cjs,mjs}`
|
|
28
|
+
* or `.jeeves-serverrc` in the package root and parent directories.
|
|
29
|
+
*
|
|
30
|
+
* @param configPath - Optional explicit path to a config file.
|
|
31
|
+
* @returns Resolved runtime configuration.
|
|
32
|
+
*/
|
|
33
|
+
export async function loadConfig(configPath?: string): Promise<RuntimeConfig> {
|
|
34
|
+
const explorer = cosmiconfig(MODULE_NAME, {
|
|
35
|
+
searchPlaces: [
|
|
36
|
+
'package.json',
|
|
37
|
+
`.${MODULE_NAME}rc`,
|
|
38
|
+
`.${MODULE_NAME}rc.json`,
|
|
39
|
+
`.${MODULE_NAME}rc.yaml`,
|
|
40
|
+
`.${MODULE_NAME}rc.yml`,
|
|
41
|
+
`${MODULE_NAME}.config.json`,
|
|
42
|
+
`${MODULE_NAME}.config.yaml`,
|
|
43
|
+
`${MODULE_NAME}.config.yml`,
|
|
44
|
+
`${MODULE_NAME}.config.js`,
|
|
45
|
+
`${MODULE_NAME}.config.ts`,
|
|
46
|
+
`${MODULE_NAME}.config.mjs`,
|
|
47
|
+
`${MODULE_NAME}.config.cjs`,
|
|
48
|
+
],
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const result = configPath
|
|
52
|
+
? await explorer.load(configPath)
|
|
53
|
+
: await explorer.search(rootDir);
|
|
54
|
+
|
|
55
|
+
if (!result || result.isEmpty) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
`No jeeves-server configuration found. Create a jeeves-server.config.json (or .yaml) file.\n` +
|
|
58
|
+
`Searched from: ${rootDir}`,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const substituted = substituteEnvVars(
|
|
63
|
+
result.config as Record<string, unknown>,
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const parseResult = jeevesConfigSchema.safeParse(substituted);
|
|
67
|
+
if (!parseResult.success) {
|
|
68
|
+
const issues = parseResult.error.issues
|
|
69
|
+
.map((i) => ` - ${i.path.join('.')}: ${i.message}`)
|
|
70
|
+
.join('\n');
|
|
71
|
+
throw new Error(`Invalid configuration in ${result.filepath}:\n${issues}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return buildRuntimeConfig(parseResult.data, rootDir, result.filepath);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let configInstance: RuntimeConfig | null = null;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Check if the config singleton has been initialized.
|
|
81
|
+
*/
|
|
82
|
+
export function isConfigInitialized(): boolean {
|
|
83
|
+
return configInstance !== null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Get the singleton config instance. Initializes on first call.
|
|
88
|
+
* @throws If config has not been initialized — call initConfig() first.
|
|
89
|
+
*/
|
|
90
|
+
export function getConfig(): RuntimeConfig {
|
|
91
|
+
if (!configInstance) {
|
|
92
|
+
throw new Error(
|
|
93
|
+
'Config not initialized. Call initConfig() before getConfig().',
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
return configInstance;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Initialize the config singleton. Must be called once at startup.
|
|
101
|
+
* @param configPath - Optional explicit path to a config file.
|
|
102
|
+
*/
|
|
103
|
+
export async function initConfig(configPath?: string): Promise<RuntimeConfig> {
|
|
104
|
+
configInstance = await loadConfig(configPath);
|
|
105
|
+
return configInstance;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Reset the config singleton (for testing).
|
|
110
|
+
*/
|
|
111
|
+
export function resetConfig(): void {
|
|
112
|
+
configInstance = null;
|
|
113
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
6
|
+
|
|
7
|
+
import { getConfig, initConfig, loadConfig, resetConfig } from './index.js';
|
|
8
|
+
|
|
9
|
+
const VALID_CONFIG = {
|
|
10
|
+
port: 9999,
|
|
11
|
+
chromePath: '/usr/bin/chromium',
|
|
12
|
+
auth: { modes: ['keys'] },
|
|
13
|
+
keys: {
|
|
14
|
+
primary: 'a'.repeat(64),
|
|
15
|
+
_internal: 'b'.repeat(64),
|
|
16
|
+
},
|
|
17
|
+
events: {},
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function writeConfig(dir: string, config: unknown): string {
|
|
21
|
+
const filePath = path.join(dir, 'jeeves-server.config.json');
|
|
22
|
+
fs.writeFileSync(filePath, JSON.stringify(config));
|
|
23
|
+
return filePath;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe('loadConfig', () => {
|
|
27
|
+
let tmpDir: string;
|
|
28
|
+
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'jeeves-config-'));
|
|
31
|
+
resetConfig();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
36
|
+
resetConfig();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('loads a valid JSON config file', async () => {
|
|
40
|
+
const configPath = writeConfig(tmpDir, VALID_CONFIG);
|
|
41
|
+
const config = await loadConfig(configPath);
|
|
42
|
+
expect(config.port).toBe(9999);
|
|
43
|
+
expect(config.chromePath).toBe('/usr/bin/chromium');
|
|
44
|
+
expect(config.configPath).toBe(configPath);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('throws on missing config', async () => {
|
|
48
|
+
await expect(
|
|
49
|
+
loadConfig(path.join(tmpDir, 'nonexistent.json')),
|
|
50
|
+
).rejects.toThrow();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('throws on invalid config (missing auth)', async () => {
|
|
54
|
+
const configPath = writeConfig(tmpDir, { port: 1234 });
|
|
55
|
+
await expect(loadConfig(configPath)).rejects.toThrow(
|
|
56
|
+
'Invalid configuration',
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('applies env var substitution', async () => {
|
|
61
|
+
const original = process.env['TEST_CHROME_PATH'];
|
|
62
|
+
process.env['TEST_CHROME_PATH'] = '/custom/chrome';
|
|
63
|
+
try {
|
|
64
|
+
const configPath = writeConfig(tmpDir, {
|
|
65
|
+
...VALID_CONFIG,
|
|
66
|
+
chromePath: '${TEST_CHROME_PATH}',
|
|
67
|
+
});
|
|
68
|
+
const config = await loadConfig(configPath);
|
|
69
|
+
expect(config.chromePath).toBe('/custom/chrome');
|
|
70
|
+
} finally {
|
|
71
|
+
if (original === undefined) delete process.env['TEST_CHROME_PATH'];
|
|
72
|
+
else process.env['TEST_CHROME_PATH'] = original;
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('applies default port when omitted', async () => {
|
|
77
|
+
const noPort = { ...VALID_CONFIG };
|
|
78
|
+
delete (noPort as Record<string, unknown>).port;
|
|
79
|
+
const configPath = writeConfig(tmpDir, noPort);
|
|
80
|
+
const config = await loadConfig(configPath);
|
|
81
|
+
expect(config.port).toBe(1934);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('rejects _plugin key with scopes', async () => {
|
|
85
|
+
const configPath = writeConfig(tmpDir, {
|
|
86
|
+
...VALID_CONFIG,
|
|
87
|
+
keys: {
|
|
88
|
+
...VALID_CONFIG.keys,
|
|
89
|
+
_plugin: { key: 'c'.repeat(64), scopes: ['/restricted'] },
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
await expect(loadConfig(configPath)).rejects.toThrow(
|
|
93
|
+
'_plugin key must not have scopes',
|
|
94
|
+
);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('rejects _internal key with scopes', async () => {
|
|
98
|
+
const configPath = writeConfig(tmpDir, {
|
|
99
|
+
...VALID_CONFIG,
|
|
100
|
+
keys: {
|
|
101
|
+
...VALID_CONFIG.keys,
|
|
102
|
+
_internal: { key: 'b'.repeat(64), scopes: ['/restricted'] },
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
await expect(loadConfig(configPath)).rejects.toThrow(
|
|
106
|
+
'_internal key must not have scopes',
|
|
107
|
+
);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('accepts _plugin key without scopes', async () => {
|
|
111
|
+
const configPath = writeConfig(tmpDir, {
|
|
112
|
+
...VALID_CONFIG,
|
|
113
|
+
keys: {
|
|
114
|
+
...VALID_CONFIG.keys,
|
|
115
|
+
_plugin: 'c'.repeat(64),
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
const config = await loadConfig(configPath);
|
|
119
|
+
expect(config.resolvedKeys.find((k) => k.name === '_plugin')?.seed).toBe(
|
|
120
|
+
'c'.repeat(64),
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe('config singleton', () => {
|
|
126
|
+
let tmpDir: string;
|
|
127
|
+
|
|
128
|
+
beforeEach(() => {
|
|
129
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'jeeves-config-'));
|
|
130
|
+
resetConfig();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
afterEach(() => {
|
|
134
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
135
|
+
resetConfig();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('throws if getConfig called before initConfig', () => {
|
|
139
|
+
expect(() => getConfig()).toThrow('Config not initialized');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('initConfig populates getConfig', async () => {
|
|
143
|
+
const configPath = writeConfig(tmpDir, VALID_CONFIG);
|
|
144
|
+
await initConfig(configPath);
|
|
145
|
+
const config = getConfig();
|
|
146
|
+
expect(config.port).toBe(9999);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('resetConfig clears the singleton', async () => {
|
|
150
|
+
const configPath = writeConfig(tmpDir, VALID_CONFIG);
|
|
151
|
+
await initConfig(configPath);
|
|
152
|
+
resetConfig();
|
|
153
|
+
expect(() => getConfig()).toThrow('Config not initialized');
|
|
154
|
+
});
|
|
155
|
+
});
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
buildRuntimeConfig,
|
|
9
|
+
deriveInternalKey,
|
|
10
|
+
normalizeScopes,
|
|
11
|
+
resolveInsiders,
|
|
12
|
+
resolveKeys,
|
|
13
|
+
resolvePlantuml,
|
|
14
|
+
} from './resolve.js';
|
|
15
|
+
import type { JeevesConfig } from './schema.js';
|
|
16
|
+
|
|
17
|
+
describe('normalizeScopes', () => {
|
|
18
|
+
it('returns null for undefined', () => {
|
|
19
|
+
expect(normalizeScopes(undefined)).toBe(null);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('returns null for null', () => {
|
|
23
|
+
expect(normalizeScopes(null)).toBe(null);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('wraps a string in allow array', () => {
|
|
27
|
+
expect(normalizeScopes('/docs')).toEqual({ allow: ['/docs'], deny: [] });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('wraps an array as allow', () => {
|
|
31
|
+
expect(normalizeScopes(['/a', '/b'])).toEqual({
|
|
32
|
+
allow: ['/a', '/b'],
|
|
33
|
+
deny: [],
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('fills defaults for partial object', () => {
|
|
38
|
+
expect(normalizeScopes({ deny: ['/secret'] })).toEqual({
|
|
39
|
+
allow: ['/**'],
|
|
40
|
+
deny: ['/secret'],
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('passes through complete object', () => {
|
|
45
|
+
const scopes = { allow: ['/a'], deny: ['/b'] };
|
|
46
|
+
expect(normalizeScopes(scopes)).toEqual(scopes);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('resolveKeys', () => {
|
|
51
|
+
it('handles string key entries', () => {
|
|
52
|
+
const result = resolveKeys({ primary: 'seed123' });
|
|
53
|
+
expect(result).toEqual([
|
|
54
|
+
{ name: 'primary', seed: 'seed123', scopes: null },
|
|
55
|
+
]);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('handles object key entries with scopes', () => {
|
|
59
|
+
const result = resolveKeys({
|
|
60
|
+
scoped: { key: 'seed456', scopes: ['/docs'] },
|
|
61
|
+
});
|
|
62
|
+
expect(result[0].name).toBe('scoped');
|
|
63
|
+
expect(result[0].seed).toBe('seed456');
|
|
64
|
+
expect(result[0].scopes).toEqual({ allow: ['/docs'], deny: [] });
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('handles mixed entries', () => {
|
|
68
|
+
const result = resolveKeys({
|
|
69
|
+
plain: 'abc',
|
|
70
|
+
complex: { key: 'def', scopes: { allow: ['/x'], deny: ['/y'] } },
|
|
71
|
+
});
|
|
72
|
+
expect(result).toHaveLength(2);
|
|
73
|
+
expect(result[0].scopes).toBe(null);
|
|
74
|
+
expect(result[1].scopes).toEqual({ allow: ['/x'], deny: ['/y'] });
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe('resolveInsiders', () => {
|
|
79
|
+
let tmpDir: string;
|
|
80
|
+
|
|
81
|
+
beforeEach(() => {
|
|
82
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'jeeves-resolve-'));
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
afterEach(() => {
|
|
86
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('normalizes email to lowercase', () => {
|
|
90
|
+
const stateFile = path.join(tmpDir, 'state.json');
|
|
91
|
+
const result = resolveInsiders({ 'Test@Example.COM': {} }, stateFile);
|
|
92
|
+
expect(result[0].email).toBe('test@example.com');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('merges state keys when available', () => {
|
|
96
|
+
const stateFile = path.join(tmpDir, 'state.json');
|
|
97
|
+
fs.writeFileSync(
|
|
98
|
+
stateFile,
|
|
99
|
+
JSON.stringify({
|
|
100
|
+
insiderKeys: {
|
|
101
|
+
'test@example.com': { seed: 'stateseed', createdAt: '2026-01-01' },
|
|
102
|
+
},
|
|
103
|
+
}),
|
|
104
|
+
);
|
|
105
|
+
const result = resolveInsiders({ 'test@example.com': {} }, stateFile);
|
|
106
|
+
expect(result[0].seed).toBe('stateseed');
|
|
107
|
+
expect(result[0].keyCreatedAt).toBe('2026-01-01');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('returns empty seed when no state exists', () => {
|
|
111
|
+
const stateFile = path.join(tmpDir, 'state.json');
|
|
112
|
+
const result = resolveInsiders({ 'new@example.com': {} }, stateFile);
|
|
113
|
+
expect(result[0].seed).toBe('');
|
|
114
|
+
expect(result[0].keyCreatedAt).toBe(null);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe('resolvePlantuml', () => {
|
|
119
|
+
it('appends community server as fallback', () => {
|
|
120
|
+
const result = resolvePlantuml();
|
|
121
|
+
expect(result.servers).toContain('https://www.plantuml.com/plantuml');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('does not duplicate community server if already listed', () => {
|
|
125
|
+
const result = resolvePlantuml({
|
|
126
|
+
servers: ['https://www.plantuml.com/plantuml'],
|
|
127
|
+
});
|
|
128
|
+
expect(
|
|
129
|
+
result.servers.filter((s) => s === 'https://www.plantuml.com/plantuml'),
|
|
130
|
+
).toHaveLength(1);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('preserves configured servers before community', () => {
|
|
134
|
+
const result = resolvePlantuml({
|
|
135
|
+
servers: ['https://private.example.com'],
|
|
136
|
+
});
|
|
137
|
+
expect(result.servers[0]).toBe('https://private.example.com');
|
|
138
|
+
expect(result.servers[1]).toBe('https://www.plantuml.com/plantuml');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('passes through jarPath and javaPath', () => {
|
|
142
|
+
const result = resolvePlantuml({
|
|
143
|
+
jarPath: '/opt/plantuml.jar',
|
|
144
|
+
javaPath: '/usr/bin/java',
|
|
145
|
+
});
|
|
146
|
+
expect(result.jarPath).toBe('/opt/plantuml.jar');
|
|
147
|
+
expect(result.javaPath).toBe('/usr/bin/java');
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe('deriveInternalKey', () => {
|
|
152
|
+
it('returns null when no _internal key exists', () => {
|
|
153
|
+
const keys = [{ name: 'primary', seed: 'abc', scopes: null }];
|
|
154
|
+
expect(deriveInternalKey(keys)).toBe(null);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('derives a key from _internal seed', () => {
|
|
158
|
+
const keys = [{ name: '_internal', seed: 'x'.repeat(64), scopes: null }];
|
|
159
|
+
const result = deriveInternalKey(keys);
|
|
160
|
+
expect(result).toBeTypeOf('string');
|
|
161
|
+
expect(result!.length).toBeGreaterThan(0);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
describe('buildRuntimeConfig', () => {
|
|
166
|
+
it('constructs correct path fields', () => {
|
|
167
|
+
const config = {
|
|
168
|
+
port: 1934,
|
|
169
|
+
eventTimeoutMs: 30000,
|
|
170
|
+
eventLogPurgeMs: 2592000000,
|
|
171
|
+
maxZipSizeMb: 100,
|
|
172
|
+
chromePath: '/usr/bin/chrome',
|
|
173
|
+
events: {},
|
|
174
|
+
auth: { modes: ['keys' as const] },
|
|
175
|
+
keys: { primary: 'a'.repeat(64) },
|
|
176
|
+
insiders: {},
|
|
177
|
+
} as JeevesConfig;
|
|
178
|
+
|
|
179
|
+
const result = buildRuntimeConfig(
|
|
180
|
+
config,
|
|
181
|
+
'/srv/jeeves',
|
|
182
|
+
'/srv/jeeves/config.json',
|
|
183
|
+
);
|
|
184
|
+
expect(result.stateFile).toBe(path.join('/srv/jeeves', 'state.json'));
|
|
185
|
+
expect(result.eventsLog).toBe(
|
|
186
|
+
path.join('/srv/jeeves', 'logs', 'webhook-events.jsonl'),
|
|
187
|
+
);
|
|
188
|
+
expect(result.configPath).toBe('/srv/jeeves/config.json');
|
|
189
|
+
expect(result.port).toBe(1934);
|
|
190
|
+
expect(result.authModes).toEqual(['keys']);
|
|
191
|
+
});
|
|
192
|
+
});
|