fix(plugins): serve frame assets root-relative and cross-origin loadable

res.sendFile(absolutePath) resolves against the rewritten req.url under
the Nest ExpressAdapter and 404s spuriously (files-download already
works around the same trap), which broke every plugin frame document.
And helmet's CORP: same-origin made the browser drop the opaque-origin
frame's own script/style subresources, so a multi-file plugin client
could never boot. Serve root-relative and mark frame responses
cross-origin — sandbox + per-plugin CSP stay the isolation boundary.
This commit is contained in:
Maurice
2026-07-14 09:57:57 +02:00
parent 6c87bf2f7a
commit 4fad7f04a9
2 changed files with 34 additions and 2 deletions
@@ -61,8 +61,18 @@ export class PluginFrameController {
const ext = path.extname(resolved).toLowerCase();
res.setHeader('Content-Type', MIME[ext] ?? 'application/octet-stream');
res.setHeader('X-Content-Type-Options', 'nosniff');
// The frame document runs at an OPAQUE origin (sandbox without
// allow-same-origin), so its own <script src>/<link> subresource loads are
// cross-origin — helmet's default CORP: same-origin makes the browser drop
// them (ERR_BLOCKED_BY_RESPONSE) and a multi-file client dies on boot.
// The sandbox + per-plugin CSP are the isolation boundary here, not CORP:
// these files are the plugin's own public client code.
res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin');
res.setHeader('Content-Security-Policy', this.frameCsp(pluginId, req.get('host')));
res.sendFile(resolved);
// Explicit { root } + basename, not the absolute path: under the Nest
// ExpressAdapter, res.sendFile(absolutePath) resolves against the rewritten
// req.url and 404s spuriously (same trap as files-download.controller.ts).
res.sendFile(path.basename(resolved), { root: path.dirname(resolved) });
}
/** Per-plugin, locked-down CSP for the sandboxed frame document. */