mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-13 06:36:26 +03:00
update
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user