@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
package/src/auth/keys.ts
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authentication key verification and management.
|
|
3
|
+
*
|
|
4
|
+
* Key model: each configured key is an API key "seed". Insider and outsider
|
|
5
|
+
* keys are derived from seeds via HMAC. Verification iterates all seeds,
|
|
6
|
+
* checking derived keys. Scopes (if present) further restrict which paths
|
|
7
|
+
* a seed grants access to.
|
|
8
|
+
*
|
|
9
|
+
* Directory outsider links: an outsider key generated for a directory path
|
|
10
|
+
* grants access to all descendants. Verification checks the provided key
|
|
11
|
+
* against the requested path AND all ancestor paths.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import picomatch from 'picomatch';
|
|
15
|
+
|
|
16
|
+
import type {
|
|
17
|
+
KeyVerificationResult,
|
|
18
|
+
NormalizedScopes,
|
|
19
|
+
ResolvedInsider,
|
|
20
|
+
ResolvedKey,
|
|
21
|
+
} from '../config/types.js';
|
|
22
|
+
import {
|
|
23
|
+
computeDeepShareKey,
|
|
24
|
+
computeInsiderKey,
|
|
25
|
+
computeOutsiderKeyWithExpiry,
|
|
26
|
+
computePathKey,
|
|
27
|
+
type DeepShareParams,
|
|
28
|
+
timingSafeEqual,
|
|
29
|
+
} from '../util/crypto.js';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Check whether a path matches a list of scope patterns.
|
|
33
|
+
* Uses picomatch for glob matching — standard glob semantics apply.
|
|
34
|
+
* Use `/**` for recursive matching, `/*` for single-level only.
|
|
35
|
+
*/
|
|
36
|
+
function pathMatchesPatterns(requestPath: string, patterns: string[]): boolean {
|
|
37
|
+
const normalized = requestPath.toLowerCase().replace(/\/+$/, '');
|
|
38
|
+
const isMatch = picomatch(
|
|
39
|
+
patterns.map((p) => p.toLowerCase().replace(/\/+$/, '')),
|
|
40
|
+
{ dot: true },
|
|
41
|
+
);
|
|
42
|
+
return isMatch(normalized);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Check whether a request path matches normalized scopes (allow/deny).
|
|
47
|
+
* Path must match at least one allow rule AND NOT match any deny rule.
|
|
48
|
+
*/
|
|
49
|
+
function pathMatchesScopes(
|
|
50
|
+
requestPath: string,
|
|
51
|
+
scopes: NormalizedScopes,
|
|
52
|
+
): boolean {
|
|
53
|
+
if (!pathMatchesPatterns(requestPath, scopes.allow)) return false;
|
|
54
|
+
if (scopes.deny.length > 0 && pathMatchesPatterns(requestPath, scopes.deny))
|
|
55
|
+
return false;
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Get the requested path and all ancestor paths.
|
|
61
|
+
* E.g. "/d/projects/foo/bar.md" results in that path plus all ancestors.
|
|
62
|
+
*/
|
|
63
|
+
function getPathAndAncestors(urlPath: string): string[] {
|
|
64
|
+
const paths = [urlPath];
|
|
65
|
+
let current = urlPath;
|
|
66
|
+
while (current.includes('/')) {
|
|
67
|
+
current = current.substring(0, current.lastIndexOf('/'));
|
|
68
|
+
paths.push(current);
|
|
69
|
+
}
|
|
70
|
+
return paths;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Check an outsider key against a seed for exact path or any ancestor (directory links).
|
|
75
|
+
* Returns the matched path if found, or null.
|
|
76
|
+
*/
|
|
77
|
+
function checkOutsiderKey(
|
|
78
|
+
seed: string,
|
|
79
|
+
urlPath: string,
|
|
80
|
+
providedKey: string,
|
|
81
|
+
expParam: string | undefined,
|
|
82
|
+
deepParams?: { d: string; dirs: string; s: string },
|
|
83
|
+
): string | null {
|
|
84
|
+
// Deep share key check (when d and s params are present)
|
|
85
|
+
if (deepParams) {
|
|
86
|
+
const params: DeepShareParams = {
|
|
87
|
+
depth: parseInt(deepParams.d, 10),
|
|
88
|
+
dirs: deepParams.dirs === '1',
|
|
89
|
+
stack: deepParams.s,
|
|
90
|
+
exp: expParam,
|
|
91
|
+
};
|
|
92
|
+
if (!isNaN(params.depth)) {
|
|
93
|
+
// Check expiry if present
|
|
94
|
+
if (params.exp) {
|
|
95
|
+
const expiry = parseInt(params.exp, 10);
|
|
96
|
+
if (isNaN(expiry) || expiry < Date.now()) return null;
|
|
97
|
+
}
|
|
98
|
+
const expectedKey = computeDeepShareKey(seed, urlPath, params);
|
|
99
|
+
if (timingSafeEqual(providedKey, expectedKey)) return urlPath;
|
|
100
|
+
}
|
|
101
|
+
return null; // Deep params present but invalid — don't fall through to legacy
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Legacy outsider key check (no deep params)
|
|
105
|
+
const pathsToCheck = getPathAndAncestors(urlPath);
|
|
106
|
+
|
|
107
|
+
for (const checkPath of pathsToCheck) {
|
|
108
|
+
// Check with expiry
|
|
109
|
+
if (expParam) {
|
|
110
|
+
const expiry = parseInt(expParam, 10);
|
|
111
|
+
if (!isNaN(expiry) && expiry >= Date.now()) {
|
|
112
|
+
const expectedKey = computeOutsiderKeyWithExpiry(
|
|
113
|
+
seed,
|
|
114
|
+
checkPath,
|
|
115
|
+
expParam,
|
|
116
|
+
);
|
|
117
|
+
if (timingSafeEqual(providedKey, expectedKey)) return checkPath;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Check without expiry
|
|
122
|
+
const expectedKey = computePathKey(seed, checkPath);
|
|
123
|
+
if (timingSafeEqual(providedKey, expectedKey)) return checkPath;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Verify a provided key against all configured seeds and determine access mode.
|
|
131
|
+
*
|
|
132
|
+
* Machine API keys can grant both insider and outsider access.
|
|
133
|
+
* Insider (Google OAuth) seeds can only grant outsider access — they are
|
|
134
|
+
* never valid as insider URL keys.
|
|
135
|
+
*
|
|
136
|
+
* Outsider keys are checked against the requested path and all ancestor
|
|
137
|
+
* paths, enabling directory-level outsider links.
|
|
138
|
+
*/
|
|
139
|
+
export function verifyKey(
|
|
140
|
+
resolvedKeys: ResolvedKey[],
|
|
141
|
+
urlPath: string,
|
|
142
|
+
providedKey: string | undefined,
|
|
143
|
+
expParam: string | undefined,
|
|
144
|
+
resolvedInsiders: ResolvedInsider[] = [],
|
|
145
|
+
deepParams?: { d: string; dirs: string; s: string },
|
|
146
|
+
): KeyVerificationResult {
|
|
147
|
+
const fail: KeyVerificationResult = {
|
|
148
|
+
valid: false,
|
|
149
|
+
mode: null,
|
|
150
|
+
keyName: null,
|
|
151
|
+
seed: null,
|
|
152
|
+
matchedPath: null,
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
if (!providedKey) return fail;
|
|
156
|
+
|
|
157
|
+
// Check machine API keys (insider + outsider access)
|
|
158
|
+
for (const rk of resolvedKeys) {
|
|
159
|
+
// Check insider key (exact only, no ancestor check)
|
|
160
|
+
const insiderKey = computeInsiderKey(rk.seed);
|
|
161
|
+
if (timingSafeEqual(providedKey, insiderKey)) {
|
|
162
|
+
if (rk.scopes && !pathMatchesScopes(urlPath, rk.scopes)) {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
valid: true,
|
|
167
|
+
mode: 'insider',
|
|
168
|
+
keyName: rk.name,
|
|
169
|
+
seed: rk.seed,
|
|
170
|
+
matchedPath: null,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Check outsider key (exact path + ancestors for directory links)
|
|
175
|
+
const machineMatch = checkOutsiderKey(
|
|
176
|
+
rk.seed,
|
|
177
|
+
urlPath,
|
|
178
|
+
providedKey,
|
|
179
|
+
expParam,
|
|
180
|
+
deepParams,
|
|
181
|
+
);
|
|
182
|
+
if (machineMatch !== null) {
|
|
183
|
+
if (rk.scopes && !pathMatchesScopes(urlPath, rk.scopes)) {
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
return {
|
|
187
|
+
valid: true,
|
|
188
|
+
mode: 'outsider',
|
|
189
|
+
keyName: rk.name,
|
|
190
|
+
seed: rk.seed,
|
|
191
|
+
matchedPath: machineMatch,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Check insider seeds for outsider access ONLY (never insider access)
|
|
197
|
+
for (const ri of resolvedInsiders) {
|
|
198
|
+
if (!ri.seed) continue;
|
|
199
|
+
|
|
200
|
+
const insiderMatch = checkOutsiderKey(
|
|
201
|
+
ri.seed,
|
|
202
|
+
urlPath,
|
|
203
|
+
providedKey,
|
|
204
|
+
expParam,
|
|
205
|
+
deepParams,
|
|
206
|
+
);
|
|
207
|
+
if (insiderMatch !== null) {
|
|
208
|
+
if (ri.scopes && !pathMatchesScopes(urlPath, ri.scopes)) {
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
return {
|
|
212
|
+
valid: true,
|
|
213
|
+
mode: 'outsider',
|
|
214
|
+
keyName: ri.email,
|
|
215
|
+
seed: ri.seed,
|
|
216
|
+
matchedPath: insiderMatch,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return fail;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Check whether a directory should be visible given allow scope patterns.
|
|
226
|
+
* A directory is visible if any allowed scope is under it OR above it.
|
|
227
|
+
* This enables navigating toward allowed paths through parent directories.
|
|
228
|
+
*/
|
|
229
|
+
function directoryVisibleUnderScopes(
|
|
230
|
+
dirUrlPath: string,
|
|
231
|
+
allowPatterns: string[],
|
|
232
|
+
): boolean {
|
|
233
|
+
const normalized = dirUrlPath.toLowerCase().replace(/\/+$/, '');
|
|
234
|
+
for (const pattern of allowPatterns) {
|
|
235
|
+
const p = pattern.toLowerCase().replace(/\/+$/, '');
|
|
236
|
+
// Strip trailing glob parts to get the "prefix" of the pattern
|
|
237
|
+
const prefix = p.replace(/\/\*\*$/, '').replace(/\/\*$/, '');
|
|
238
|
+
// Directory is above a scope (navigate toward it)
|
|
239
|
+
if (prefix.startsWith(normalized + '/') || prefix === normalized)
|
|
240
|
+
return true;
|
|
241
|
+
// Directory is under a scope (already inside an allowed area)
|
|
242
|
+
if (normalized.startsWith(prefix + '/') || normalized === prefix)
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export {
|
|
249
|
+
directoryVisibleUnderScopes as _directoryVisibleUnderScopes,
|
|
250
|
+
pathMatchesPatterns as _pathMatchesPatterns,
|
|
251
|
+
pathMatchesScopes as _pathMatchesScopes,
|
|
252
|
+
};
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified auth resolution — single function to resolve authentication
|
|
3
|
+
* from any combination of key params, session cookies, and insider seeds.
|
|
4
|
+
*
|
|
5
|
+
* Replaces duplicated auth resolution logic across middleware, keys, sharing,
|
|
6
|
+
* and auth-status routes.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { FastifyRequest } from 'fastify';
|
|
10
|
+
|
|
11
|
+
import type {
|
|
12
|
+
AccessMode,
|
|
13
|
+
NormalizedScopes,
|
|
14
|
+
ResolvedInsider,
|
|
15
|
+
RuntimeConfig,
|
|
16
|
+
} from '../config/types.js';
|
|
17
|
+
import { computeInsiderKey, timingSafeEqual } from '../util/crypto.js';
|
|
18
|
+
import { formatRelativeTime } from '../util/formatters.js';
|
|
19
|
+
import { verifyKey } from './keys.js';
|
|
20
|
+
import { COOKIE_NAME, verifySessionCookie } from './session.js';
|
|
21
|
+
|
|
22
|
+
export interface AuthResult {
|
|
23
|
+
valid: boolean;
|
|
24
|
+
mode?: AccessMode;
|
|
25
|
+
seed?: string;
|
|
26
|
+
scopes?: NormalizedScopes | null;
|
|
27
|
+
email?: string;
|
|
28
|
+
keyAge?: string | null;
|
|
29
|
+
keyName?: string | null;
|
|
30
|
+
matchedPath?: string | null;
|
|
31
|
+
deepShareParams?: { d: string; dirs: string; s: string };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const FAIL: AuthResult = { valid: false };
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Resolve auth from a key parameter (machine API keys + insider outsider keys).
|
|
38
|
+
* Checks both resolvedKeys and resolvedInsiders.
|
|
39
|
+
*/
|
|
40
|
+
export function resolveKeyAuth(
|
|
41
|
+
config: RuntimeConfig,
|
|
42
|
+
urlPath: string,
|
|
43
|
+
key: string | undefined,
|
|
44
|
+
expParam: string | undefined,
|
|
45
|
+
deepParams?: { d: string; dirs: string; s: string },
|
|
46
|
+
): AuthResult {
|
|
47
|
+
if (!key) return FAIL;
|
|
48
|
+
|
|
49
|
+
const result = verifyKey(
|
|
50
|
+
config.resolvedKeys,
|
|
51
|
+
urlPath,
|
|
52
|
+
key,
|
|
53
|
+
expParam,
|
|
54
|
+
config.resolvedInsiders,
|
|
55
|
+
deepParams,
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
if (result.valid) {
|
|
59
|
+
return {
|
|
60
|
+
valid: true,
|
|
61
|
+
mode: result.mode ?? undefined,
|
|
62
|
+
seed: result.seed ?? undefined,
|
|
63
|
+
keyName: result.keyName,
|
|
64
|
+
matchedPath: result.matchedPath,
|
|
65
|
+
deepShareParams: deepParams,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return FAIL;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Resolve auth from a key that might be an insider key (derived from seed).
|
|
74
|
+
* Used when we need to match a provided key against insider seeds directly.
|
|
75
|
+
*/
|
|
76
|
+
export function resolveInsiderKeyAuth(
|
|
77
|
+
config: RuntimeConfig,
|
|
78
|
+
key: string,
|
|
79
|
+
): AuthResult {
|
|
80
|
+
// Check machine keys
|
|
81
|
+
for (const rk of config.resolvedKeys) {
|
|
82
|
+
const insiderKey = computeInsiderKey(rk.seed);
|
|
83
|
+
if (timingSafeEqual(key, insiderKey)) {
|
|
84
|
+
return {
|
|
85
|
+
valid: true,
|
|
86
|
+
mode: 'insider',
|
|
87
|
+
seed: rk.seed,
|
|
88
|
+
scopes: rk.scopes,
|
|
89
|
+
keyName: rk.name,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Check insider seeds
|
|
95
|
+
for (const ri of config.resolvedInsiders) {
|
|
96
|
+
if (!ri.seed) continue;
|
|
97
|
+
const insiderKey = computeInsiderKey(ri.seed);
|
|
98
|
+
if (timingSafeEqual(key, insiderKey)) {
|
|
99
|
+
return {
|
|
100
|
+
valid: true,
|
|
101
|
+
mode: 'insider',
|
|
102
|
+
seed: ri.seed,
|
|
103
|
+
scopes: ri.scopes,
|
|
104
|
+
email: ri.email,
|
|
105
|
+
keyName: ri.email,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return FAIL;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Resolve auth from a session cookie.
|
|
115
|
+
* Returns insider auth if the session email matches a configured insider with a seed.
|
|
116
|
+
*/
|
|
117
|
+
export function resolveSessionAuth(
|
|
118
|
+
config: RuntimeConfig,
|
|
119
|
+
request: FastifyRequest,
|
|
120
|
+
): AuthResult {
|
|
121
|
+
const { sessionSecret } = config;
|
|
122
|
+
if (!sessionSecret) return FAIL;
|
|
123
|
+
|
|
124
|
+
const cookieValue = (request.cookies as Record<string, string> | undefined)?.[
|
|
125
|
+
COOKIE_NAME
|
|
126
|
+
];
|
|
127
|
+
if (!cookieValue) return FAIL;
|
|
128
|
+
|
|
129
|
+
const session = verifySessionCookie(cookieValue, sessionSecret);
|
|
130
|
+
if (!session) return FAIL;
|
|
131
|
+
|
|
132
|
+
const insider = config.resolvedInsiders.find(
|
|
133
|
+
(i) => i.email.toLowerCase() === session.email.toLowerCase(),
|
|
134
|
+
);
|
|
135
|
+
if (!insider?.seed) return FAIL;
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
valid: true,
|
|
139
|
+
mode: 'insider',
|
|
140
|
+
seed: insider.seed,
|
|
141
|
+
scopes: insider.scopes,
|
|
142
|
+
email: insider.email,
|
|
143
|
+
keyAge: insider.keyCreatedAt
|
|
144
|
+
? formatRelativeTime(insider.keyCreatedAt)
|
|
145
|
+
: null,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Find a resolved insider by email.
|
|
151
|
+
*/
|
|
152
|
+
export function findInsider(
|
|
153
|
+
insiders: ResolvedInsider[],
|
|
154
|
+
email: string,
|
|
155
|
+
): ResolvedInsider | undefined {
|
|
156
|
+
return insiders.find((i) => i.email.toLowerCase() === email.toLowerCase());
|
|
157
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
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
|
+
|
|
8
|
+
import crypto from 'node:crypto';
|
|
9
|
+
|
|
10
|
+
const COOKIE_NAME = 'jeeves_session';
|
|
11
|
+
const SESSION_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000; // 30 days
|
|
12
|
+
|
|
13
|
+
export interface SessionPayload {
|
|
14
|
+
email: string;
|
|
15
|
+
picture?: string;
|
|
16
|
+
exp: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Create a signed session cookie value.
|
|
21
|
+
*/
|
|
22
|
+
export function createSessionCookie(
|
|
23
|
+
email: string,
|
|
24
|
+
sessionSecret: string,
|
|
25
|
+
picture?: string,
|
|
26
|
+
): string {
|
|
27
|
+
const payload: SessionPayload = {
|
|
28
|
+
email,
|
|
29
|
+
...(picture ? { picture } : {}),
|
|
30
|
+
exp: Date.now() + SESSION_MAX_AGE_MS,
|
|
31
|
+
};
|
|
32
|
+
const b64 = Buffer.from(JSON.stringify(payload)).toString('base64url');
|
|
33
|
+
const sig = crypto
|
|
34
|
+
.createHmac('sha256', sessionSecret)
|
|
35
|
+
.update(b64)
|
|
36
|
+
.digest('hex');
|
|
37
|
+
return `${b64}.${sig}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Verify and decode a session cookie. Returns null if invalid/expired.
|
|
42
|
+
*/
|
|
43
|
+
export function verifySessionCookie(
|
|
44
|
+
cookieValue: string,
|
|
45
|
+
sessionSecret: string,
|
|
46
|
+
): SessionPayload | null {
|
|
47
|
+
const dotIdx = cookieValue.lastIndexOf('.');
|
|
48
|
+
if (dotIdx < 0) return null;
|
|
49
|
+
|
|
50
|
+
const b64 = cookieValue.slice(0, dotIdx);
|
|
51
|
+
const sig = cookieValue.slice(dotIdx + 1);
|
|
52
|
+
|
|
53
|
+
const expectedSig = crypto
|
|
54
|
+
.createHmac('sha256', sessionSecret)
|
|
55
|
+
.update(b64)
|
|
56
|
+
.digest('hex');
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expectedSig))) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
} catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const payload = JSON.parse(
|
|
68
|
+
Buffer.from(b64, 'base64url').toString(),
|
|
69
|
+
) as SessionPayload;
|
|
70
|
+
if (payload.exp < Date.now()) return null;
|
|
71
|
+
return payload;
|
|
72
|
+
} catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export { COOKIE_NAME };
|
|
@@ -0,0 +1,107 @@
|
|
|
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
|
+
|
|
7
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
8
|
+
|
|
9
|
+
const execFileAsync = promisify(execFile);
|
|
10
|
+
|
|
11
|
+
const CLI_PATH = path.resolve(
|
|
12
|
+
import.meta.dirname,
|
|
13
|
+
'../../../dist/src/cli/index.js',
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
const VALID_CONFIG = {
|
|
17
|
+
port: 8765,
|
|
18
|
+
chromePath: '/usr/bin/chromium',
|
|
19
|
+
auth: { modes: ['keys'] },
|
|
20
|
+
keys: {
|
|
21
|
+
primary: 'a'.repeat(64),
|
|
22
|
+
_internal: 'b'.repeat(64),
|
|
23
|
+
},
|
|
24
|
+
events: {},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function writeConfig(dir: string, config: unknown): string {
|
|
28
|
+
const filePath = path.join(dir, 'jeeves-server.config.json');
|
|
29
|
+
fs.writeFileSync(filePath, JSON.stringify(config));
|
|
30
|
+
return filePath;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function runCli(
|
|
34
|
+
args: string[],
|
|
35
|
+
): Promise<{ stdout: string; stderr: string }> {
|
|
36
|
+
return execFileAsync('node', [CLI_PATH, ...args], { timeout: 10_000 });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe('jeeves-server config validate', () => {
|
|
40
|
+
let tmpDir: string;
|
|
41
|
+
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'jeeves-cli-'));
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
afterEach(() => {
|
|
47
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('validates a valid config and prints summary', async () => {
|
|
51
|
+
const configPath = writeConfig(tmpDir, VALID_CONFIG);
|
|
52
|
+
const { stdout } = await runCli(['config', 'validate', '-c', configPath]);
|
|
53
|
+
expect(stdout).toContain('Configuration valid');
|
|
54
|
+
expect(stdout).toContain('Port: 8765');
|
|
55
|
+
expect(stdout).toContain('Auth modes: keys');
|
|
56
|
+
expect(stdout).toContain('Keys: 2');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('exits with error for invalid config', async () => {
|
|
60
|
+
const configPath = writeConfig(tmpDir, { port: 1234 });
|
|
61
|
+
await expect(
|
|
62
|
+
runCli(['config', 'validate', '-c', configPath]),
|
|
63
|
+
).rejects.toMatchObject({
|
|
64
|
+
code: 1,
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('jeeves-server config show', () => {
|
|
70
|
+
let tmpDir: string;
|
|
71
|
+
|
|
72
|
+
beforeEach(() => {
|
|
73
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'jeeves-cli-'));
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
afterEach(() => {
|
|
77
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('shows resolved config with key and insider details', async () => {
|
|
81
|
+
const configPath = writeConfig(tmpDir, {
|
|
82
|
+
...VALID_CONFIG,
|
|
83
|
+
insiders: { 'test@example.com': {} },
|
|
84
|
+
watcherUrl: 'http://localhost:3458',
|
|
85
|
+
});
|
|
86
|
+
const { stdout } = await runCli(['config', 'show', '-c', configPath]);
|
|
87
|
+
expect(stdout).toContain('Config file:');
|
|
88
|
+
expect(stdout).toContain('port: 8765');
|
|
89
|
+
expect(stdout).toContain('modes: keys');
|
|
90
|
+
expect(stdout).toContain('primary:');
|
|
91
|
+
expect(stdout).toContain('unscoped');
|
|
92
|
+
expect(stdout).toContain('test@example.com');
|
|
93
|
+
expect(stdout).toContain('watcherUrl: http://localhost:3458');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('shows scoped keys correctly', async () => {
|
|
97
|
+
const configPath = writeConfig(tmpDir, {
|
|
98
|
+
...VALID_CONFIG,
|
|
99
|
+
keys: {
|
|
100
|
+
...VALID_CONFIG.keys,
|
|
101
|
+
scoped: { key: 'c'.repeat(64), scopes: ['/docs'] },
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
const { stdout } = await runCli(['config', 'show', '-c', configPath]);
|
|
105
|
+
expect(stdout).toContain('scoped (allow: 1, deny: 0)');
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @packageDocumentation
|
|
3
|
+
*
|
|
4
|
+
* CLI commands: config validate, config show.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Command } from '@commander-js/extra-typings';
|
|
8
|
+
|
|
9
|
+
import { loadConfig } from '../../config/index.js';
|
|
10
|
+
import type { NormalizedScopes } from '../../config/types.js';
|
|
11
|
+
|
|
12
|
+
function formatScopes(scopes: NormalizedScopes | null): string {
|
|
13
|
+
return scopes
|
|
14
|
+
? `scoped (allow: ${String(scopes.allow.length)}, deny: ${String(scopes.deny.length)})`
|
|
15
|
+
: 'unscoped';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function registerConfigCommand(cli: Command): void {
|
|
19
|
+
const config = cli.command('config').description('Configuration management');
|
|
20
|
+
|
|
21
|
+
config
|
|
22
|
+
.command('validate')
|
|
23
|
+
.description('Validate the configuration file')
|
|
24
|
+
.option('-c, --config <path>', 'Path to configuration file')
|
|
25
|
+
.action(async (options) => {
|
|
26
|
+
try {
|
|
27
|
+
const cfg = await loadConfig(options.config);
|
|
28
|
+
console.log('\u2713 Configuration valid');
|
|
29
|
+
console.log(` Port: ${String(cfg.port)}`);
|
|
30
|
+
console.log(` Auth modes: ${cfg.authModes.join(', ')}`);
|
|
31
|
+
console.log(` Keys: ${String(cfg.resolvedKeys.length)}`);
|
|
32
|
+
console.log(` Insiders: ${String(cfg.resolvedInsiders.length)}`);
|
|
33
|
+
console.log(
|
|
34
|
+
` Events: ${String(Object.keys(cfg.events).length)} schemas`,
|
|
35
|
+
);
|
|
36
|
+
if (cfg.watcherUrl) console.log(` Watcher: ${cfg.watcherUrl}`);
|
|
37
|
+
if (cfg.runnerUrl) console.log(` Runner: ${cfg.runnerUrl}`);
|
|
38
|
+
} catch (error) {
|
|
39
|
+
console.error('\u2717 Configuration invalid');
|
|
40
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
config
|
|
46
|
+
.command('show')
|
|
47
|
+
.description('Display resolved configuration with provenance')
|
|
48
|
+
.option('-c, --config <path>', 'Path to configuration file')
|
|
49
|
+
.action(async (options) => {
|
|
50
|
+
try {
|
|
51
|
+
const cfg = await loadConfig(options.config);
|
|
52
|
+
console.log(`Config file: ${cfg.configPath}`);
|
|
53
|
+
console.log('');
|
|
54
|
+
console.log('Server:');
|
|
55
|
+
console.log(` port: ${String(cfg.port)}`);
|
|
56
|
+
console.log(` chromePath: ${cfg.chromePath}`);
|
|
57
|
+
if (cfg.roots) {
|
|
58
|
+
console.log(` roots: ${JSON.stringify(cfg.roots)}`);
|
|
59
|
+
}
|
|
60
|
+
console.log('');
|
|
61
|
+
console.log('Auth:');
|
|
62
|
+
console.log(` modes: ${cfg.authModes.join(', ')}`);
|
|
63
|
+
console.log(` googleAuth: ${cfg.googleAuth ? 'configured' : 'none'}`);
|
|
64
|
+
console.log(` sessionSecret: ${cfg.sessionSecret ? '***' : 'none'}`);
|
|
65
|
+
console.log('');
|
|
66
|
+
console.log('Keys:');
|
|
67
|
+
for (const key of cfg.resolvedKeys) {
|
|
68
|
+
console.log(
|
|
69
|
+
` ${key.name}: ${key.seed.slice(0, 8)}... (${formatScopes(key.scopes)})`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
console.log('');
|
|
73
|
+
console.log('Insiders:');
|
|
74
|
+
for (const insider of cfg.resolvedInsiders) {
|
|
75
|
+
console.log(` ${insider.email}: ${formatScopes(insider.scopes)}`);
|
|
76
|
+
}
|
|
77
|
+
console.log('');
|
|
78
|
+
console.log('Integrations:');
|
|
79
|
+
console.log(` watcherUrl: ${cfg.watcherUrl ?? 'not configured'}`);
|
|
80
|
+
console.log(` runnerUrl: ${cfg.runnerUrl ?? 'not configured'}`);
|
|
81
|
+
console.log('');
|
|
82
|
+
console.log('Events:');
|
|
83
|
+
const eventNames = Object.keys(cfg.events);
|
|
84
|
+
if (eventNames.length === 0) {
|
|
85
|
+
console.log(' (none)');
|
|
86
|
+
} else {
|
|
87
|
+
for (const name of eventNames) {
|
|
88
|
+
console.log(` ${name}: ${cfg.events[name].cmd}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
console.log('');
|
|
92
|
+
console.log('Diagrams:');
|
|
93
|
+
console.log(
|
|
94
|
+
` mermaidCliPath: ${cfg.mermaidCliPath ?? 'not configured'}`,
|
|
95
|
+
);
|
|
96
|
+
console.log(
|
|
97
|
+
` plantuml.jarPath: ${cfg.plantuml.jarPath ?? 'not configured'}`,
|
|
98
|
+
);
|
|
99
|
+
console.log(` plantuml.servers: ${cfg.plantuml.servers.join(', ')}`);
|
|
100
|
+
console.log('');
|
|
101
|
+
console.log('Paths:');
|
|
102
|
+
console.log(` stateFile: ${cfg.stateFile}`);
|
|
103
|
+
console.log(` eventsLog: ${cfg.eventsLog}`);
|
|
104
|
+
console.log(
|
|
105
|
+
` diagramCachePath: ${cfg.diagramCachePath ?? '(default)'}`,
|
|
106
|
+
);
|
|
107
|
+
} catch (error) {
|
|
108
|
+
console.error('Failed to load config');
|
|
109
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
}
|