Repository navigation
Expand file tree
/
Copy pathserverMode.js
More file actions
205 lines (189 loc) · 9.24 KB
/
Copy pathserverMode.js
File metadata and controls
205 lines (189 loc) · 9.24 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
// Route classification for the two run modes.
//
// 'local' — a desktop install: every route is mounted (today's behaviour).
// 'server' — the Docker deployment: only the shared *data* routes exist. It has
// no GPU, no ComfyUI, no Python sidecars and no per-user secrets, so
// the compute and machine-local routes are not merely unused there,
// they are meaningless and must not answer.
//
// A local install that is pointed at a remote server uses BOTH lists: the
// local-only routes it serves itself, and the remote-data routes it forwards
// (see gateway.js). The two sets are disjoint and between them cover every
// route, which is what makes a single origin work for the frontend.
//
// This is expressed as path tests rather than by moving the 129 route
// registrations out of server.js. Those registrations are interleaved with
// ~9000 lines of helpers; relocating them is the highest-risk change available
// and buys nothing that a path test does not.
// Whole subtrees that belong to the local machine.
const LOCAL_ONLY_PREFIXES = [
'/api/comfyui', // workflow execution (WebSocket to a local ComfyUI)
'/api/image-edits', // ComfyUI + vendor-API image editing
'/api/meshes', // every mesh op: generate, edit, texture, rig, bake, LODs...
'/api/mocap', // video-to-motion sidecar (:8401)
'/api/setup', // first-run wizard; downloads models onto the local disk
'/api/filesystem', // browses the *server's* disk — nonsense when remote
'/api/logs', // tails log files the desktop shell owns
'/api/remote', // this install's own remote-server connection settings
'/mcp', // MCP clients drive a user's own app, not the shared server
'/resources' // bundled reference animation library (mesh editor only)
];
// Individual routes, where a sibling under the same prefix IS shared data.
// /api/motions/library* is the shared motion catalogue and must stay reachable,
// so /api/motions cannot be blanket-listed above.
const LOCAL_ONLY_EXACT = new Set([
'/api/images/generate',
'/api/motions/generate',
'/api/motions/skeleton',
'/api/motions/health',
'/api/settings', // ComfyUI URL, service ports and third-party API keys
'/api/system/stats', // reports the container's hardware, not the user's
'/api/export/mesh' // writes files to a path on the local filesystem
]);
// Routes that live under a shared-data prefix but must NOT be forwarded, because
// they read or write the *user's own filesystem* while their data lives on the
// shared server. The local handler answers them and pulls what it needs through
// dataStore instead.
//
// Project export writes a .3dgp bundle into a folder the user picked with a
// native folder picker, and import reads one back. Forwarded, the destination
// path was interpreted on the container: "C:\Travaux" is not absolute on Linux,
// so export failed with "The destination folder must be an absolute path" — and
// had it been a POSIX-looking path it would have silently written inside the
// container instead.
//
// These are prefixes, so /api/projects/import/files (the staged-ingest sibling
// the local side posts to directly) is covered too.
const LOCAL_EXECUTION_DATA_PREFIXES = [
'/api/projects/import'
];
// Same idea, but the path carries an id: /api/projects/42/export, and
// /api/assets/42/vfx-export, /api/assets/42/building-export.
//
// NOTE THE PAIRING IN BOTH CASES. The route that WRITES a bundle to the user's
// disk is listed here and runs locally; its sibling that only builds the PLAN
// (/export-plan, /vfx-export-plan) is deliberately absent, because that half
// has to run where the database is. This classifier sees the path and not the
// method, which is exactly why the two halves have different paths rather than
// being a GET and a POST on one.
const LOCAL_EXECUTION_DATA_PATTERNS = [
/^\/api\/projects\/[^/]+\/export$/,
/^\/api\/assets\/[^/]+\/vfx-export$/,
/^\/api\/assets\/[^/]+\/building-export$/
];
// The subdirectories of data/assets, as URL prefixes.
//
// Load-bearing in TWO places, because the URL prefix /assets serves two
// completely unrelated things:
//
// /assets/images/1787-42.png -> data/assets/images/... (a user asset)
// /assets/index-DJ9Bu3Jb.js -> dist/assets/... (the app's own bundle)
//
// Vite emits its build output flat under dist/assets and express.static serves
// it on the same prefix, so anything that treats '/assets' as one thing gets one
// of the two wrong — and it has already happened twice:
//
// * the gateway forwarded all of /assets, sending the frontend's own
// JavaScript to the shared server (different build — 404, blank window);
// * the auth gate protected all of /assets, answering 401 for that same
// JavaScript. That one is worse than it looks: the login form IS the
// JavaScript, so it was a deadlock with no way in.
//
// User assets always sit in one of these subdirectories (see storage.js) and
// Vite's output never does, so the split is unambiguous. Adding a new asset
// subdirectory means adding it here.
export const USER_ASSET_PREFIXES = [
'/assets/images',
'/assets/meshes',
'/assets/thumbnails',
'/assets/workflows',
'/assets/brushes',
'/assets/paintdocs',
'/assets/wiki',
'/assets/motions',
'/assets/animations',
'/assets/trees',
'/assets/vfx',
'/assets/buildings'
];
// The note above says to add each new subdirectory here, and twice it was not
// done -- animations and tree presets both shipped missing, which in remote mode
// looks like a 404 from the LOCAL server for a file that only ever existed on the
// shared one. Nothing about the feature hints that this file is involved, so the
// rule is checked at startup instead of trusted to memory. Directories are made
// on first write, so a name only shows up once the feature has been used; that is
// still far earlier than a user reporting it.
export function findUncoveredAssetDirectories(names) {
const covered = new Set(USER_ASSET_PREFIXES.map(prefix => prefix.replace('/assets/', '')));
return Array.from(names || []).filter(name => !covered.has(name));
}
// Everything the shared server owns. Forwarded verbatim by the gateway when a
// remote is configured. /api/health is deliberately absent: a local install
// answers for its own liveness, not the remote's.
const REMOTE_DATA_PREFIXES = [
'/api/events', // SSE mutation bus
'/api/projects',
'/api/assets',
'/api/cards',
'/api/card-attributes',
'/api/graph',
'/api/boards',
'/api/wiki',
'/api/vfx/presets', // the shared starter-effect library, like the wiki
'/api/vfx/preset-assets', // and the sprites/meshes those presets draw with
'/api/tasks',
'/api/mesh-assemblies', // global assembly documents; hyphenated so it is NOT
// caught by the '/api/meshes' local-only prefix above
'/api/motions/library', // the shared clip catalogue (not /generate)
'/api/animations/library', // hand-edited clips, shared like the motions
'/api/library/comfy-workflows', // workflow DEFINITIONS are shared; execution is local
'/api/auth', // login/logout/me on the shared server
'/api/users',
'/wiki-media',
// Asset bytes. See USER_ASSET_PREFIXES above for why these are listed one by
// one and not as a blanket '/assets'.
...USER_ASSET_PREFIXES
];
function matchesPrefix(pathname, prefixes) {
return prefixes.some(prefix => pathname === prefix || pathname.startsWith(`${prefix}/`));
}
function normalize(pathname) {
return String(pathname || '').replace(/\/+$/, '') || '/';
}
export function isLocalOnlyPath(pathname) {
const normalized = normalize(pathname);
if (LOCAL_ONLY_EXACT.has(normalized)) return true;
return matchesPrefix(normalized, LOCAL_ONLY_PREFIXES);
}
// Asset BYTES, as opposed to the frontend bundle that shares the prefix.
export function isUserAssetPath(pathname) {
return matchesPrefix(normalize(pathname), USER_ASSET_PREFIXES);
}
export function isLocalExecutionDataPath(pathname) {
const normalized = normalize(pathname);
if (matchesPrefix(normalized, LOCAL_EXECUTION_DATA_PREFIXES)) return true;
return LOCAL_EXECUTION_DATA_PATTERNS.some(pattern => pattern.test(normalized));
}
export function isRemoteDataPath(pathname) {
const normalized = normalize(pathname);
// A local-only route always wins, so /api/motions/generate cannot be dragged
// remote by the /api/motions/library prefix and /api/settings stays local.
if (isLocalOnlyPath(normalized)) return false;
// Likewise a route that runs here but reads the shared data itself.
if (isLocalExecutionDataPath(normalized)) return false;
return matchesPrefix(normalized, REMOTE_DATA_PREFIXES);
}
// Mount as early as possible — before the body parsers, the auth gate and the
// static mounts. A local-only path should read as absent regardless of whether
// the caller is authenticated, so this deliberately runs ahead of the gate.
export function mountLocalOnlyGuard(app, { mode }) {
if (mode !== 'server') return;
app.use((req, res, next) => {
if (!isLocalOnlyPath(req.path)) return next();
res.status(404).json({
error: 'This endpoint runs on your local 3D Gen Studio installation, not on the shared server.',
path: req.path,
mode: 'server'
});
});
}