From 9f4c7588f553313e5a9a7aaa724957a723536862 Mon Sep 17 00:00:00 2001 From: jayforgex <15557956+jayforgex@users.noreply.github.com> Date: Fri, 16 Oct 2026 13:48:14 +0800 Subject: [PATCH] fix(server): serve bundled preset avatars on SQLite host --- server/app.mjs | 6 ++++-- tests/server.test.mjs | 8 ++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/server/app.mjs b/server/app.mjs index 79341ba..004e18e 100644 --- a/server/app.mjs +++ b/server/app.mjs @@ -22,8 +22,10 @@ export async function createApplication({ worker, databasePath, setupSecret, pub if (!['GET', 'HEAD'].includes(request.method)) return new Response('Method not allowed.', { status: 405 }); const pathname = new URL(request.url).pathname; const name = pathname === '/' ? 'index.html' : pathname.slice(1); - // Serve only top-level public assets, never directories or project/configuration files. - if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(name)) return new Response('Not found.', { status: 404 }); + // Serve top-level public assets and known preset avatars, never arbitrary directories or project/configuration files. + const topLevelAsset = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(name); + const presetAvatar = /^avatars\/(?:dots|grok|muse)\.svg$/.test(name); + if (!topLevelAsset && !presetAvatar) return new Response('Not found.', { status: 404 }); const filename = name.includes('.') ? name : name + '.html'; const extension = filename.slice(filename.lastIndexOf('.')); if (!types[extension]) return new Response('Not found.', { status: 404 }); try { const bytes = await readFile(join(assetsDirectory, filename)); return new Response(request.method === 'HEAD' ? null : bytes, { headers: { 'Content-Type': types[extension], 'Cache-Control': 'no-cache' } }); } diff --git a/tests/server.test.mjs b/tests/server.test.mjs index 4fd9cd0..65b3b8e 100644 --- a/tests/server.test.mjs +++ b/tests/server.test.mjs @@ -32,6 +32,14 @@ test('server persists messages, acknowledgments and keys across restart; HTTP pr const oversized = await call('/api/v1/messages', { method: 'POST', key, body: { content: 'a'.repeat(70000) } }); assert.equal(oversized.status, 413); for (const path of ['/.server.env', '/store.sqlite', '/server/start.mjs', '/%2e%2e/.server.env']) assert.equal((await fetch(`http://127.0.0.1:${app.server.address().port}${path}`)).status, 404); assert.equal((await fetch(`http://127.0.0.1:${app.server.address().port}/`)).status, 200); + for (const name of ['dots', 'grok', 'muse']) { + const avatar = await fetch(`http://127.0.0.1:${app.server.address().port}/avatars/${name}.svg`); + assert.equal(avatar.status, 200, `Preset ${name} must be served by the SQLite host`); + assert.match(avatar.headers.get('content-type'), /^image\/svg\+xml/); + assert.match(await avatar.text(), /