This commit is contained in:
Abdul Wahab
2026-09-01 10:04:59 +05:00
parent 6cfeb7d374
commit 8b6bbaef3d
7 changed files with 439 additions and 5 deletions
+45
View File
@@ -36,10 +36,19 @@ const answersPath = path.join(interviewDir, 'answers.json');
const sessionPath = path.join(interviewDir, 'doc-session.json');
const ledgerPath = path.join(interviewDir, 'doc-edits.jsonl');
const fontsDir = path.join(interviewDir, 'fonts');
const brandAssetsDir = path.join(interviewDir, 'assets');
const designPath = path.resolve(process.cwd(), 'DESIGN.md');
const MAX_BODY_BYTES = 1024 * 1024;
const FONT_EXTENSIONS = new Set(['.woff2', '.woff', '.ttf', '.otf']);
const BRAND_ASSET_MIME = new Map([
['.svg', 'image/svg+xml'],
['.png', 'image/png'],
['.jpg', 'image/jpeg'],
['.jpeg', 'image/jpeg'],
['.webp', 'image/webp'],
['.gif', 'image/gif'],
]);
const ROLES = new Set(['primary', 'secondary', 'tertiary', 'neutral']);
const REQUEST_KINDS = new Set(['font', 'freeform']);
/* Long polls are sliced under common proxy/undici header timeouts, the same
@@ -251,6 +260,42 @@ async function handleRequest(request, response) {
return;
}
/* Brand-asset images for the document's Brand article. The picker server
serves the same directory while it lives; it exits on submit, and the
article's images load after that, so the tab fetches them from here
with the session token on the query string, the same rule as the
sibling GET routes. Filenames only, extension-gated, one directory. */
if (request.method === 'GET' && requestPath.startsWith('/brand-assets/')) {
if (url.searchParams.get('token') !== token) throw httpError(403, 'Bad token');
let assetName;
try {
assetName = decodeURIComponent(requestPath.slice('/brand-assets/'.length));
} catch {
throw httpError(400, 'Invalid path');
}
const extension = path.extname(assetName).toLowerCase();
const filePath = path.resolve(brandAssetsDir, assetName);
if (!assetName || assetName !== path.basename(assetName)
|| !BRAND_ASSET_MIME.has(extension)
|| path.relative(brandAssetsDir, filePath).startsWith('..')) {
throw httpError(404, 'Not found');
}
let body;
try {
body = await readFile(filePath);
} catch {
throw httpError(404, 'Not found');
}
response.writeHead(200, {
'Content-Type': BRAND_ASSET_MIME.get(extension),
'Content-Length': body.length,
'Access-Control-Allow-Origin': '*',
'Cache-Control': 'max-age=86400',
});
response.end(body);
return;
}
if (request.method === 'GET' && requestPath === '/doc/state') {
if (url.searchParams.get('token') !== token) throw httpError(403, 'Bad token');
lastBrowserSeen = Date.now();
+24
View File
@@ -17,13 +17,18 @@ const scriptDir = path.dirname(fileURLToPath(import.meta.url));
const pickerDir = path.join(scriptDir, 'picker');
const answersPath = path.resolve(process.cwd(), '.impeccable/design-interview/answers.json');
const fontsDir = path.resolve(process.cwd(), '.impeccable/design-interview/fonts');
const brandAssetsDir = path.resolve(process.cwd(), '.impeccable/design-interview/assets');
const MAX_BODY_BYTES = 1024 * 1024;
const FONT_EXTENSIONS = new Set(['.woff2', '.woff', '.ttf', '.otf']);
const BRAND_ASSET_EXTENSIONS = ['.svg', '.png', '.jpg', '.jpeg', '.webp', '.gif'];
const MIME = new Map([
['.html', 'text/html; charset=utf-8'],
['.css', 'text/css; charset=utf-8'],
['.js', 'text/javascript; charset=utf-8'],
['.jpg', 'image/jpeg'],
['.jpeg', 'image/jpeg'],
['.webp', 'image/webp'],
['.gif', 'image/gif'],
['.png', 'image/png'],
['.svg', 'image/svg+xml'],
['.json', 'application/json; charset=utf-8'],
@@ -267,9 +272,28 @@ async function handleRequest(request, response) {
sendJson(response, 404, { error: 'Not found' });
return;
}
// Cue images are re-requested by the design context document after this
// process has exited (article content only enters the live DOM after
// submit), so they must be servable from the browser's cache.
response.setHeader('Cache-Control', 'max-age=86400');
await serveFile(response, options.cuesDir, cueName, ['.png']);
return;
}
/* Brand-asset files the agent staged from the chat interview (logos, mood
boards, reference images), displayed by the design context document.
Read-only, one directory, filenames only. The /assets/ prefix is taken
by the picker's own static files, hence the distinct name. */
if (requestPath.startsWith('/brand-assets/')) {
const assetName = requestPath.slice('/brand-assets/'.length);
if (!assetName || assetName.includes('/')) {
sendJson(response, 404, { error: 'Not found' });
return;
}
response.setHeader('Cache-Control', 'max-age=86400');
await serveFile(response, brandAssetsDir, assetName, BRAND_ASSET_EXTENSIONS);
return;
}
// Uploaded faces are read back so the specimen can render in them.
if (requestPath.startsWith('/fonts/')) {
const fontName = requestPath.slice('/fonts/'.length);