Skip to content

Commit 5cb84bc

Browse files
phodalQoder-AI
andcommitted
fix(studio-apps): gate a disabled app's ui bundle behind the same switch
The enablement predicate already decides what /api/apps reports and what the proxy forwards, but the static tier served /apps/<name>/ui/* for any installed app. A disabled app's entry could still be imported and mounted by any page, so the switch the app list offers only held for half the app. createStaticHandler now takes the same isAppEnabled predicate and answers the disabled case with the 404 an uninstalled app already gets, so the static tier discloses no more than the missing app does. Verified with a live host: a disabled echo-app fixture answers 404 where an enabled and a default-enabled one answer 200; the package test suite stays green. Co-authored-by: QoderAI (Qwen 3.8 Max) <qoder_ai@qoder.com>
1 parent 6ba02e0 commit 5cb84bc

2 files changed

Lines changed: 23 additions & 2 deletions

File tree

‎packages/harness-studio-apps/lib/static.mjs‎

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010
* GET /app-assets/* -> dist/app-assets/* (store art, app icons)
1111
* GET /vendor/* -> dist/vendor/* (vendored frontend deps)
1212
* GET /apps/<name>/ui/<entry> -> installed apps/<name>/ui/<entry>, else the
13-
* builtin's own ui/ dir (federated app UI ESM)
13+
* builtin's own ui/ dir (app UI ESM). Gated
14+
* on enablement: a disabled app's bundle is
15+
* 404, like one that is not installed.
1416
* GET /<anything-else> -> dist/<path> when it exists, otherwise
1517
* dist/index.html (SPA fallback, injected)
1618
*
@@ -118,7 +120,16 @@ function contained(root, candidate) {
118120
return rel === root || rel.startsWith(root + sep)
119121
}
120122

121-
export function createStaticHandler({ distDir, apps, installedAppsDir, appRoutes = [] }) {
123+
export function createStaticHandler({
124+
distDir,
125+
apps,
126+
installedAppsDir,
127+
appRoutes = [],
128+
// The same predicate the proxy and the host API read, so what `/api/apps`
129+
// reports, what the proxy forwards and what this tier serves cannot diverge.
130+
// Defaulting to "everything is on" keeps the handler usable standalone.
131+
isAppEnabled = () => true,
132+
}) {
122133
const root = distDir ? resolve(distDir) : null
123134

124135
/** First existing candidate under the dist root, or null. */
@@ -202,6 +213,15 @@ export function createStaticHandler({ distDir, apps, installedAppsDir, appRoutes
202213
sendJson(res, 400, { error: 'invalid path' })
203214
return true
204215
}
216+
// Enablement gates the BUNDLE, not only the API. Serving a disabled
217+
// app's entry let any page import and mount it, so the switch the app
218+
// list offers only held for half the app. The refusal is the same 404 an
219+
// app that is not installed gets: whether a disabled app exists is not
220+
// this tier's to disclose.
221+
if (!isAppEnabled(name)) {
222+
sendJson(res, 404, { error: 'not found' })
223+
return true
224+
}
205225
const ext = extname(entry).toLowerCase()
206226
if (!UI_ALLOWED_EXTENSIONS.has(ext)) {
207227
sendJson(res, 403, { error: `file type '${ext}' not allowed` })

‎packages/harness-studio-apps/server.mjs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ async function main() {
151151
apps,
152152
installedAppsDir: join(config.homeDir, 'apps'),
153153
appRoutes,
154+
isAppEnabled,
154155
})
155156
const hostApi = createHostApi({ apps, state, backends, config, logger, isAppEnabled })
156157
const proxy = createAppProxy({

0 commit comments

Comments
 (0)