@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,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cookie-based session management for Google OAuth insiders.
|
|
3
|
+
*
|
|
4
|
+
* Session cookie = base64(payload) + "." + HMAC(sessionSecret, base64part)
|
|
5
|
+
* No server-side session store needed — config has all insider state.
|
|
6
|
+
*/
|
|
7
|
+
import crypto from 'node:crypto';
|
|
8
|
+
const COOKIE_NAME = 'jeeves_session';
|
|
9
|
+
const SESSION_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000; // 30 days
|
|
10
|
+
/**
|
|
11
|
+
* Create a signed session cookie value.
|
|
12
|
+
*/
|
|
13
|
+
export function createSessionCookie(email, sessionSecret, picture) {
|
|
14
|
+
const payload = {
|
|
15
|
+
email,
|
|
16
|
+
...(picture ? { picture } : {}),
|
|
17
|
+
exp: Date.now() + SESSION_MAX_AGE_MS,
|
|
18
|
+
};
|
|
19
|
+
const b64 = Buffer.from(JSON.stringify(payload)).toString('base64url');
|
|
20
|
+
const sig = crypto
|
|
21
|
+
.createHmac('sha256', sessionSecret)
|
|
22
|
+
.update(b64)
|
|
23
|
+
.digest('hex');
|
|
24
|
+
return `${b64}.${sig}`;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Verify and decode a session cookie. Returns null if invalid/expired.
|
|
28
|
+
*/
|
|
29
|
+
export function verifySessionCookie(cookieValue, sessionSecret) {
|
|
30
|
+
const dotIdx = cookieValue.lastIndexOf('.');
|
|
31
|
+
if (dotIdx < 0)
|
|
32
|
+
return null;
|
|
33
|
+
const b64 = cookieValue.slice(0, dotIdx);
|
|
34
|
+
const sig = cookieValue.slice(dotIdx + 1);
|
|
35
|
+
const expectedSig = crypto
|
|
36
|
+
.createHmac('sha256', sessionSecret)
|
|
37
|
+
.update(b64)
|
|
38
|
+
.digest('hex');
|
|
39
|
+
try {
|
|
40
|
+
if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expectedSig))) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
const payload = JSON.parse(Buffer.from(b64, 'base64url').toString());
|
|
49
|
+
if (payload.exp < Date.now())
|
|
50
|
+
return null;
|
|
51
|
+
return payload;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export { COOKIE_NAME };
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @packageDocumentation
|
|
3
|
+
*
|
|
4
|
+
* CLI commands: config validate, config show.
|
|
5
|
+
*/
|
|
6
|
+
import { loadConfig } from '../../config/index.js';
|
|
7
|
+
function formatScopes(scopes) {
|
|
8
|
+
return scopes
|
|
9
|
+
? `scoped (allow: ${String(scopes.allow.length)}, deny: ${String(scopes.deny.length)})`
|
|
10
|
+
: 'unscoped';
|
|
11
|
+
}
|
|
12
|
+
export function registerConfigCommand(cli) {
|
|
13
|
+
const config = cli.command('config').description('Configuration management');
|
|
14
|
+
config
|
|
15
|
+
.command('validate')
|
|
16
|
+
.description('Validate the configuration file')
|
|
17
|
+
.option('-c, --config <path>', 'Path to configuration file')
|
|
18
|
+
.action(async (options) => {
|
|
19
|
+
try {
|
|
20
|
+
const cfg = await loadConfig(options.config);
|
|
21
|
+
console.log('\u2713 Configuration valid');
|
|
22
|
+
console.log(` Port: ${String(cfg.port)}`);
|
|
23
|
+
console.log(` Auth modes: ${cfg.authModes.join(', ')}`);
|
|
24
|
+
console.log(` Keys: ${String(cfg.resolvedKeys.length)}`);
|
|
25
|
+
console.log(` Insiders: ${String(cfg.resolvedInsiders.length)}`);
|
|
26
|
+
console.log(` Events: ${String(Object.keys(cfg.events).length)} schemas`);
|
|
27
|
+
if (cfg.watcherUrl)
|
|
28
|
+
console.log(` Watcher: ${cfg.watcherUrl}`);
|
|
29
|
+
if (cfg.runnerUrl)
|
|
30
|
+
console.log(` Runner: ${cfg.runnerUrl}`);
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
console.error('\u2717 Configuration invalid');
|
|
34
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
config
|
|
39
|
+
.command('show')
|
|
40
|
+
.description('Display resolved configuration with provenance')
|
|
41
|
+
.option('-c, --config <path>', 'Path to configuration file')
|
|
42
|
+
.action(async (options) => {
|
|
43
|
+
try {
|
|
44
|
+
const cfg = await loadConfig(options.config);
|
|
45
|
+
console.log(`Config file: ${cfg.configPath}`);
|
|
46
|
+
console.log('');
|
|
47
|
+
console.log('Server:');
|
|
48
|
+
console.log(` port: ${String(cfg.port)}`);
|
|
49
|
+
console.log(` chromePath: ${cfg.chromePath}`);
|
|
50
|
+
if (cfg.roots) {
|
|
51
|
+
console.log(` roots: ${JSON.stringify(cfg.roots)}`);
|
|
52
|
+
}
|
|
53
|
+
console.log('');
|
|
54
|
+
console.log('Auth:');
|
|
55
|
+
console.log(` modes: ${cfg.authModes.join(', ')}`);
|
|
56
|
+
console.log(` googleAuth: ${cfg.googleAuth ? 'configured' : 'none'}`);
|
|
57
|
+
console.log(` sessionSecret: ${cfg.sessionSecret ? '***' : 'none'}`);
|
|
58
|
+
console.log('');
|
|
59
|
+
console.log('Keys:');
|
|
60
|
+
for (const key of cfg.resolvedKeys) {
|
|
61
|
+
console.log(` ${key.name}: ${key.seed.slice(0, 8)}... (${formatScopes(key.scopes)})`);
|
|
62
|
+
}
|
|
63
|
+
console.log('');
|
|
64
|
+
console.log('Insiders:');
|
|
65
|
+
for (const insider of cfg.resolvedInsiders) {
|
|
66
|
+
console.log(` ${insider.email}: ${formatScopes(insider.scopes)}`);
|
|
67
|
+
}
|
|
68
|
+
console.log('');
|
|
69
|
+
console.log('Integrations:');
|
|
70
|
+
console.log(` watcherUrl: ${cfg.watcherUrl ?? 'not configured'}`);
|
|
71
|
+
console.log(` runnerUrl: ${cfg.runnerUrl ?? 'not configured'}`);
|
|
72
|
+
console.log('');
|
|
73
|
+
console.log('Events:');
|
|
74
|
+
const eventNames = Object.keys(cfg.events);
|
|
75
|
+
if (eventNames.length === 0) {
|
|
76
|
+
console.log(' (none)');
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
for (const name of eventNames) {
|
|
80
|
+
console.log(` ${name}: ${cfg.events[name].cmd}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
console.log('');
|
|
84
|
+
console.log('Diagrams:');
|
|
85
|
+
console.log(` mermaidCliPath: ${cfg.mermaidCliPath ?? 'not configured'}`);
|
|
86
|
+
console.log(` plantuml.jarPath: ${cfg.plantuml.jarPath ?? 'not configured'}`);
|
|
87
|
+
console.log(` plantuml.servers: ${cfg.plantuml.servers.join(', ')}`);
|
|
88
|
+
console.log('');
|
|
89
|
+
console.log('Paths:');
|
|
90
|
+
console.log(` stateFile: ${cfg.stateFile}`);
|
|
91
|
+
console.log(` eventsLog: ${cfg.eventsLog}`);
|
|
92
|
+
console.log(` diagramCachePath: ${cfg.diagramCachePath ?? '(default)'}`);
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
console.error('Failed to load config');
|
|
96
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { promisify } from 'node:util';
|
|
6
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
7
|
+
const execFileAsync = promisify(execFile);
|
|
8
|
+
const CLI_PATH = path.resolve(import.meta.dirname, '../../../dist/src/cli/index.js');
|
|
9
|
+
const VALID_CONFIG = {
|
|
10
|
+
port: 8765,
|
|
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
|
+
function writeConfig(dir, config) {
|
|
20
|
+
const filePath = path.join(dir, 'jeeves-server.config.json');
|
|
21
|
+
fs.writeFileSync(filePath, JSON.stringify(config));
|
|
22
|
+
return filePath;
|
|
23
|
+
}
|
|
24
|
+
async function runCli(args) {
|
|
25
|
+
return execFileAsync('node', [CLI_PATH, ...args], { timeout: 10_000 });
|
|
26
|
+
}
|
|
27
|
+
describe('jeeves-server config validate', () => {
|
|
28
|
+
let tmpDir;
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'jeeves-cli-'));
|
|
31
|
+
});
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
34
|
+
});
|
|
35
|
+
it('validates a valid config and prints summary', async () => {
|
|
36
|
+
const configPath = writeConfig(tmpDir, VALID_CONFIG);
|
|
37
|
+
const { stdout } = await runCli(['config', 'validate', '-c', configPath]);
|
|
38
|
+
expect(stdout).toContain('Configuration valid');
|
|
39
|
+
expect(stdout).toContain('Port: 8765');
|
|
40
|
+
expect(stdout).toContain('Auth modes: keys');
|
|
41
|
+
expect(stdout).toContain('Keys: 2');
|
|
42
|
+
});
|
|
43
|
+
it('exits with error for invalid config', async () => {
|
|
44
|
+
const configPath = writeConfig(tmpDir, { port: 1234 });
|
|
45
|
+
await expect(runCli(['config', 'validate', '-c', configPath])).rejects.toMatchObject({
|
|
46
|
+
code: 1,
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
describe('jeeves-server config show', () => {
|
|
51
|
+
let tmpDir;
|
|
52
|
+
beforeEach(() => {
|
|
53
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'jeeves-cli-'));
|
|
54
|
+
});
|
|
55
|
+
afterEach(() => {
|
|
56
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
57
|
+
});
|
|
58
|
+
it('shows resolved config with key and insider details', async () => {
|
|
59
|
+
const configPath = writeConfig(tmpDir, {
|
|
60
|
+
...VALID_CONFIG,
|
|
61
|
+
insiders: { 'test@example.com': {} },
|
|
62
|
+
watcherUrl: 'http://localhost:3458',
|
|
63
|
+
});
|
|
64
|
+
const { stdout } = await runCli(['config', 'show', '-c', configPath]);
|
|
65
|
+
expect(stdout).toContain('Config file:');
|
|
66
|
+
expect(stdout).toContain('port: 8765');
|
|
67
|
+
expect(stdout).toContain('modes: keys');
|
|
68
|
+
expect(stdout).toContain('primary:');
|
|
69
|
+
expect(stdout).toContain('unscoped');
|
|
70
|
+
expect(stdout).toContain('test@example.com');
|
|
71
|
+
expect(stdout).toContain('watcherUrl: http://localhost:3458');
|
|
72
|
+
});
|
|
73
|
+
it('shows scoped keys correctly', async () => {
|
|
74
|
+
const configPath = writeConfig(tmpDir, {
|
|
75
|
+
...VALID_CONFIG,
|
|
76
|
+
keys: {
|
|
77
|
+
...VALID_CONFIG.keys,
|
|
78
|
+
scoped: { key: 'c'.repeat(64), scopes: ['/docs'] },
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
const { stdout } = await runCli(['config', 'show', '-c', configPath]);
|
|
82
|
+
expect(stdout).toContain('scoped (allow: 1, deny: 0)');
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @packageDocumentation
|
|
3
|
+
*
|
|
4
|
+
* CLI commands: service install, service uninstall, service start/stop/restart.
|
|
5
|
+
*/
|
|
6
|
+
import { Command } from '@commander-js/extra-typings';
|
|
7
|
+
const DEFAULT_SERVICE_NAME = 'JeevesServer';
|
|
8
|
+
const LINUX_SERVICE_NAME = 'jeeves-server';
|
|
9
|
+
export function registerServiceCommand(cli) {
|
|
10
|
+
const service = cli
|
|
11
|
+
.command('service')
|
|
12
|
+
.description('System service management');
|
|
13
|
+
service.addCommand(new Command('install')
|
|
14
|
+
.description('Print install instructions for a system service')
|
|
15
|
+
.option('-c, --config <path>', 'Path to configuration file')
|
|
16
|
+
.option('-n, --name <name>', 'Service name', process.platform === 'win32'
|
|
17
|
+
? DEFAULT_SERVICE_NAME
|
|
18
|
+
: LINUX_SERVICE_NAME)
|
|
19
|
+
.action((options) => {
|
|
20
|
+
const name = options.name;
|
|
21
|
+
const configArg = options.config ? ` --config "${options.config}"` : '';
|
|
22
|
+
if (process.platform === 'win32') {
|
|
23
|
+
console.log('# NSSM install commands:');
|
|
24
|
+
console.log(`nssm install ${name} node "%CD%\\node_modules\\@karmaniverous\\jeeves-server\\dist\\src\\cli\\index.js" start${configArg}`);
|
|
25
|
+
console.log(`nssm set ${name} AppDirectory "%CD%"`);
|
|
26
|
+
console.log(`nssm set ${name} AppStdout "%CD%\\logs\\service.log"`);
|
|
27
|
+
console.log(`nssm set ${name} AppStderr "%CD%\\logs\\service-error.log"`);
|
|
28
|
+
console.log(`nssm set ${name} Start SERVICE_AUTO_START`);
|
|
29
|
+
console.log(`nssm start ${name}`);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const unit = [
|
|
33
|
+
'[Unit]',
|
|
34
|
+
'Description=Jeeves Server',
|
|
35
|
+
'After=network.target',
|
|
36
|
+
'',
|
|
37
|
+
'[Service]',
|
|
38
|
+
'Type=simple',
|
|
39
|
+
'WorkingDirectory=%h',
|
|
40
|
+
`ExecStart=/usr/bin/env jeeves-server start${configArg}`,
|
|
41
|
+
'Restart=on-failure',
|
|
42
|
+
'',
|
|
43
|
+
'[Install]',
|
|
44
|
+
'WantedBy=default.target',
|
|
45
|
+
].join('\n');
|
|
46
|
+
console.log('# systemd unit file');
|
|
47
|
+
console.log(`# Save to: ~/.config/systemd/user/${name}.service`);
|
|
48
|
+
console.log('');
|
|
49
|
+
console.log(unit);
|
|
50
|
+
console.log('');
|
|
51
|
+
console.log('# Then run:');
|
|
52
|
+
console.log('systemctl --user daemon-reload');
|
|
53
|
+
console.log(`systemctl --user enable --now ${name}.service`);
|
|
54
|
+
}));
|
|
55
|
+
service.addCommand(new Command('uninstall')
|
|
56
|
+
.description('Print uninstall instructions for a system service')
|
|
57
|
+
.option('-n, --name <name>', 'Service name', process.platform === 'win32'
|
|
58
|
+
? DEFAULT_SERVICE_NAME
|
|
59
|
+
: LINUX_SERVICE_NAME)
|
|
60
|
+
.action((options) => {
|
|
61
|
+
const name = options.name;
|
|
62
|
+
if (process.platform === 'win32') {
|
|
63
|
+
console.log('# NSSM uninstall commands:');
|
|
64
|
+
console.log(`nssm stop ${name}`);
|
|
65
|
+
console.log(`nssm remove ${name} confirm`);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
console.log('# systemd uninstall:');
|
|
69
|
+
console.log(`systemctl --user disable --now ${name}.service`);
|
|
70
|
+
console.log(`rm ~/.config/systemd/user/${name}.service`);
|
|
71
|
+
console.log('systemctl --user daemon-reload');
|
|
72
|
+
}));
|
|
73
|
+
for (const action of ['start', 'stop', 'restart']) {
|
|
74
|
+
service.addCommand(new Command(action)
|
|
75
|
+
.description(`${action.charAt(0).toUpperCase() + action.slice(1)} the system service`)
|
|
76
|
+
.option('-n, --name <name>', 'Service name', process.platform === 'win32'
|
|
77
|
+
? DEFAULT_SERVICE_NAME
|
|
78
|
+
: LINUX_SERVICE_NAME)
|
|
79
|
+
.action((options) => {
|
|
80
|
+
const name = options.name;
|
|
81
|
+
if (process.platform === 'win32') {
|
|
82
|
+
if (action === 'restart') {
|
|
83
|
+
console.log(`nssm restart ${name}`);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
console.log(`nssm ${action} ${name}`);
|
|
87
|
+
}
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
console.log(`systemctl --user ${action} ${name}.service`);
|
|
91
|
+
}));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @packageDocumentation
|
|
3
|
+
*
|
|
4
|
+
* CLI command: start — launches the Fastify server.
|
|
5
|
+
*/
|
|
6
|
+
export function registerStartCommand(cli) {
|
|
7
|
+
cli
|
|
8
|
+
.command('start')
|
|
9
|
+
.description('Start the jeeves-server')
|
|
10
|
+
.option('-c, --config <path>', 'Path to configuration file')
|
|
11
|
+
.action(async (options) => {
|
|
12
|
+
try {
|
|
13
|
+
// Dynamic import to avoid loading server code at CLI parse time
|
|
14
|
+
const { initConfig } = await import('../../config/index.js');
|
|
15
|
+
await initConfig(options.config);
|
|
16
|
+
// Import server after config is initialized
|
|
17
|
+
await import('../../server.js');
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
console.error('Failed to start:', error);
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
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
|
+
import { Command } from '@commander-js/extra-typings';
|
|
9
|
+
import { packageVersion } from '../util/packageVersion.js';
|
|
10
|
+
import { registerConfigCommand } from './commands/config.js';
|
|
11
|
+
import { registerServiceCommand } from './commands/service.js';
|
|
12
|
+
import { registerStartCommand } from './commands/start.js';
|
|
13
|
+
const cli = new Command()
|
|
14
|
+
.name('jeeves-server')
|
|
15
|
+
.description('Self-hosted file browser, document server, and webhook gateway')
|
|
16
|
+
.version(packageVersion);
|
|
17
|
+
registerStartCommand(cli);
|
|
18
|
+
registerConfigCommand(cli);
|
|
19
|
+
registerServiceCommand(cli);
|
|
20
|
+
cli.parse();
|
|
@@ -0,0 +1,90 @@
|
|
|
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
|
+
import path from 'node:path';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
import { cosmiconfig } from 'cosmiconfig';
|
|
11
|
+
import { buildRuntimeConfig } from './resolve.js';
|
|
12
|
+
import { jeevesConfigSchema } from './schema.js';
|
|
13
|
+
import { substituteEnvVars } from './substituteEnvVars.js';
|
|
14
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
const rootDir = path.resolve(__dirname, '../../..');
|
|
16
|
+
const MODULE_NAME = 'jeeves-server';
|
|
17
|
+
/**
|
|
18
|
+
* Load and validate jeeves-server configuration via cosmiconfig.
|
|
19
|
+
*
|
|
20
|
+
* Searches for `jeeves-server.config.{json,yaml,yml,js,ts,cjs,mjs}`
|
|
21
|
+
* or `.jeeves-serverrc` in the package root and parent directories.
|
|
22
|
+
*
|
|
23
|
+
* @param configPath - Optional explicit path to a config file.
|
|
24
|
+
* @returns Resolved runtime configuration.
|
|
25
|
+
*/
|
|
26
|
+
export async function loadConfig(configPath) {
|
|
27
|
+
const explorer = cosmiconfig(MODULE_NAME, {
|
|
28
|
+
searchPlaces: [
|
|
29
|
+
'package.json',
|
|
30
|
+
`.${MODULE_NAME}rc`,
|
|
31
|
+
`.${MODULE_NAME}rc.json`,
|
|
32
|
+
`.${MODULE_NAME}rc.yaml`,
|
|
33
|
+
`.${MODULE_NAME}rc.yml`,
|
|
34
|
+
`${MODULE_NAME}.config.json`,
|
|
35
|
+
`${MODULE_NAME}.config.yaml`,
|
|
36
|
+
`${MODULE_NAME}.config.yml`,
|
|
37
|
+
`${MODULE_NAME}.config.js`,
|
|
38
|
+
`${MODULE_NAME}.config.ts`,
|
|
39
|
+
`${MODULE_NAME}.config.mjs`,
|
|
40
|
+
`${MODULE_NAME}.config.cjs`,
|
|
41
|
+
],
|
|
42
|
+
});
|
|
43
|
+
const result = configPath
|
|
44
|
+
? await explorer.load(configPath)
|
|
45
|
+
: await explorer.search(rootDir);
|
|
46
|
+
if (!result || result.isEmpty) {
|
|
47
|
+
throw new Error(`No jeeves-server configuration found. Create a jeeves-server.config.json (or .yaml) file.\n` +
|
|
48
|
+
`Searched from: ${rootDir}`);
|
|
49
|
+
}
|
|
50
|
+
const substituted = substituteEnvVars(result.config);
|
|
51
|
+
const parseResult = jeevesConfigSchema.safeParse(substituted);
|
|
52
|
+
if (!parseResult.success) {
|
|
53
|
+
const issues = parseResult.error.issues
|
|
54
|
+
.map((i) => ` - ${i.path.join('.')}: ${i.message}`)
|
|
55
|
+
.join('\n');
|
|
56
|
+
throw new Error(`Invalid configuration in ${result.filepath}:\n${issues}`);
|
|
57
|
+
}
|
|
58
|
+
return buildRuntimeConfig(parseResult.data, rootDir, result.filepath);
|
|
59
|
+
}
|
|
60
|
+
let configInstance = null;
|
|
61
|
+
/**
|
|
62
|
+
* Check if the config singleton has been initialized.
|
|
63
|
+
*/
|
|
64
|
+
export function isConfigInitialized() {
|
|
65
|
+
return configInstance !== null;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Get the singleton config instance. Initializes on first call.
|
|
69
|
+
* @throws If config has not been initialized — call initConfig() first.
|
|
70
|
+
*/
|
|
71
|
+
export function getConfig() {
|
|
72
|
+
if (!configInstance) {
|
|
73
|
+
throw new Error('Config not initialized. Call initConfig() before getConfig().');
|
|
74
|
+
}
|
|
75
|
+
return configInstance;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Initialize the config singleton. Must be called once at startup.
|
|
79
|
+
* @param configPath - Optional explicit path to a config file.
|
|
80
|
+
*/
|
|
81
|
+
export async function initConfig(configPath) {
|
|
82
|
+
configInstance = await loadConfig(configPath);
|
|
83
|
+
return configInstance;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Reset the config singleton (for testing).
|
|
87
|
+
*/
|
|
88
|
+
export function resetConfig() {
|
|
89
|
+
configInstance = null;
|
|
90
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
5
|
+
import { getConfig, initConfig, loadConfig, resetConfig } from './index.js';
|
|
6
|
+
const VALID_CONFIG = {
|
|
7
|
+
port: 9999,
|
|
8
|
+
chromePath: '/usr/bin/chromium',
|
|
9
|
+
auth: { modes: ['keys'] },
|
|
10
|
+
keys: {
|
|
11
|
+
primary: 'a'.repeat(64),
|
|
12
|
+
_internal: 'b'.repeat(64),
|
|
13
|
+
},
|
|
14
|
+
events: {},
|
|
15
|
+
};
|
|
16
|
+
function writeConfig(dir, config) {
|
|
17
|
+
const filePath = path.join(dir, 'jeeves-server.config.json');
|
|
18
|
+
fs.writeFileSync(filePath, JSON.stringify(config));
|
|
19
|
+
return filePath;
|
|
20
|
+
}
|
|
21
|
+
describe('loadConfig', () => {
|
|
22
|
+
let tmpDir;
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'jeeves-config-'));
|
|
25
|
+
resetConfig();
|
|
26
|
+
});
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
29
|
+
resetConfig();
|
|
30
|
+
});
|
|
31
|
+
it('loads a valid JSON config file', async () => {
|
|
32
|
+
const configPath = writeConfig(tmpDir, VALID_CONFIG);
|
|
33
|
+
const config = await loadConfig(configPath);
|
|
34
|
+
expect(config.port).toBe(9999);
|
|
35
|
+
expect(config.chromePath).toBe('/usr/bin/chromium');
|
|
36
|
+
expect(config.configPath).toBe(configPath);
|
|
37
|
+
});
|
|
38
|
+
it('throws on missing config', async () => {
|
|
39
|
+
await expect(loadConfig(path.join(tmpDir, 'nonexistent.json'))).rejects.toThrow();
|
|
40
|
+
});
|
|
41
|
+
it('throws on invalid config (missing auth)', async () => {
|
|
42
|
+
const configPath = writeConfig(tmpDir, { port: 1234 });
|
|
43
|
+
await expect(loadConfig(configPath)).rejects.toThrow('Invalid configuration');
|
|
44
|
+
});
|
|
45
|
+
it('applies env var substitution', async () => {
|
|
46
|
+
const original = process.env['TEST_CHROME_PATH'];
|
|
47
|
+
process.env['TEST_CHROME_PATH'] = '/custom/chrome';
|
|
48
|
+
try {
|
|
49
|
+
const configPath = writeConfig(tmpDir, {
|
|
50
|
+
...VALID_CONFIG,
|
|
51
|
+
chromePath: '${TEST_CHROME_PATH}',
|
|
52
|
+
});
|
|
53
|
+
const config = await loadConfig(configPath);
|
|
54
|
+
expect(config.chromePath).toBe('/custom/chrome');
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
if (original === undefined)
|
|
58
|
+
delete process.env['TEST_CHROME_PATH'];
|
|
59
|
+
else
|
|
60
|
+
process.env['TEST_CHROME_PATH'] = original;
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
it('applies default port when omitted', async () => {
|
|
64
|
+
const noPort = { ...VALID_CONFIG };
|
|
65
|
+
delete noPort.port;
|
|
66
|
+
const configPath = writeConfig(tmpDir, noPort);
|
|
67
|
+
const config = await loadConfig(configPath);
|
|
68
|
+
expect(config.port).toBe(1934);
|
|
69
|
+
});
|
|
70
|
+
it('rejects _plugin key with scopes', async () => {
|
|
71
|
+
const configPath = writeConfig(tmpDir, {
|
|
72
|
+
...VALID_CONFIG,
|
|
73
|
+
keys: {
|
|
74
|
+
...VALID_CONFIG.keys,
|
|
75
|
+
_plugin: { key: 'c'.repeat(64), scopes: ['/restricted'] },
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
await expect(loadConfig(configPath)).rejects.toThrow('_plugin key must not have scopes');
|
|
79
|
+
});
|
|
80
|
+
it('rejects _internal key with scopes', async () => {
|
|
81
|
+
const configPath = writeConfig(tmpDir, {
|
|
82
|
+
...VALID_CONFIG,
|
|
83
|
+
keys: {
|
|
84
|
+
...VALID_CONFIG.keys,
|
|
85
|
+
_internal: { key: 'b'.repeat(64), scopes: ['/restricted'] },
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
await expect(loadConfig(configPath)).rejects.toThrow('_internal key must not have scopes');
|
|
89
|
+
});
|
|
90
|
+
it('accepts _plugin key without scopes', async () => {
|
|
91
|
+
const configPath = writeConfig(tmpDir, {
|
|
92
|
+
...VALID_CONFIG,
|
|
93
|
+
keys: {
|
|
94
|
+
...VALID_CONFIG.keys,
|
|
95
|
+
_plugin: 'c'.repeat(64),
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
const config = await loadConfig(configPath);
|
|
99
|
+
expect(config.resolvedKeys.find((k) => k.name === '_plugin')?.seed).toBe('c'.repeat(64));
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
describe('config singleton', () => {
|
|
103
|
+
let tmpDir;
|
|
104
|
+
beforeEach(() => {
|
|
105
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'jeeves-config-'));
|
|
106
|
+
resetConfig();
|
|
107
|
+
});
|
|
108
|
+
afterEach(() => {
|
|
109
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
110
|
+
resetConfig();
|
|
111
|
+
});
|
|
112
|
+
it('throws if getConfig called before initConfig', () => {
|
|
113
|
+
expect(() => getConfig()).toThrow('Config not initialized');
|
|
114
|
+
});
|
|
115
|
+
it('initConfig populates getConfig', async () => {
|
|
116
|
+
const configPath = writeConfig(tmpDir, VALID_CONFIG);
|
|
117
|
+
await initConfig(configPath);
|
|
118
|
+
const config = getConfig();
|
|
119
|
+
expect(config.port).toBe(9999);
|
|
120
|
+
});
|
|
121
|
+
it('resetConfig clears the singleton', async () => {
|
|
122
|
+
const configPath = writeConfig(tmpDir, VALID_CONFIG);
|
|
123
|
+
await initConfig(configPath);
|
|
124
|
+
resetConfig();
|
|
125
|
+
expect(() => getConfig()).toThrow('Config not initialized');
|
|
126
|
+
});
|
|
127
|
+
});
|