@karmaniverous/jeeves-server 3.2.1 → 3.3.1
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/.tsbuildinfo +1 -1
- package/CHANGELOG.md +19 -1
- package/dist/src/cli/commands/config.js +5 -0
- package/dist/src/config/resolve.js +2 -0
- package/dist/src/config/resolve.test.js +2 -0
- package/dist/src/config/schema.js +11 -0
- package/dist/src/routes/api/status.js +3 -1
- package/dist/src/routes/api/status.test.js +2 -0
- package/dist/src/routes/config.js +40 -0
- package/dist/src/routes/config.test.js +108 -0
- package/dist/src/server.js +4 -2
- package/package.json +2 -1
- package/src/cli/commands/config.ts +4 -0
- package/src/config/resolve.test.ts +2 -0
- package/src/config/resolve.ts +2 -0
- package/src/config/schema.ts +11 -0
- package/src/config/types.ts +2 -0
- package/src/routes/api/status.test.ts +2 -0
- package/src/routes/api/status.ts +3 -1
- package/src/routes/config.test.ts +133 -0
- package/src/routes/config.ts +49 -0
- package/src/server.ts +6 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,9 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. Dates are displayed in UTC.
|
|
4
4
|
|
|
5
|
-
#### [3.
|
|
5
|
+
#### [3.3.1](https://github.com/karmaniverous/jeeves-server/compare/service/3.3.0...3.3.1)
|
|
6
|
+
|
|
7
|
+
- Add configurable host bind + metaUrl service probe [`#114`](https://github.com/karmaniverous/jeeves-server/pull/114)
|
|
8
|
+
- chore: release @karmaniverous/jeeves-server-openclaw v0.4.0 [`566e438`](https://github.com/karmaniverous/jeeves-server/commit/566e438a5262e5f2e80db4690c9907b6658e5519)
|
|
9
|
+
- [113] feat: add host bind and metaUrl config options [`798d133`](https://github.com/karmaniverous/jeeves-server/commit/798d133825f99f5a5fc19bb0ba9093b757f62f0f)
|
|
10
|
+
- npm audit fix [`a07adac`](https://github.com/karmaniverous/jeeves-server/commit/a07adac906ca032bedeef25b16b9e0c05231b4b0)
|
|
11
|
+
|
|
12
|
+
#### [service/3.3.0](https://github.com/karmaniverous/jeeves-server/compare/service/3.2.1...service/3.3.0)
|
|
13
|
+
|
|
14
|
+
> 21 March 2026
|
|
15
|
+
|
|
16
|
+
- feat: core v0.2.0 SDK adoption [`#111`](https://github.com/karmaniverous/jeeves-server/pull/111)
|
|
17
|
+
- chore: release @karmaniverous/jeeves-server-openclaw v0.3.1 [`11309c1`](https://github.com/karmaniverous/jeeves-server/commit/11309c1c3d82f6950b3d0291546206b523df83fa)
|
|
18
|
+
- chore: release @karmaniverous/jeeves-server v3.3.0 [`1b1aec6`](https://github.com/karmaniverous/jeeves-server/commit/1b1aec644106ce05576e608d278a37860a47de61)
|
|
19
|
+
|
|
20
|
+
#### [service/3.2.1](https://github.com/karmaniverous/jeeves-server/compare/service/3.2.0...service/3.2.1)
|
|
21
|
+
|
|
22
|
+
> 19 March 2026
|
|
6
23
|
|
|
7
24
|
- fix(openclaw): bundle @karmaniverous/jeeves into plugin dist [`61fb7ac`](https://github.com/karmaniverous/jeeves-server/commit/61fb7ac3f91a036cef16720c16c33fe399f7c4e6)
|
|
25
|
+
- chore: release @karmaniverous/jeeves-server v3.2.1 [`f13677b`](https://github.com/karmaniverous/jeeves-server/commit/f13677bd1cd020416ba510eaabc58ad85ee9aabf)
|
|
8
26
|
- chore(openclaw): use resolveWorkspacePath from jeeves 0.1.4 [`71ddd58`](https://github.com/karmaniverous/jeeves-server/commit/71ddd588f1b5736f0cbd4d81d42bb0f9356eafe8)
|
|
9
27
|
- chore(openclaw): update jeeves to 0.1.6, add servicePackage/pluginPackage [`1847ff7`](https://github.com/karmaniverous/jeeves-server/commit/1847ff750613b3770198b20b4bacd7e93bf0ec52)
|
|
10
28
|
- chore(openclaw): update @karmaniverous/jeeves to 0.1.5 [`c8b040b`](https://github.com/karmaniverous/jeeves-server/commit/c8b040bfa945b3814cbde9395cf1d3f00cdd64a8)
|
|
@@ -20,6 +20,7 @@ export function registerConfigCommand(cli) {
|
|
|
20
20
|
const cfg = await loadConfig(options.config);
|
|
21
21
|
console.log('\u2713 Configuration valid');
|
|
22
22
|
console.log(` Port: ${String(cfg.port)}`);
|
|
23
|
+
console.log(` Host: ${cfg.host}`);
|
|
23
24
|
console.log(` Auth modes: ${cfg.authModes.join(', ')}`);
|
|
24
25
|
console.log(` Keys: ${String(cfg.resolvedKeys.length)}`);
|
|
25
26
|
console.log(` Insiders: ${String(cfg.resolvedInsiders.length)}`);
|
|
@@ -28,6 +29,8 @@ export function registerConfigCommand(cli) {
|
|
|
28
29
|
console.log(` Watcher: ${cfg.watcherUrl}`);
|
|
29
30
|
if (cfg.runnerUrl)
|
|
30
31
|
console.log(` Runner: ${cfg.runnerUrl}`);
|
|
32
|
+
if (cfg.metaUrl)
|
|
33
|
+
console.log(` Meta: ${cfg.metaUrl}`);
|
|
31
34
|
}
|
|
32
35
|
catch (error) {
|
|
33
36
|
console.error('\u2717 Configuration invalid');
|
|
@@ -46,6 +49,7 @@ export function registerConfigCommand(cli) {
|
|
|
46
49
|
console.log('');
|
|
47
50
|
console.log('Server:');
|
|
48
51
|
console.log(` port: ${String(cfg.port)}`);
|
|
52
|
+
console.log(` host: ${cfg.host}`);
|
|
49
53
|
console.log(` chromePath: ${cfg.chromePath}`);
|
|
50
54
|
if (cfg.roots) {
|
|
51
55
|
console.log(` roots: ${JSON.stringify(cfg.roots)}`);
|
|
@@ -69,6 +73,7 @@ export function registerConfigCommand(cli) {
|
|
|
69
73
|
console.log('Integrations:');
|
|
70
74
|
console.log(` watcherUrl: ${cfg.watcherUrl ?? 'not configured'}`);
|
|
71
75
|
console.log(` runnerUrl: ${cfg.runnerUrl ?? 'not configured'}`);
|
|
76
|
+
console.log(` metaUrl: ${cfg.metaUrl ?? 'not configured'}`);
|
|
72
77
|
console.log('');
|
|
73
78
|
console.log('Events:');
|
|
74
79
|
const eventNames = Object.keys(cfg.events);
|
|
@@ -186,6 +186,7 @@ export function buildRuntimeConfig(config, rootDir, configPath) {
|
|
|
186
186
|
const resolvedInsiders = resolveInsiders(config.insiders, config.scopes, stateFile);
|
|
187
187
|
return {
|
|
188
188
|
port: config.port,
|
|
189
|
+
host: config.host,
|
|
189
190
|
eventTimeoutMs: config.eventTimeoutMs,
|
|
190
191
|
eventLogPurgeMs: config.eventLogPurgeMs,
|
|
191
192
|
maxZipSizeMb: config.maxZipSizeMb,
|
|
@@ -216,6 +217,7 @@ export function buildRuntimeConfig(config, rootDir, configPath) {
|
|
|
216
217
|
internalInsiderKey: deriveInternalKey(resolvedKeys),
|
|
217
218
|
runnerUrl: config.runnerUrl,
|
|
218
219
|
watcherUrl: config.watcherUrl,
|
|
220
|
+
metaUrl: config.metaUrl,
|
|
219
221
|
diagramCachePath: config.diagramCachePath,
|
|
220
222
|
configPath,
|
|
221
223
|
eventsLog: path.join(rootDir, 'logs', 'webhook-events.jsonl'),
|
|
@@ -187,6 +187,8 @@ describe('buildRuntimeConfig', () => {
|
|
|
187
187
|
it('constructs correct path fields', () => {
|
|
188
188
|
const config = {
|
|
189
189
|
port: 1934,
|
|
190
|
+
host: '0.0.0.0',
|
|
191
|
+
metaUrl: 'http://127.0.0.1:1938',
|
|
190
192
|
eventTimeoutMs: 30000,
|
|
191
193
|
eventLogPurgeMs: 2592000000,
|
|
192
194
|
maxZipSizeMb: 100,
|
|
@@ -89,6 +89,12 @@ function getScopeRefs(scopes) {
|
|
|
89
89
|
export const jeevesConfigSchema = z
|
|
90
90
|
.object({
|
|
91
91
|
port: z.number().int().positive().default(1934),
|
|
92
|
+
/**
|
|
93
|
+
* Network interface to bind the server to.
|
|
94
|
+
* Default: '0.0.0.0' (all interfaces — required for external access by insiders, share links, etc.)
|
|
95
|
+
* Set to '127.0.0.1' to restrict to loopback only.
|
|
96
|
+
*/
|
|
97
|
+
host: z.string().min(1).default('0.0.0.0'),
|
|
92
98
|
chromePath: z.string().min(1),
|
|
93
99
|
auth: authSchema,
|
|
94
100
|
/** Named scope definitions, referenced by insiders/keys/outsiderPolicy */
|
|
@@ -139,6 +145,11 @@ export const jeevesConfigSchema = z
|
|
|
139
145
|
* When set, the search UI appears in the header. Example: 'http://localhost:3458'
|
|
140
146
|
*/
|
|
141
147
|
watcherUrl: z.url().optional(),
|
|
148
|
+
/**
|
|
149
|
+
* URL of the jeeves-meta API for synthesis engine health checks.
|
|
150
|
+
* Default: 'http://127.0.0.1:1938'
|
|
151
|
+
*/
|
|
152
|
+
metaUrl: z.url().default('http://127.0.0.1:1938'),
|
|
142
153
|
/**
|
|
143
154
|
* Global outsider policy â€" constrains which paths are eligible for outsider sharing.
|
|
144
155
|
* Uses the same allow/deny model as insider scopes.
|
|
@@ -30,9 +30,10 @@ async function checkService(url) {
|
|
|
30
30
|
export const statusRoutes = async (fastify) => {
|
|
31
31
|
fastify.get('/api/status', async (request) => {
|
|
32
32
|
const config = getConfig();
|
|
33
|
-
const [watcher, runner] = await Promise.all([
|
|
33
|
+
const [watcher, runner, meta] = await Promise.all([
|
|
34
34
|
config.watcherUrl ? checkService(config.watcherUrl) : null,
|
|
35
35
|
config.runnerUrl ? checkService(config.runnerUrl) : null,
|
|
36
|
+
config.metaUrl ? checkService(config.metaUrl) : null,
|
|
36
37
|
]);
|
|
37
38
|
return {
|
|
38
39
|
version: packageVersion,
|
|
@@ -67,6 +68,7 @@ export const statusRoutes = async (fastify) => {
|
|
|
67
68
|
services: {
|
|
68
69
|
watcher,
|
|
69
70
|
runner,
|
|
71
|
+
meta,
|
|
70
72
|
},
|
|
71
73
|
...(request.query.events
|
|
72
74
|
? {
|
|
@@ -2,6 +2,7 @@ import { describe, expect, it, vi } from 'vitest';
|
|
|
2
2
|
// Mock config
|
|
3
3
|
const mockConfig = {
|
|
4
4
|
port: 1934,
|
|
5
|
+
host: '0.0.0.0',
|
|
5
6
|
chromePath: '/usr/bin/chromium',
|
|
6
7
|
authModes: ['keys'],
|
|
7
8
|
resolvedInsiders: [{ email: 'a@b.com' }, { email: 'c@d.com' }],
|
|
@@ -17,6 +18,7 @@ const mockConfig = {
|
|
|
17
18
|
},
|
|
18
19
|
watcherUrl: null,
|
|
19
20
|
runnerUrl: null,
|
|
21
|
+
metaUrl: null,
|
|
20
22
|
exportFormats: ['pdf', 'docx', 'zip'],
|
|
21
23
|
};
|
|
22
24
|
vi.mock('../../config/index.js', () => ({
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /config — query service configuration with optional JSONPath.
|
|
3
|
+
*
|
|
4
|
+
* Uses the core SDK's `createConfigQueryHandler()` for JSONPath support.
|
|
5
|
+
*
|
|
6
|
+
* @packageDocumentation
|
|
7
|
+
*/
|
|
8
|
+
import { createConfigQueryHandler } from '@karmaniverous/jeeves';
|
|
9
|
+
import { getConfig } from '../config/index.js';
|
|
10
|
+
/** Return a sanitized copy of the config (redact sensitive fields). */
|
|
11
|
+
export function sanitizeConfig(config) {
|
|
12
|
+
return {
|
|
13
|
+
...config,
|
|
14
|
+
sessionSecret: config.sessionSecret ? '[REDACTED]' : null,
|
|
15
|
+
internalInsiderKey: config.internalInsiderKey ? '[REDACTED]' : null,
|
|
16
|
+
googleAuth: config.googleAuth
|
|
17
|
+
? {
|
|
18
|
+
clientId: config.googleAuth.clientId,
|
|
19
|
+
clientSecret: '[REDACTED]',
|
|
20
|
+
}
|
|
21
|
+
: null,
|
|
22
|
+
resolvedKeys: config.resolvedKeys.map((k) => ({
|
|
23
|
+
...k,
|
|
24
|
+
seed: '[REDACTED]',
|
|
25
|
+
})),
|
|
26
|
+
resolvedInsiders: config.resolvedInsiders.map((i) => ({
|
|
27
|
+
...i,
|
|
28
|
+
seed: '[REDACTED]',
|
|
29
|
+
})),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/** Register the GET /config route. */
|
|
33
|
+
export function registerConfigRoute(app) {
|
|
34
|
+
const configHandler = createConfigQueryHandler(() => sanitizeConfig(getConfig()));
|
|
35
|
+
app.get('/config', async (request, reply) => {
|
|
36
|
+
const { path } = request.query;
|
|
37
|
+
const result = await configHandler({ path });
|
|
38
|
+
return reply.status(result.status).send(result.body);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for GET /config route sanitization logic.
|
|
3
|
+
*/
|
|
4
|
+
import { describe, expect, it } from 'vitest';
|
|
5
|
+
import { sanitizeConfig } from './config.js';
|
|
6
|
+
/** Minimal RuntimeConfig fixture with sensitive fields populated. */
|
|
7
|
+
function makeConfig(overrides = {}) {
|
|
8
|
+
return {
|
|
9
|
+
port: 1934,
|
|
10
|
+
host: '0.0.0.0',
|
|
11
|
+
eventTimeoutMs: 30_000,
|
|
12
|
+
eventLogPurgeMs: 604_800_000,
|
|
13
|
+
maxZipSizeMb: 100,
|
|
14
|
+
chromePath: '/usr/bin/chromium',
|
|
15
|
+
plantuml: { servers: [] },
|
|
16
|
+
outsiderPolicy: null,
|
|
17
|
+
events: {},
|
|
18
|
+
authModes: ['keys'],
|
|
19
|
+
resolvedKeys: [
|
|
20
|
+
{
|
|
21
|
+
name: 'primary',
|
|
22
|
+
seed: 'secret-key-seed-abc',
|
|
23
|
+
scopes: null,
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
resolvedInsiders: [
|
|
27
|
+
{
|
|
28
|
+
email: 'jason@example.com',
|
|
29
|
+
seed: 'secret-insider-seed-xyz',
|
|
30
|
+
scopes: null,
|
|
31
|
+
keyCreatedAt: '2026-01-01T00:00:00Z',
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
googleAuth: {
|
|
35
|
+
clientId: 'public-client-id',
|
|
36
|
+
clientSecret: 'super-secret-oauth-secret',
|
|
37
|
+
},
|
|
38
|
+
sessionSecret: 'session-hmac-secret',
|
|
39
|
+
internalInsiderKey: 'internal-key-seed',
|
|
40
|
+
configPath: '/etc/jeeves-server.config.json',
|
|
41
|
+
eventsLog: '/var/log/events.log',
|
|
42
|
+
stateFile: '/var/state.json',
|
|
43
|
+
eventQueuePath: '/var/queue.jsonl',
|
|
44
|
+
eventQueueCursorPath: '/var/cursor.json',
|
|
45
|
+
eventLogPath: '/var/event-log.jsonl',
|
|
46
|
+
...overrides,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
describe('sanitizeConfig', () => {
|
|
50
|
+
it('redacts sessionSecret', () => {
|
|
51
|
+
const result = sanitizeConfig(makeConfig());
|
|
52
|
+
expect(result.sessionSecret).toBe('[REDACTED]');
|
|
53
|
+
});
|
|
54
|
+
it('returns null for sessionSecret when not configured', () => {
|
|
55
|
+
const result = sanitizeConfig(makeConfig({ sessionSecret: null }));
|
|
56
|
+
expect(result.sessionSecret).toBeNull();
|
|
57
|
+
});
|
|
58
|
+
it('redacts internalInsiderKey', () => {
|
|
59
|
+
const result = sanitizeConfig(makeConfig());
|
|
60
|
+
expect(result.internalInsiderKey).toBe('[REDACTED]');
|
|
61
|
+
});
|
|
62
|
+
it('redacts googleAuth.clientSecret but preserves clientId', () => {
|
|
63
|
+
const result = sanitizeConfig(makeConfig());
|
|
64
|
+
const auth = result.googleAuth;
|
|
65
|
+
expect(auth.clientId).toBe('public-client-id');
|
|
66
|
+
expect(auth.clientSecret).toBe('[REDACTED]');
|
|
67
|
+
});
|
|
68
|
+
it('returns null for googleAuth when not configured', () => {
|
|
69
|
+
const result = sanitizeConfig(makeConfig({ googleAuth: null }));
|
|
70
|
+
expect(result.googleAuth).toBeNull();
|
|
71
|
+
});
|
|
72
|
+
it('redacts all key seeds', () => {
|
|
73
|
+
const config = makeConfig({
|
|
74
|
+
resolvedKeys: [
|
|
75
|
+
{ name: 'a', seed: 'seed-a', scopes: null },
|
|
76
|
+
{ name: 'b', seed: 'seed-b', scopes: null },
|
|
77
|
+
],
|
|
78
|
+
});
|
|
79
|
+
const result = sanitizeConfig(config);
|
|
80
|
+
const keys = result.resolvedKeys;
|
|
81
|
+
expect(keys).toHaveLength(2);
|
|
82
|
+
expect(keys[0].name).toBe('a');
|
|
83
|
+
expect(keys[0].seed).toBe('[REDACTED]');
|
|
84
|
+
expect(keys[1].seed).toBe('[REDACTED]');
|
|
85
|
+
});
|
|
86
|
+
it('redacts all insider seeds but preserves other fields', () => {
|
|
87
|
+
const result = sanitizeConfig(makeConfig());
|
|
88
|
+
const insiders = result.resolvedInsiders;
|
|
89
|
+
expect(insiders[0].email).toBe('jason@example.com');
|
|
90
|
+
expect(insiders[0].seed).toBe('[REDACTED]');
|
|
91
|
+
expect(insiders[0].keyCreatedAt).toBe('2026-01-01T00:00:00Z');
|
|
92
|
+
});
|
|
93
|
+
it('preserves non-sensitive fields', () => {
|
|
94
|
+
const result = sanitizeConfig(makeConfig());
|
|
95
|
+
expect(result.port).toBe(1934);
|
|
96
|
+
expect(result.chromePath).toBe('/usr/bin/chromium');
|
|
97
|
+
expect(result.configPath).toBe('/etc/jeeves-server.config.json');
|
|
98
|
+
});
|
|
99
|
+
it('never leaks raw secret values', () => {
|
|
100
|
+
const config = makeConfig();
|
|
101
|
+
const json = JSON.stringify(sanitizeConfig(config));
|
|
102
|
+
expect(json).not.toContain('secret-key-seed-abc');
|
|
103
|
+
expect(json).not.toContain('secret-insider-seed-xyz');
|
|
104
|
+
expect(json).not.toContain('super-secret-oauth-secret');
|
|
105
|
+
expect(json).not.toContain('session-hmac-secret');
|
|
106
|
+
expect(json).not.toContain('internal-key-seed');
|
|
107
|
+
});
|
|
108
|
+
});
|
package/dist/src/server.js
CHANGED
|
@@ -11,6 +11,7 @@ import Fastify from 'fastify';
|
|
|
11
11
|
import { getConfig, initConfig, isConfigInitialized } from './config/index.js';
|
|
12
12
|
import { apiRoute } from './routes/api/index.js';
|
|
13
13
|
import { authRoute } from './routes/auth.js';
|
|
14
|
+
import { registerConfigRoute } from './routes/config.js';
|
|
14
15
|
import { eventRoute } from './routes/event.js';
|
|
15
16
|
import { healthRoute } from './routes/health.js';
|
|
16
17
|
import { keysRoute } from './routes/keys.js';
|
|
@@ -35,6 +36,7 @@ async function start() {
|
|
|
35
36
|
// Register routes
|
|
36
37
|
await fastify.register(staticRoutes);
|
|
37
38
|
await fastify.register(healthRoute);
|
|
39
|
+
registerConfigRoute(fastify);
|
|
38
40
|
await fastify.register(authRoute);
|
|
39
41
|
await fastify.register(keysRoute);
|
|
40
42
|
await fastify.register(eventRoute);
|
|
@@ -69,8 +71,8 @@ async function start() {
|
|
|
69
71
|
initExportCache();
|
|
70
72
|
// Start queue processor
|
|
71
73
|
startQueueProcessor();
|
|
72
|
-
await fastify.listen({ port: config.port, host:
|
|
73
|
-
console.log(`Jeeves server listening on
|
|
74
|
+
await fastify.listen({ port: config.port, host: config.host });
|
|
75
|
+
console.log(`Jeeves server listening on ${config.host}:${String(config.port)}`);
|
|
74
76
|
console.log(`Endpoints:`);
|
|
75
77
|
console.log(` GET /browse/* - File browser SPA`);
|
|
76
78
|
console.log(` GET /api/raw/* - Raw file serving`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@karmaniverous/jeeves-server",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.1",
|
|
4
4
|
"description": "Secure file browser, markdown viewer, and webhook gateway with PDF/DOCX export and expiring share links",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"fastify",
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
"license": "MIT",
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"@commander-js/extra-typings": "^14.0.0",
|
|
50
|
+
"@karmaniverous/jeeves": "^0.2.0",
|
|
50
51
|
"@fastify/cookie": "^11.0.2",
|
|
51
52
|
"@fastify/static": "^8.3.0",
|
|
52
53
|
"@karmaniverous/jsonmap": "^0.3.1",
|
|
@@ -27,6 +27,7 @@ export function registerConfigCommand(cli: Command): void {
|
|
|
27
27
|
const cfg = await loadConfig(options.config);
|
|
28
28
|
console.log('\u2713 Configuration valid');
|
|
29
29
|
console.log(` Port: ${String(cfg.port)}`);
|
|
30
|
+
console.log(` Host: ${cfg.host}`);
|
|
30
31
|
console.log(` Auth modes: ${cfg.authModes.join(', ')}`);
|
|
31
32
|
console.log(` Keys: ${String(cfg.resolvedKeys.length)}`);
|
|
32
33
|
console.log(` Insiders: ${String(cfg.resolvedInsiders.length)}`);
|
|
@@ -35,6 +36,7 @@ export function registerConfigCommand(cli: Command): void {
|
|
|
35
36
|
);
|
|
36
37
|
if (cfg.watcherUrl) console.log(` Watcher: ${cfg.watcherUrl}`);
|
|
37
38
|
if (cfg.runnerUrl) console.log(` Runner: ${cfg.runnerUrl}`);
|
|
39
|
+
if (cfg.metaUrl) console.log(` Meta: ${cfg.metaUrl}`);
|
|
38
40
|
} catch (error) {
|
|
39
41
|
console.error('\u2717 Configuration invalid');
|
|
40
42
|
console.error(error instanceof Error ? error.message : String(error));
|
|
@@ -53,6 +55,7 @@ export function registerConfigCommand(cli: Command): void {
|
|
|
53
55
|
console.log('');
|
|
54
56
|
console.log('Server:');
|
|
55
57
|
console.log(` port: ${String(cfg.port)}`);
|
|
58
|
+
console.log(` host: ${cfg.host}`);
|
|
56
59
|
console.log(` chromePath: ${cfg.chromePath}`);
|
|
57
60
|
if (cfg.roots) {
|
|
58
61
|
console.log(` roots: ${JSON.stringify(cfg.roots)}`);
|
|
@@ -78,6 +81,7 @@ export function registerConfigCommand(cli: Command): void {
|
|
|
78
81
|
console.log('Integrations:');
|
|
79
82
|
console.log(` watcherUrl: ${cfg.watcherUrl ?? 'not configured'}`);
|
|
80
83
|
console.log(` runnerUrl: ${cfg.runnerUrl ?? 'not configured'}`);
|
|
84
|
+
console.log(` metaUrl: ${cfg.metaUrl ?? 'not configured'}`);
|
|
81
85
|
console.log('');
|
|
82
86
|
console.log('Events:');
|
|
83
87
|
const eventNames = Object.keys(cfg.events);
|
|
@@ -236,6 +236,8 @@ describe('buildRuntimeConfig', () => {
|
|
|
236
236
|
it('constructs correct path fields', () => {
|
|
237
237
|
const config = {
|
|
238
238
|
port: 1934,
|
|
239
|
+
host: '0.0.0.0',
|
|
240
|
+
metaUrl: 'http://127.0.0.1:1938',
|
|
239
241
|
eventTimeoutMs: 30000,
|
|
240
242
|
eventLogPurgeMs: 2592000000,
|
|
241
243
|
maxZipSizeMb: 100,
|
package/src/config/resolve.ts
CHANGED
|
@@ -250,6 +250,7 @@ export function buildRuntimeConfig(
|
|
|
250
250
|
|
|
251
251
|
return {
|
|
252
252
|
port: config.port,
|
|
253
|
+
host: config.host,
|
|
253
254
|
eventTimeoutMs: config.eventTimeoutMs,
|
|
254
255
|
eventLogPurgeMs: config.eventLogPurgeMs,
|
|
255
256
|
maxZipSizeMb: config.maxZipSizeMb,
|
|
@@ -285,6 +286,7 @@ export function buildRuntimeConfig(
|
|
|
285
286
|
internalInsiderKey: deriveInternalKey(resolvedKeys),
|
|
286
287
|
runnerUrl: config.runnerUrl,
|
|
287
288
|
watcherUrl: config.watcherUrl,
|
|
289
|
+
metaUrl: config.metaUrl,
|
|
288
290
|
diagramCachePath: config.diagramCachePath,
|
|
289
291
|
configPath,
|
|
290
292
|
eventsLog: path.join(rootDir, 'logs', 'webhook-events.jsonl'),
|
package/src/config/schema.ts
CHANGED
|
@@ -100,6 +100,12 @@ function getScopeRefs(scopes: unknown): string[] {
|
|
|
100
100
|
export const jeevesConfigSchema = z
|
|
101
101
|
.object({
|
|
102
102
|
port: z.number().int().positive().default(1934),
|
|
103
|
+
/**
|
|
104
|
+
* Network interface to bind the server to.
|
|
105
|
+
* Default: '0.0.0.0' (all interfaces — required for external access by insiders, share links, etc.)
|
|
106
|
+
* Set to '127.0.0.1' to restrict to loopback only.
|
|
107
|
+
*/
|
|
108
|
+
host: z.string().min(1).default('0.0.0.0'),
|
|
103
109
|
chromePath: z.string().min(1),
|
|
104
110
|
auth: authSchema,
|
|
105
111
|
/** Named scope definitions, referenced by insiders/keys/outsiderPolicy */
|
|
@@ -150,6 +156,11 @@ export const jeevesConfigSchema = z
|
|
|
150
156
|
* When set, the search UI appears in the header. Example: 'http://localhost:3458'
|
|
151
157
|
*/
|
|
152
158
|
watcherUrl: z.url().optional(),
|
|
159
|
+
/**
|
|
160
|
+
* URL of the jeeves-meta API for synthesis engine health checks.
|
|
161
|
+
* Default: 'http://127.0.0.1:1938'
|
|
162
|
+
*/
|
|
163
|
+
metaUrl: z.url().default('http://127.0.0.1:1938'),
|
|
153
164
|
/**
|
|
154
165
|
* Global outsider policy â€" constrains which paths are eligible for outsider sharing.
|
|
155
166
|
* Uses the same allow/deny model as insider scopes.
|
package/src/config/types.ts
CHANGED
|
@@ -50,6 +50,7 @@ export interface ResolvedInsider {
|
|
|
50
50
|
*/
|
|
51
51
|
export interface RuntimeConfig {
|
|
52
52
|
port: number;
|
|
53
|
+
host: string;
|
|
53
54
|
eventTimeoutMs: number;
|
|
54
55
|
eventLogPurgeMs: number;
|
|
55
56
|
maxZipSizeMb: number;
|
|
@@ -64,6 +65,7 @@ export interface RuntimeConfig {
|
|
|
64
65
|
diagramCachePath?: string;
|
|
65
66
|
runnerUrl?: string;
|
|
66
67
|
watcherUrl?: string;
|
|
68
|
+
metaUrl?: string;
|
|
67
69
|
outsiderPolicy: NormalizedScopes | null;
|
|
68
70
|
events: JeevesConfig['events'];
|
|
69
71
|
authModes: AuthMode[];
|
|
@@ -3,6 +3,7 @@ import { describe, expect, it, vi } from 'vitest';
|
|
|
3
3
|
// Mock config
|
|
4
4
|
const mockConfig = {
|
|
5
5
|
port: 1934,
|
|
6
|
+
host: '0.0.0.0',
|
|
6
7
|
chromePath: '/usr/bin/chromium',
|
|
7
8
|
authModes: ['keys'],
|
|
8
9
|
resolvedInsiders: [{ email: 'a@b.com' }, { email: 'c@d.com' }],
|
|
@@ -18,6 +19,7 @@ const mockConfig = {
|
|
|
18
19
|
},
|
|
19
20
|
watcherUrl: null,
|
|
20
21
|
runnerUrl: null,
|
|
22
|
+
metaUrl: null,
|
|
21
23
|
exportFormats: ['pdf', 'docx', 'zip'],
|
|
22
24
|
};
|
|
23
25
|
|
package/src/routes/api/status.ts
CHANGED
|
@@ -44,9 +44,10 @@ export const statusRoutes: FastifyPluginAsync = async (fastify) => {
|
|
|
44
44
|
async (request) => {
|
|
45
45
|
const config = getConfig();
|
|
46
46
|
|
|
47
|
-
const [watcher, runner] = await Promise.all([
|
|
47
|
+
const [watcher, runner, meta] = await Promise.all([
|
|
48
48
|
config.watcherUrl ? checkService(config.watcherUrl) : null,
|
|
49
49
|
config.runnerUrl ? checkService(config.runnerUrl) : null,
|
|
50
|
+
config.metaUrl ? checkService(config.metaUrl) : null,
|
|
50
51
|
]);
|
|
51
52
|
|
|
52
53
|
return {
|
|
@@ -82,6 +83,7 @@ export const statusRoutes: FastifyPluginAsync = async (fastify) => {
|
|
|
82
83
|
services: {
|
|
83
84
|
watcher,
|
|
84
85
|
runner,
|
|
86
|
+
meta,
|
|
85
87
|
},
|
|
86
88
|
...(request.query.events
|
|
87
89
|
? {
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for GET /config route sanitization logic.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, it } from 'vitest';
|
|
6
|
+
|
|
7
|
+
import type { RuntimeConfig } from '../config/types.js';
|
|
8
|
+
import { sanitizeConfig } from './config.js';
|
|
9
|
+
|
|
10
|
+
/** Minimal RuntimeConfig fixture with sensitive fields populated. */
|
|
11
|
+
function makeConfig(overrides: Partial<RuntimeConfig> = {}): RuntimeConfig {
|
|
12
|
+
return {
|
|
13
|
+
port: 1934,
|
|
14
|
+
host: '0.0.0.0',
|
|
15
|
+
eventTimeoutMs: 30_000,
|
|
16
|
+
eventLogPurgeMs: 604_800_000,
|
|
17
|
+
maxZipSizeMb: 100,
|
|
18
|
+
chromePath: '/usr/bin/chromium',
|
|
19
|
+
plantuml: { servers: [] },
|
|
20
|
+
outsiderPolicy: null,
|
|
21
|
+
events: {},
|
|
22
|
+
authModes: ['keys'],
|
|
23
|
+
resolvedKeys: [
|
|
24
|
+
{
|
|
25
|
+
name: 'primary',
|
|
26
|
+
seed: 'secret-key-seed-abc',
|
|
27
|
+
scopes: null,
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
resolvedInsiders: [
|
|
31
|
+
{
|
|
32
|
+
email: 'jason@example.com',
|
|
33
|
+
seed: 'secret-insider-seed-xyz',
|
|
34
|
+
scopes: null,
|
|
35
|
+
keyCreatedAt: '2026-01-01T00:00:00Z',
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
googleAuth: {
|
|
39
|
+
clientId: 'public-client-id',
|
|
40
|
+
clientSecret: 'super-secret-oauth-secret',
|
|
41
|
+
},
|
|
42
|
+
sessionSecret: 'session-hmac-secret',
|
|
43
|
+
internalInsiderKey: 'internal-key-seed',
|
|
44
|
+
configPath: '/etc/jeeves-server.config.json',
|
|
45
|
+
eventsLog: '/var/log/events.log',
|
|
46
|
+
stateFile: '/var/state.json',
|
|
47
|
+
eventQueuePath: '/var/queue.jsonl',
|
|
48
|
+
eventQueueCursorPath: '/var/cursor.json',
|
|
49
|
+
eventLogPath: '/var/event-log.jsonl',
|
|
50
|
+
...overrides,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
describe('sanitizeConfig', () => {
|
|
55
|
+
it('redacts sessionSecret', () => {
|
|
56
|
+
const result = sanitizeConfig(makeConfig()) as Record<string, unknown>;
|
|
57
|
+
expect(result.sessionSecret).toBe('[REDACTED]');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('returns null for sessionSecret when not configured', () => {
|
|
61
|
+
const result = sanitizeConfig(
|
|
62
|
+
makeConfig({ sessionSecret: null }),
|
|
63
|
+
) as Record<string, unknown>;
|
|
64
|
+
expect(result.sessionSecret).toBeNull();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('redacts internalInsiderKey', () => {
|
|
68
|
+
const result = sanitizeConfig(makeConfig()) as Record<string, unknown>;
|
|
69
|
+
expect(result.internalInsiderKey).toBe('[REDACTED]');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('redacts googleAuth.clientSecret but preserves clientId', () => {
|
|
73
|
+
const result = sanitizeConfig(makeConfig()) as Record<string, unknown>;
|
|
74
|
+
const auth = result.googleAuth as {
|
|
75
|
+
clientId: string;
|
|
76
|
+
clientSecret: string;
|
|
77
|
+
};
|
|
78
|
+
expect(auth.clientId).toBe('public-client-id');
|
|
79
|
+
expect(auth.clientSecret).toBe('[REDACTED]');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('returns null for googleAuth when not configured', () => {
|
|
83
|
+
const result = sanitizeConfig(makeConfig({ googleAuth: null })) as Record<
|
|
84
|
+
string,
|
|
85
|
+
unknown
|
|
86
|
+
>;
|
|
87
|
+
expect(result.googleAuth).toBeNull();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('redacts all key seeds', () => {
|
|
91
|
+
const config = makeConfig({
|
|
92
|
+
resolvedKeys: [
|
|
93
|
+
{ name: 'a', seed: 'seed-a', scopes: null },
|
|
94
|
+
{ name: 'b', seed: 'seed-b', scopes: null },
|
|
95
|
+
],
|
|
96
|
+
});
|
|
97
|
+
const result = sanitizeConfig(config) as Record<string, unknown>;
|
|
98
|
+
const keys = result.resolvedKeys as Array<{ name: string; seed: string }>;
|
|
99
|
+
expect(keys).toHaveLength(2);
|
|
100
|
+
expect(keys[0].name).toBe('a');
|
|
101
|
+
expect(keys[0].seed).toBe('[REDACTED]');
|
|
102
|
+
expect(keys[1].seed).toBe('[REDACTED]');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('redacts all insider seeds but preserves other fields', () => {
|
|
106
|
+
const result = sanitizeConfig(makeConfig()) as Record<string, unknown>;
|
|
107
|
+
const insiders = result.resolvedInsiders as Array<{
|
|
108
|
+
email: string;
|
|
109
|
+
seed: string;
|
|
110
|
+
keyCreatedAt: string;
|
|
111
|
+
}>;
|
|
112
|
+
expect(insiders[0].email).toBe('jason@example.com');
|
|
113
|
+
expect(insiders[0].seed).toBe('[REDACTED]');
|
|
114
|
+
expect(insiders[0].keyCreatedAt).toBe('2026-01-01T00:00:00Z');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('preserves non-sensitive fields', () => {
|
|
118
|
+
const result = sanitizeConfig(makeConfig()) as Record<string, unknown>;
|
|
119
|
+
expect(result.port).toBe(1934);
|
|
120
|
+
expect(result.chromePath).toBe('/usr/bin/chromium');
|
|
121
|
+
expect(result.configPath).toBe('/etc/jeeves-server.config.json');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('never leaks raw secret values', () => {
|
|
125
|
+
const config = makeConfig();
|
|
126
|
+
const json = JSON.stringify(sanitizeConfig(config));
|
|
127
|
+
expect(json).not.toContain('secret-key-seed-abc');
|
|
128
|
+
expect(json).not.toContain('secret-insider-seed-xyz');
|
|
129
|
+
expect(json).not.toContain('super-secret-oauth-secret');
|
|
130
|
+
expect(json).not.toContain('session-hmac-secret');
|
|
131
|
+
expect(json).not.toContain('internal-key-seed');
|
|
132
|
+
});
|
|
133
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /config — query service configuration with optional JSONPath.
|
|
3
|
+
*
|
|
4
|
+
* Uses the core SDK's `createConfigQueryHandler()` for JSONPath support.
|
|
5
|
+
*
|
|
6
|
+
* @packageDocumentation
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { createConfigQueryHandler } from '@karmaniverous/jeeves';
|
|
10
|
+
import type { FastifyInstance } from 'fastify';
|
|
11
|
+
|
|
12
|
+
import { getConfig } from '../config/index.js';
|
|
13
|
+
import type { RuntimeConfig } from '../config/types.js';
|
|
14
|
+
|
|
15
|
+
/** Return a sanitized copy of the config (redact sensitive fields). */
|
|
16
|
+
export function sanitizeConfig(config: RuntimeConfig): unknown {
|
|
17
|
+
return {
|
|
18
|
+
...config,
|
|
19
|
+
sessionSecret: config.sessionSecret ? '[REDACTED]' : null,
|
|
20
|
+
internalInsiderKey: config.internalInsiderKey ? '[REDACTED]' : null,
|
|
21
|
+
googleAuth: config.googleAuth
|
|
22
|
+
? {
|
|
23
|
+
clientId: config.googleAuth.clientId,
|
|
24
|
+
clientSecret: '[REDACTED]',
|
|
25
|
+
}
|
|
26
|
+
: null,
|
|
27
|
+
resolvedKeys: config.resolvedKeys.map((k) => ({
|
|
28
|
+
...k,
|
|
29
|
+
seed: '[REDACTED]',
|
|
30
|
+
})),
|
|
31
|
+
resolvedInsiders: config.resolvedInsiders.map((i) => ({
|
|
32
|
+
...i,
|
|
33
|
+
seed: '[REDACTED]',
|
|
34
|
+
})),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Register the GET /config route. */
|
|
39
|
+
export function registerConfigRoute(app: FastifyInstance): void {
|
|
40
|
+
const configHandler = createConfigQueryHandler(() =>
|
|
41
|
+
sanitizeConfig(getConfig()),
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
app.get('/config', async (request, reply) => {
|
|
45
|
+
const { path } = request.query as { path?: string };
|
|
46
|
+
const result = await configHandler({ path });
|
|
47
|
+
return reply.status(result.status).send(result.body);
|
|
48
|
+
});
|
|
49
|
+
}
|