@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,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event route tests
|
|
3
|
+
*/
|
|
4
|
+
import { JsonMap } from '@karmaniverous/jsonmap';
|
|
5
|
+
import Ajv from 'ajv';
|
|
6
|
+
import * as _ from 'radash';
|
|
7
|
+
import { describe, expect, it } from 'vitest';
|
|
8
|
+
const ajv = new Ajv();
|
|
9
|
+
describe('event route', () => {
|
|
10
|
+
describe('schema matching', () => {
|
|
11
|
+
it('should match against JSON Schema', () => {
|
|
12
|
+
const schema = {
|
|
13
|
+
type: 'object',
|
|
14
|
+
properties: {
|
|
15
|
+
type: { const: 'page.content_updated' },
|
|
16
|
+
},
|
|
17
|
+
required: ['type'],
|
|
18
|
+
};
|
|
19
|
+
const validate = ajv.compile(schema);
|
|
20
|
+
expect(validate({ type: 'page.content_updated', data: {} })).toBe(true);
|
|
21
|
+
expect(validate({ type: 'other' })).toBe(false);
|
|
22
|
+
expect(validate({ data: {} })).toBe(false);
|
|
23
|
+
});
|
|
24
|
+
it('should match first schema in order', () => {
|
|
25
|
+
const schema1 = {
|
|
26
|
+
type: 'object',
|
|
27
|
+
properties: {
|
|
28
|
+
source: { const: 'github' },
|
|
29
|
+
},
|
|
30
|
+
required: ['source'],
|
|
31
|
+
};
|
|
32
|
+
const schema2 = {
|
|
33
|
+
type: 'object',
|
|
34
|
+
properties: {
|
|
35
|
+
type: { const: 'generic' },
|
|
36
|
+
},
|
|
37
|
+
required: ['type'],
|
|
38
|
+
};
|
|
39
|
+
const body = { source: 'github', type: 'generic' };
|
|
40
|
+
const validate1 = ajv.compile(schema1);
|
|
41
|
+
const validate2 = ajv.compile(schema2);
|
|
42
|
+
// Both match, but first wins
|
|
43
|
+
expect(validate1(body)).toBe(true);
|
|
44
|
+
expect(validate2(body)).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
it('should handle complex nested schemas', () => {
|
|
47
|
+
const schema = {
|
|
48
|
+
type: 'object',
|
|
49
|
+
properties: {
|
|
50
|
+
data: {
|
|
51
|
+
type: 'object',
|
|
52
|
+
properties: {
|
|
53
|
+
page_id: { type: 'string' },
|
|
54
|
+
},
|
|
55
|
+
required: ['page_id'],
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
required: ['data'],
|
|
59
|
+
};
|
|
60
|
+
const validate = ajv.compile(schema);
|
|
61
|
+
expect(validate({ data: { page_id: 'abc123' } })).toBe(true);
|
|
62
|
+
expect(validate({ data: {} })).toBe(false);
|
|
63
|
+
expect(validate({})).toBe(false);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
describe('JsonMap transformation', () => {
|
|
67
|
+
it('should extract fields using radash get', async () => {
|
|
68
|
+
const body = {
|
|
69
|
+
type: 'page.content_updated',
|
|
70
|
+
data: {
|
|
71
|
+
page_id: 'abc123',
|
|
72
|
+
title: 'Test Page',
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
const map = {
|
|
76
|
+
pageId: {
|
|
77
|
+
$: { method: '$.lib._.get', params: ['$.input', 'data.page_id'] },
|
|
78
|
+
},
|
|
79
|
+
type: {
|
|
80
|
+
$: { method: '$.lib._.get', params: ['$.input', 'type'] },
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
const mapper = new JsonMap(map, { _: _ });
|
|
84
|
+
const result = (await mapper.transform(body));
|
|
85
|
+
expect(result.pageId).toBe('abc123');
|
|
86
|
+
expect(result.type).toBe('page.content_updated');
|
|
87
|
+
});
|
|
88
|
+
it('should handle missing fields gracefully', async () => {
|
|
89
|
+
const body = {
|
|
90
|
+
type: 'event',
|
|
91
|
+
};
|
|
92
|
+
const map = {
|
|
93
|
+
pageId: {
|
|
94
|
+
$: { method: '$.lib._.get', params: ['$.input', 'data.page_id'] },
|
|
95
|
+
},
|
|
96
|
+
type: {
|
|
97
|
+
$: { method: '$.lib._.get', params: ['$.input', 'type'] },
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
const mapper = new JsonMap(map, { _: _ });
|
|
101
|
+
const result = (await mapper.transform(body));
|
|
102
|
+
expect(result.pageId).toBeUndefined();
|
|
103
|
+
expect(result.type).toBe('event');
|
|
104
|
+
});
|
|
105
|
+
it('should preserve full body when no map is defined', () => {
|
|
106
|
+
const body = {
|
|
107
|
+
type: 'event',
|
|
108
|
+
data: { foo: 'bar' },
|
|
109
|
+
nested: { a: { b: 'c' } },
|
|
110
|
+
};
|
|
111
|
+
// No transformation when map is undefined
|
|
112
|
+
const result = body;
|
|
113
|
+
expect(result).toEqual(body);
|
|
114
|
+
expect(result.data).toEqual({ foo: 'bar' });
|
|
115
|
+
expect(result.nested).toEqual({ a: { b: 'c' } });
|
|
116
|
+
});
|
|
117
|
+
it('should handle nested path extraction', async () => {
|
|
118
|
+
const body = {
|
|
119
|
+
metadata: {
|
|
120
|
+
author: {
|
|
121
|
+
name: 'Alice',
|
|
122
|
+
email: 'alice@example.com',
|
|
123
|
+
},
|
|
124
|
+
tags: ['important', 'review'],
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
const map = {
|
|
128
|
+
authorName: {
|
|
129
|
+
$: {
|
|
130
|
+
method: '$.lib._.get',
|
|
131
|
+
params: ['$.input', 'metadata.author.name'],
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
authorEmail: {
|
|
135
|
+
$: {
|
|
136
|
+
method: '$.lib._.get',
|
|
137
|
+
params: ['$.input', 'metadata.author.email'],
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
const mapper = new JsonMap(map, { _: _ });
|
|
142
|
+
const result = (await mapper.transform(body));
|
|
143
|
+
expect(result.authorName).toBe('Alice');
|
|
144
|
+
expect(result.authorEmail).toBe('alice@example.com');
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
describe('queue entry formatting', () => {
|
|
148
|
+
it('should format entry with all required fields', () => {
|
|
149
|
+
const entry = {
|
|
150
|
+
ts: new Date().toISOString(),
|
|
151
|
+
event: 'test-event',
|
|
152
|
+
cmd: 'node test.js',
|
|
153
|
+
body: { foo: 'bar' },
|
|
154
|
+
timeoutMs: 30000,
|
|
155
|
+
};
|
|
156
|
+
expect(entry.ts).toMatch(/^\d{4}-\d{2}-\d{2}T/);
|
|
157
|
+
expect(entry.event).toBe('test-event');
|
|
158
|
+
expect(entry.cmd).toBe('node test.js');
|
|
159
|
+
expect(entry.body).toEqual({ foo: 'bar' });
|
|
160
|
+
expect(entry.timeoutMs).toBe(30000);
|
|
161
|
+
});
|
|
162
|
+
it('should use default timeout when not specified', () => {
|
|
163
|
+
const defaultTimeoutMs = 30000;
|
|
164
|
+
const eventConfig = {}; // Simulate config without timeoutMs
|
|
165
|
+
const timeoutMs = eventConfig.timeoutMs ?? defaultTimeoutMs;
|
|
166
|
+
expect(timeoutMs).toBe(30000);
|
|
167
|
+
});
|
|
168
|
+
it('should use event-specific timeout when specified', () => {
|
|
169
|
+
const defaultTimeoutMs = 30000;
|
|
170
|
+
const eventConfig = { timeoutMs: 60000 };
|
|
171
|
+
const timeoutMs = eventConfig.timeoutMs ?? defaultTimeoutMs;
|
|
172
|
+
expect(timeoutMs).toBe(60000);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
describe('event log formatting', () => {
|
|
176
|
+
it('should format matched event log entry', () => {
|
|
177
|
+
const entry = {
|
|
178
|
+
ts: new Date().toISOString(),
|
|
179
|
+
event: 'test-event',
|
|
180
|
+
matched: true,
|
|
181
|
+
exitCode: 0,
|
|
182
|
+
durationMs: 1234,
|
|
183
|
+
};
|
|
184
|
+
expect(entry.matched).toBe(true);
|
|
185
|
+
expect(entry.event).toBe('test-event');
|
|
186
|
+
expect(entry.exitCode).toBe(0);
|
|
187
|
+
expect(entry.durationMs).toBe(1234);
|
|
188
|
+
});
|
|
189
|
+
it('should format unmatched event log entry', () => {
|
|
190
|
+
const entry = {
|
|
191
|
+
ts: new Date().toISOString(),
|
|
192
|
+
event: null,
|
|
193
|
+
matched: false,
|
|
194
|
+
bodyPreview: '{"type":"unknown"}',
|
|
195
|
+
};
|
|
196
|
+
expect(entry.matched).toBe(false);
|
|
197
|
+
expect(entry.event).toBeNull();
|
|
198
|
+
expect(entry.bodyPreview).toBe('{"type":"unknown"}');
|
|
199
|
+
});
|
|
200
|
+
it('should truncate body preview to reasonable length', () => {
|
|
201
|
+
const longBody = { data: 'x'.repeat(300) };
|
|
202
|
+
const preview = JSON.stringify(longBody).slice(0, 200);
|
|
203
|
+
expect(preview.length).toBe(200);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Health check endpoint
|
|
3
|
+
*/
|
|
4
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
5
|
+
export const healthRoute = async (fastify) => {
|
|
6
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
7
|
+
fastify.get('/health', async () => {
|
|
8
|
+
return { ok: true, uptime: process.uptime() };
|
|
9
|
+
});
|
|
10
|
+
};
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Key generation and management endpoints (legacy API-key-header auth).
|
|
3
|
+
*
|
|
4
|
+
* These endpoints use X-API-Key header auth (raw seed) for machine-to-machine
|
|
5
|
+
* access. The SPA equivalents in api/sharing.ts use cookie/URL-key auth.
|
|
6
|
+
*/
|
|
7
|
+
import crypto from 'node:crypto';
|
|
8
|
+
import { findInsider, resolveInsiderKeyAuth, resolveSessionAuth, } from '../auth/resolve.js';
|
|
9
|
+
import { getConfig, resetConfig } from '../config/index.js';
|
|
10
|
+
import { appendEvent } from '../services/eventQueue.js';
|
|
11
|
+
import { computeInsiderKey, computeOutsiderKeyWithExpiry, computePathKey, timingSafeEqual, } from '../util/crypto.js';
|
|
12
|
+
import { setInsiderKey, setKeyRotationTimestamp } from '../util/state.js';
|
|
13
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
14
|
+
export const keysRoute = async (fastify) => {
|
|
15
|
+
// GET /key - Compute path-specific outsider key (requires raw API key seed in header)
|
|
16
|
+
fastify.get('/key', async (request, reply) => {
|
|
17
|
+
const provided = request.headers['x-api-key'];
|
|
18
|
+
const config = getConfig();
|
|
19
|
+
if (!provided) {
|
|
20
|
+
return reply.code(401).send({ error: 'X-API-Key header required' });
|
|
21
|
+
}
|
|
22
|
+
const matched = config.resolvedKeys.find((rk) => timingSafeEqual(provided, rk.seed));
|
|
23
|
+
if (!matched) {
|
|
24
|
+
return reply.code(401).send({ error: 'Invalid API key' });
|
|
25
|
+
}
|
|
26
|
+
const targetPath = request.query.path;
|
|
27
|
+
if (!targetPath) {
|
|
28
|
+
return reply.code(400).send({ error: 'path query param required' });
|
|
29
|
+
}
|
|
30
|
+
const key = computePathKey(matched.seed, targetPath);
|
|
31
|
+
return { path: targetPath, key };
|
|
32
|
+
});
|
|
33
|
+
// GET /insider-key - Generate insider key (requires raw API key seed in header)
|
|
34
|
+
fastify.get('/insider-key', async (request, reply) => {
|
|
35
|
+
const provided = request.headers['x-api-key'];
|
|
36
|
+
const config = getConfig();
|
|
37
|
+
if (!provided) {
|
|
38
|
+
return reply.code(401).send({ error: 'X-API-Key header required' });
|
|
39
|
+
}
|
|
40
|
+
const matched = config.resolvedKeys.find((rk) => timingSafeEqual(provided, rk.seed));
|
|
41
|
+
if (!matched) {
|
|
42
|
+
return reply.code(401).send({ error: 'Invalid API key' });
|
|
43
|
+
}
|
|
44
|
+
const key = computeInsiderKey(matched.seed);
|
|
45
|
+
return { key };
|
|
46
|
+
});
|
|
47
|
+
// POST /rotate-key - Rotate key (insider key or session cookie)
|
|
48
|
+
fastify.post('/rotate-key', async (request, reply) => {
|
|
49
|
+
const provided = request.query.key;
|
|
50
|
+
const config = getConfig();
|
|
51
|
+
if (provided) {
|
|
52
|
+
// Try insider key auth
|
|
53
|
+
const insiderResult = resolveInsiderKeyAuth(config, provided);
|
|
54
|
+
if (insiderResult.valid && insiderResult.email) {
|
|
55
|
+
// Insider key rotation
|
|
56
|
+
return rotateInsiderSeed(insiderResult.email, config);
|
|
57
|
+
}
|
|
58
|
+
// Machine key rotation is not supported with TS config
|
|
59
|
+
const matched = config.resolvedKeys.find((rk) => timingSafeEqual(provided, computeInsiderKey(rk.seed)));
|
|
60
|
+
if (matched) {
|
|
61
|
+
return reply.code(501).send({
|
|
62
|
+
error: 'Machine key rotation is not supported with TypeScript config. ' +
|
|
63
|
+
'Update the key manually in jeeves.config.ts and restart the server.',
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// Try session-based auth
|
|
68
|
+
const sessionResult = resolveSessionAuth(config, request);
|
|
69
|
+
if (sessionResult.valid && sessionResult.email) {
|
|
70
|
+
return rotateInsiderSeed(sessionResult.email, config);
|
|
71
|
+
}
|
|
72
|
+
return reply.code(401).send({ error: 'Invalid insider key' });
|
|
73
|
+
});
|
|
74
|
+
// GET /share - Generate outsider link (insider key or session cookie)
|
|
75
|
+
fastify.get('/share', async (request, reply) => {
|
|
76
|
+
const config = getConfig();
|
|
77
|
+
const targetPath = request.query.path;
|
|
78
|
+
if (!targetPath) {
|
|
79
|
+
return reply.code(400).send({ error: 'path query param required' });
|
|
80
|
+
}
|
|
81
|
+
// Try provided key
|
|
82
|
+
if (request.query.key) {
|
|
83
|
+
const insiderResult = resolveInsiderKeyAuth(config, request.query.key);
|
|
84
|
+
if (insiderResult.valid && insiderResult.seed) {
|
|
85
|
+
return buildShareResponse(insiderResult.seed, targetPath, request.query.exp);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// Try session cookie
|
|
89
|
+
const sessionResult = resolveSessionAuth(config, request);
|
|
90
|
+
if (sessionResult.valid && sessionResult.seed) {
|
|
91
|
+
return buildShareResponse(sessionResult.seed, targetPath, request.query.exp);
|
|
92
|
+
}
|
|
93
|
+
return reply.code(401).send({ error: 'Invalid insider key' });
|
|
94
|
+
});
|
|
95
|
+
};
|
|
96
|
+
function rotateInsiderSeed(email, config) {
|
|
97
|
+
const insider = findInsider(config.resolvedInsiders, email);
|
|
98
|
+
if (!insider?.seed)
|
|
99
|
+
return { ok: false, error: 'Insider not found' };
|
|
100
|
+
const rotatedSeed = crypto.randomBytes(32).toString('hex');
|
|
101
|
+
const timestamp = new Date().toISOString();
|
|
102
|
+
setInsiderKey(insider.email, rotatedSeed, timestamp);
|
|
103
|
+
appendEvent({
|
|
104
|
+
kind: 'insider_key_rotated',
|
|
105
|
+
email: insider.email,
|
|
106
|
+
at: timestamp,
|
|
107
|
+
});
|
|
108
|
+
setKeyRotationTimestamp(timestamp);
|
|
109
|
+
resetConfig();
|
|
110
|
+
return { ok: true, keyName: insider.email };
|
|
111
|
+
}
|
|
112
|
+
function buildShareResponse(seed, targetPath, expiry) {
|
|
113
|
+
let outsiderKey;
|
|
114
|
+
let shareUrl;
|
|
115
|
+
if (expiry) {
|
|
116
|
+
outsiderKey = computeOutsiderKeyWithExpiry(seed, targetPath, expiry);
|
|
117
|
+
shareUrl = `/browse${targetPath}?key=${outsiderKey}&exp=${expiry}`;
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
outsiderKey = computePathKey(seed, targetPath);
|
|
121
|
+
shareUrl = `/browse${targetPath}?key=${outsiderKey}`;
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
path: targetPath,
|
|
125
|
+
key: outsiderKey,
|
|
126
|
+
exp: expiry ?? null,
|
|
127
|
+
url: shareUrl,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Legacy /path redirect — sends all /path/* requests to /browse/*
|
|
3
|
+
*/
|
|
4
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
5
|
+
export const pathRoute = async (fastify) => {
|
|
6
|
+
// Redirect /path (root) to /browse
|
|
7
|
+
fastify.get('/path', async (_request, reply) => {
|
|
8
|
+
return reply.redirect('/browse');
|
|
9
|
+
});
|
|
10
|
+
// Redirect /path/* to /browse/*
|
|
11
|
+
fastify.get('/path/*', async (request, reply) => {
|
|
12
|
+
const reqPath = request.params['*'];
|
|
13
|
+
const url = new URL(request.url, 'http://localhost');
|
|
14
|
+
const query = url.search;
|
|
15
|
+
return reply.redirect(`/browse/${reqPath}${query}`);
|
|
16
|
+
});
|
|
17
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static asset routes — serves locally-bundled libraries from node_modules.
|
|
3
|
+
* Eliminates CDN dependencies for Lucide, Panzoom, and highlight.js.
|
|
4
|
+
*/
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
10
|
+
export const staticRoutes = async (fastify) => {
|
|
11
|
+
// robots.txt — block all crawlers
|
|
12
|
+
fastify.get('/robots.txt', async (_request, reply) => {
|
|
13
|
+
reply.type('text/plain').send('User-agent: *\nDisallow: /\n');
|
|
14
|
+
});
|
|
15
|
+
// Favicon
|
|
16
|
+
fastify.get('/favicon.svg', async (_request, reply) => {
|
|
17
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><text y="0.9em" font-size="90">🎩</text></svg>`;
|
|
18
|
+
reply.type('image/svg+xml').send(svg);
|
|
19
|
+
});
|
|
20
|
+
// Lucide icons
|
|
21
|
+
fastify.get('/static/lucide.min.js', async (_request, reply) => {
|
|
22
|
+
const filePath = path.join(__dirname, '..', 'node_modules', 'lucide', 'dist', 'umd', 'lucide.min.js');
|
|
23
|
+
return reply.type('application/javascript').send(fs.readFileSync(filePath));
|
|
24
|
+
});
|
|
25
|
+
// Panzoom
|
|
26
|
+
fastify.get('/static/panzoom.min.js', async (_request, reply) => {
|
|
27
|
+
const filePath = path.join(__dirname, '..', 'node_modules', '@panzoom', 'panzoom', 'dist', 'panzoom.min.js');
|
|
28
|
+
return reply.type('application/javascript').send(fs.readFileSync(filePath));
|
|
29
|
+
});
|
|
30
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Jeeves Server - Main entry point
|
|
3
|
+
* Fastify server for webhooks, file serving, and markdown rendering
|
|
4
|
+
*/
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import cookie from '@fastify/cookie';
|
|
9
|
+
import fastifyStatic from '@fastify/static';
|
|
10
|
+
import Fastify from 'fastify';
|
|
11
|
+
import { getConfig, initConfig, isConfigInitialized } from './config/index.js';
|
|
12
|
+
import { apiRoute } from './routes/api/index.js';
|
|
13
|
+
import { authRoute } from './routes/auth.js';
|
|
14
|
+
import { eventRoute } from './routes/event.js';
|
|
15
|
+
import { healthRoute } from './routes/health.js';
|
|
16
|
+
import { keysRoute } from './routes/keys.js';
|
|
17
|
+
import { pathRoute } from './routes/path/index.js';
|
|
18
|
+
import { staticRoutes } from './routes/static.js';
|
|
19
|
+
import { initDiagramCache } from './services/diagramCache.js';
|
|
20
|
+
import { startQueueProcessor } from './services/eventQueue.js';
|
|
21
|
+
import { initExportCache } from './services/exportCache.js';
|
|
22
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
23
|
+
async function start() {
|
|
24
|
+
try {
|
|
25
|
+
const config = isConfigInitialized() ? getConfig() : await initConfig();
|
|
26
|
+
const fastify = Fastify({
|
|
27
|
+
logger: true,
|
|
28
|
+
});
|
|
29
|
+
// Register plugins
|
|
30
|
+
await fastify.register(cookie);
|
|
31
|
+
// X-Robots-Tag on all responses — invisible to search engines
|
|
32
|
+
fastify.addHook('onSend', async (_request, reply) => {
|
|
33
|
+
void reply.header('X-Robots-Tag', 'noindex, nofollow');
|
|
34
|
+
});
|
|
35
|
+
// Register routes
|
|
36
|
+
await fastify.register(staticRoutes);
|
|
37
|
+
await fastify.register(healthRoute);
|
|
38
|
+
await fastify.register(authRoute);
|
|
39
|
+
await fastify.register(keysRoute);
|
|
40
|
+
await fastify.register(eventRoute);
|
|
41
|
+
await fastify.register(apiRoute);
|
|
42
|
+
await fastify.register(pathRoute);
|
|
43
|
+
// Serve React SPA (if built)
|
|
44
|
+
const clientDir = path.join(__dirname, '..', 'client');
|
|
45
|
+
if (fs.existsSync(clientDir)) {
|
|
46
|
+
await fastify.register(fastifyStatic, {
|
|
47
|
+
root: clientDir,
|
|
48
|
+
prefix: '/app/',
|
|
49
|
+
});
|
|
50
|
+
// SPA fallback for React routes
|
|
51
|
+
fastify.get('/', async (_request, reply) => {
|
|
52
|
+
return reply.sendFile('index.html', clientDir);
|
|
53
|
+
});
|
|
54
|
+
fastify.get('/browse', async (_request, reply) => {
|
|
55
|
+
return reply.sendFile('index.html', clientDir);
|
|
56
|
+
});
|
|
57
|
+
fastify.get('/browse/*', async (_request, reply) => {
|
|
58
|
+
return reply.sendFile('index.html', clientDir);
|
|
59
|
+
});
|
|
60
|
+
fastify.get('/runner', async (_request, reply) => {
|
|
61
|
+
return reply.sendFile('index.html', clientDir);
|
|
62
|
+
});
|
|
63
|
+
fastify.get('/runner/*', async (_request, reply) => {
|
|
64
|
+
return reply.sendFile('index.html', clientDir);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
// Initialize caches
|
|
68
|
+
initDiagramCache(config.diagramCachePath);
|
|
69
|
+
initExportCache();
|
|
70
|
+
// Start queue processor
|
|
71
|
+
startQueueProcessor();
|
|
72
|
+
await fastify.listen({ port: config.port, host: '0.0.0.0' });
|
|
73
|
+
console.log(`Jeeves server listening on port ${String(config.port)}`);
|
|
74
|
+
console.log(`Endpoints:`);
|
|
75
|
+
console.log(` GET /browse/* - File browser SPA`);
|
|
76
|
+
console.log(` GET /api/raw/* - Raw file serving`);
|
|
77
|
+
console.log(` GET /api/export/* - PDF/DOCX/ZIP export`);
|
|
78
|
+
console.log(` POST /event - Event Gateway (key auth)`);
|
|
79
|
+
console.log(` GET /key - Compute path key (X-API-Key auth)`);
|
|
80
|
+
console.log(` GET /health - Health check (no auth)`);
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
console.error('Fatal startup error:', err);
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
start().catch((err) => {
|
|
88
|
+
console.error('Unhandled startup error:', err);
|
|
89
|
+
process.exit(1);
|
|
90
|
+
});
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deep share link rewriting for outsider access with depth traversal.
|
|
3
|
+
*
|
|
4
|
+
* Rewrites outgoing links in rendered HTML with computed sub-keys,
|
|
5
|
+
* enabling outsiders to follow links up to N levels deep.
|
|
6
|
+
*/
|
|
7
|
+
import * as cheerio from 'cheerio';
|
|
8
|
+
import LZString from 'lz-string';
|
|
9
|
+
import { computeDeepShareKey } from '../util/crypto.js';
|
|
10
|
+
/**
|
|
11
|
+
* Parse a compressed stack string into an array of paths.
|
|
12
|
+
*/
|
|
13
|
+
export function decodeStack(compressed) {
|
|
14
|
+
if (!compressed)
|
|
15
|
+
return [];
|
|
16
|
+
const json = LZString.decompressFromEncodedURIComponent(compressed);
|
|
17
|
+
if (!json)
|
|
18
|
+
return [];
|
|
19
|
+
try {
|
|
20
|
+
const parsed = JSON.parse(json);
|
|
21
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Encode a path stack array into a compressed string.
|
|
29
|
+
*/
|
|
30
|
+
export function encodeStack(stack) {
|
|
31
|
+
return LZString.compressToEncodedURIComponent(JSON.stringify(stack));
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Compute the remaining depth for a given stack.
|
|
35
|
+
*/
|
|
36
|
+
export function remainingDepth(maxDepth, stack) {
|
|
37
|
+
return maxDepth - (stack.length - 1);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Compute a sub-link URL for an outgoing link target.
|
|
41
|
+
* Returns null if the link should be stripped (depth exhausted or type not allowed).
|
|
42
|
+
*/
|
|
43
|
+
export function computeSubLink(seed, targetUrlPath, currentStack, maxDepth, dirs, exp, isDirectory) {
|
|
44
|
+
// Check if directories are allowed
|
|
45
|
+
if (isDirectory && !dirs)
|
|
46
|
+
return null;
|
|
47
|
+
// Compute new stack
|
|
48
|
+
const existingIndex = currentStack.indexOf(targetUrlPath);
|
|
49
|
+
let newStack;
|
|
50
|
+
if (existingIndex >= 0) {
|
|
51
|
+
// Revisiting — truncate stack to that point
|
|
52
|
+
newStack = currentStack.slice(0, existingIndex + 1);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
// New page — append
|
|
56
|
+
newStack = [...currentStack, targetUrlPath];
|
|
57
|
+
}
|
|
58
|
+
// Check remaining depth
|
|
59
|
+
const remaining = remainingDepth(maxDepth, newStack);
|
|
60
|
+
if (remaining < 0)
|
|
61
|
+
return null;
|
|
62
|
+
const compressedStack = encodeStack(newStack);
|
|
63
|
+
// Compute key for the target
|
|
64
|
+
const params = {
|
|
65
|
+
depth: maxDepth,
|
|
66
|
+
dirs,
|
|
67
|
+
stack: compressedStack,
|
|
68
|
+
exp,
|
|
69
|
+
};
|
|
70
|
+
const key = computeDeepShareKey(seed, targetUrlPath, params);
|
|
71
|
+
// Build URL
|
|
72
|
+
let url = `/browse${targetUrlPath}?key=${key}&d=${String(maxDepth)}&dirs=${dirs ? '1' : '0'}&s=${compressedStack}`;
|
|
73
|
+
if (exp)
|
|
74
|
+
url += `&exp=${exp}`;
|
|
75
|
+
return url;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Rewrite outgoing links in rendered HTML for deep share access.
|
|
79
|
+
*
|
|
80
|
+
* For outsiders with depth \> 0:
|
|
81
|
+
* - Internal links get rewritten with sub-keys
|
|
82
|
+
* - Links beyond depth get stripped (text preserved, link removed)
|
|
83
|
+
* - External links (http/https) are left unchanged
|
|
84
|
+
* - Anchor links (#) are left unchanged
|
|
85
|
+
*/
|
|
86
|
+
export function rewriteLinksForDeepShare(html, seed, currentPath, maxDepth, dirs, stackCompressed, exp) {
|
|
87
|
+
const currentStack = decodeStack(stackCompressed);
|
|
88
|
+
// Ensure current path is in the stack
|
|
89
|
+
if (currentStack.length === 0 ||
|
|
90
|
+
currentStack[currentStack.length - 1] !== currentPath) {
|
|
91
|
+
currentStack.push(currentPath);
|
|
92
|
+
}
|
|
93
|
+
const remaining = remainingDepth(maxDepth, currentStack);
|
|
94
|
+
const $ = cheerio.load(html, null, false);
|
|
95
|
+
// Rewrite <a> tags
|
|
96
|
+
$('a').each((_i, el) => {
|
|
97
|
+
const $el = $(el);
|
|
98
|
+
const href = $el.attr('href');
|
|
99
|
+
if (!href)
|
|
100
|
+
return;
|
|
101
|
+
// Skip external links, anchors, and data URLs
|
|
102
|
+
if (href.startsWith('http://') ||
|
|
103
|
+
href.startsWith('https://') ||
|
|
104
|
+
href.startsWith('#') ||
|
|
105
|
+
href.startsWith('//') ||
|
|
106
|
+
href.startsWith('data:') ||
|
|
107
|
+
href.startsWith('mailto:')) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
// If no remaining depth, strip the link (keep text)
|
|
111
|
+
if (remaining <= 0) {
|
|
112
|
+
$el.replaceWith($el.html() ?? '');
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
// Raw file links — leave as-is (these are for images/downloads)
|
|
116
|
+
if (href.startsWith('/api/raw/'))
|
|
117
|
+
return;
|
|
118
|
+
// Determine target path
|
|
119
|
+
let targetPath;
|
|
120
|
+
if (href.startsWith('/browse/')) {
|
|
121
|
+
targetPath = '/' + href.replace('/browse/', '').split('?')[0];
|
|
122
|
+
}
|
|
123
|
+
else if (href.startsWith('/')) {
|
|
124
|
+
targetPath = href.split('?')[0];
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
// Relative link — resolve against current path directory
|
|
128
|
+
const dir = currentPath.substring(0, currentPath.lastIndexOf('/'));
|
|
129
|
+
targetPath = dir
|
|
130
|
+
? `${dir}/${href.split('?')[0]}`
|
|
131
|
+
: `/${href.split('?')[0]}`;
|
|
132
|
+
}
|
|
133
|
+
// Normalize
|
|
134
|
+
targetPath = targetPath.replace(/\/+/g, '/');
|
|
135
|
+
const isDirectory = targetPath.endsWith('/');
|
|
136
|
+
const subLink = computeSubLink(seed, targetPath, currentStack, maxDepth, dirs, exp, isDirectory);
|
|
137
|
+
if (subLink === null) {
|
|
138
|
+
// Strip link, keep text
|
|
139
|
+
$el.replaceWith($el.html() ?? '');
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
$el.attr('href', subLink);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
// Rewrite <img> src for images that use /api/raw/ — add key auth
|
|
146
|
+
$('img').each((_i, el) => {
|
|
147
|
+
const $el = $(el);
|
|
148
|
+
const src = $el.attr('src');
|
|
149
|
+
if (!src || !src.startsWith('/api/raw/'))
|
|
150
|
+
return;
|
|
151
|
+
const params = {
|
|
152
|
+
depth: maxDepth,
|
|
153
|
+
dirs,
|
|
154
|
+
stack: stackCompressed,
|
|
155
|
+
exp,
|
|
156
|
+
};
|
|
157
|
+
const rawPath = '/' + src.replace('/api/raw/', '').split('?')[0];
|
|
158
|
+
const key = computeDeepShareKey(seed, rawPath, params);
|
|
159
|
+
const authSrc = `${src}${src.includes('?') ? '&' : '?'}key=${key}&d=${String(maxDepth)}&dirs=${dirs ? '1' : '0'}&s=${stackCompressed}${exp ? `&exp=${exp}` : ''}`;
|
|
160
|
+
$el.attr('src', authSrc);
|
|
161
|
+
});
|
|
162
|
+
return $.html();
|
|
163
|
+
}
|