From f7cac5dabfdf32db0d4ba54a977ad68a1f5c5e1b Mon Sep 17 00:00:00 2001 From: suyx Date: Wed, 5 Aug 2026 14:07:42 +0800 Subject: [PATCH] =?UTF-8?q?fix(POSTV1-02):=20=E4=BF=AE=E5=A4=8D=E4=BE=BF?= =?UTF-8?q?=E6=90=BA=E7=BD=91=E9=A1=B5=E9=9D=99=E6=80=81=E8=B5=84=E6=BA=90?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/api/src/app.ts | 21 ++++++++++++++++++++- tests/package/postv1-runtime.test.mjs | 14 +++++++++++++- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/apps/api/src/app.ts b/apps/api/src/app.ts index d0b7102..9f350aa 100644 --- a/apps/api/src/app.ts +++ b/apps/api/src/app.ts @@ -1,6 +1,6 @@ import { randomBytes, randomUUID } from "node:crypto"; import { createReadStream, existsSync, readFileSync } from "node:fs"; -import { resolve } from "node:path"; +import { extname, resolve } from "node:path"; import { AccountDeletionCompleteRequestSchema, @@ -218,6 +218,24 @@ import type { ManagedStorage } from "./managed-storage.js"; import { PrivateContentError, PrivateContentService } from "./private-content.js"; import { assertSafeAdminDiagnostics, assertSafeAdminServicesStorage } from "./admin-state.js"; +const productAssetContentTypes: Readonly> = { + ".css": "text/css; charset=utf-8", + ".jpeg": "image/jpeg", + ".jpg": "image/jpeg", + ".js": "text/javascript; charset=utf-8", + ".json": "application/json; charset=utf-8", + ".mjs": "text/javascript; charset=utf-8", + ".png": "image/png", + ".svg": "image/svg+xml", + ".webp": "image/webp", + ".woff": "font/woff", + ".woff2": "font/woff2", +}; + +function productAssetContentType(path: string) { + return productAssetContentTypes[extname(path).toLowerCase()] ?? "application/octet-stream"; +} + const defaultBootstrap: BootstrapResponse = { app_version: "0.0.0", dependencies: [], @@ -923,6 +941,7 @@ export async function createApp(options: CreateAppOptions = {}) { const relativePath = decodeURIComponent(request.url.split("?", 1)[0]!.slice("/assets/".length)); const assetPath = resolve(productWebRoot, "assets", relativePath); if (!assetPath.startsWith(resolve(productWebRoot, "assets")) || !existsSync(assetPath)) return reply.code(404).send(); + reply.type(productAssetContentType(assetPath)); return reply.send(readFileSync(assetPath)); }); app.get("/support-gate.css", { schema: { hide: true } }, async (_request, reply) => { diff --git a/tests/package/postv1-runtime.test.mjs b/tests/package/postv1-runtime.test.mjs index 51556dc..9befee7 100644 --- a/tests/package/postv1-runtime.test.mjs +++ b/tests/package/postv1-runtime.test.mjs @@ -138,7 +138,19 @@ test("portable package serves the product and keeps SQLite data across API resta assert.ok(cookie); const page = await fetch(`http://127.0.0.1:${port}/app`, { headers: { host: `127.0.0.1:${port}`, cookie, "sec-ch-ua": '"Google Chrome";v="150"' } }); assert.equal(page.status, 200); - assert.match(await page.text(), /
<\/div>/); + const pageHtml = await page.text(); + assert.match(pageHtml, /
<\/div>/); + const scriptPath = pageHtml.match(/]+src="([^"]+)"/)?.[1]; + const stylesheetPath = pageHtml.match(/]+href="([^"]+)"/)?.[1]; + assert.ok(scriptPath); + assert.ok(stylesheetPath); + const assetHeaders = { host: `127.0.0.1:${port}`, cookie, "sec-ch-ua": '"Google Chrome";v="150"' }; + const script = await fetch(`http://127.0.0.1:${port}${scriptPath}`, { headers: assetHeaders }); + const stylesheet = await fetch(`http://127.0.0.1:${port}${stylesheetPath}`, { headers: assetHeaders }); + assert.equal(script.status, 200); + assert.match(script.headers.get("content-type") ?? "", /^(?:application|text)\/javascript\b/); + assert.equal(stylesheet.status, 200); + assert.match(stylesheet.headers.get("content-type") ?? "", /^text\/css\b/); assert.ok(existsSync(join(dataRoot, "db", "dada.sqlite3"))); await verifyWorkerStartup(configPath); } finally {