feat(web): gesture-control overlay integration (Phase 5, opt-in via CODEMAN_GESTURE=1)

Loads a hand-tracking overlay into the dashboard that detaches a session by
pinch-grabbing its tab and pulling it out — driving the existing
app.detachSession(id) hook. Bundle (src/web/public/gesture/gesture-codeman.js)
is built from the codeman-gesture-control project's src/codeman/entry.ts
(esbuild, MediaPipe included) and served same-origin.

OFF by default — guarded entirely by CODEMAN_GESTURE=1:
- server.ts: injects the module script into the dashboard HTML only (not solo
  /session/:id popups, which have no tab strip).
- auth.ts: widens CSP only under the flag — adds 'wasm-unsafe-eval' (MediaPipe
  WASM) and the pinned MediaPipe CDNs (cdn.jsdelivr.net wasm, storage.googleapis.com
  model) to connect-src, plus worker-src 'self' blob:. Production CSP is unchanged
  when the flag is off.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ark0N
2026-06-07 03:15:29 +02:00
co-authored by Claude Opus 4.8
parent 2e341e3897
commit afea6d6a1c
3 changed files with 4631 additions and 4 deletions
+13 -4
View File
@@ -154,13 +154,22 @@ export function registerAuthMiddleware(app: FastifyInstance, https: boolean): Au
* Register security headers and CORS middleware on every response.
*/
export function registerSecurityHeaders(app: FastifyInstance, https: boolean): void {
// Gesture-control overlay (opt-in via CODEMAN_GESTURE=1) runs MediaPipe, which
// needs WebAssembly eval and must fetch its wasm/model from the pinned CDNs.
// Computed once; OFF by default so the production CSP is byte-for-byte unchanged.
const gesture = process.env.CODEMAN_GESTURE === '1';
const scriptSrc = "script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net" + (gesture ? " 'wasm-unsafe-eval'" : '');
const connectSrc =
"connect-src 'self' wss://api.deepgram.com" + (gesture ? ' https://cdn.jsdelivr.net https://storage.googleapis.com' : '');
const workerSrc = gesture ? "; worker-src 'self' blob:" : '';
const csp =
`default-src 'self'; ${scriptSrc}; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; ` +
`img-src 'self' data: blob:; ${connectSrc}; font-src 'self' https://cdn.jsdelivr.net; frame-ancestors 'self'${workerSrc}`;
app.addHook('onRequest', (req, reply, done) => {
reply.header('X-Content-Type-Options', 'nosniff');
reply.header('X-Frame-Options', 'SAMEORIGIN');
reply.header(
'Content-Security-Policy',
"default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; img-src 'self' data: blob:; connect-src 'self' wss://api.deepgram.com; font-src 'self' https://cdn.jsdelivr.net; frame-ancestors 'self'"
);
reply.header('Content-Security-Policy', csp);
if (https) {
reply.header('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
}
File diff suppressed because one or more lines are too long
+7
View File
@@ -1007,6 +1007,13 @@ export class WebServer extends EventEmitter {
const safeId = JSON.stringify(soloSessionId).replace(/</g, '\\u003c');
html = html.replace('</head>', `<script>window.__CODEMAN_SOLO__=${safeId};</script>\n</head>`);
}
// Gesture-control overlay (Phase 5): dashboard only (not solo popups, which
// have no tab strip), opt-in via CODEMAN_GESTURE=1. The bundle is served
// same-origin from /gesture/ so 'self' covers it; CSP is widened to match in
// registerSecurityHeaders under the same flag.
if (!soloSessionId && process.env.CODEMAN_GESTURE === '1') {
html = html.replace('</head>', `<script type="module" src="/gesture/gesture-codeman.js"></script>\n</head>`);
}
return html;
}