spicebook/frontend/src/middleware.ts
Ryan Malloy 3f3ca58521 Add embeddable notebook viewer for Mims library integration
New /embed/[id] route renders notebooks in a read-only, chromeless
layout for iframe embedding. Supports light/dark themes via URL
param and postMessage from the parent window.

- EmbedLayout: minimal HTML shell, no navbar/footer
- EmbedViewer: fetches notebook, runs simulations, syncs theme
- EmbedCell: read-only markdown + SPICE cell renderer
- SpiceEditor: added readOnly prop (EditorState.readOnly + editable.of)
- embed-theme.css: light mode CSS variable overrides
- Astro middleware: CSP frame-ancestors on /embed/* routes
- Backend: env-configurable CORS origins, CSP header middleware

Security hardening from review:
- postMessage origin validation (ALLOWED_MESSAGE_ORIGINS)
- markdown XSS fix: isSafeUrl() blocks javascript: URIs in links
- escapeHtml now covers single quotes
- Notebook ID validated against /^[a-zA-Z0-9_-]+$/
- Theme param normalized at Astro boundary
- classList.remove/add instead of className stomping
2026-02-13 15:46:37 -07:00

25 lines
718 B
TypeScript

import { defineMiddleware } from 'astro:middleware';
// CSP frame-ancestors: controls which origins can embed this site in an iframe.
// Only applied to /embed/* routes (the main app doesn't need to be framed).
const FRAME_ANCESTORS = "'self' https://forrest.warehack.ing";
export const onRequest = defineMiddleware(async ({ url }, next) => {
const response = await next();
if (url.pathname.startsWith('/embed/')) {
response.headers.set(
'Content-Security-Policy',
`frame-ancestors ${FRAME_ANCESTORS}`,
);
} else {
// Prevent framing of the main app entirely
response.headers.set(
'Content-Security-Policy',
"frame-ancestors 'self'",
);
}
return response;
});