diff --git a/.github/workflows/generate-openapi-artifact.yml b/.github/workflows/generate-openapi-artifact.yml index 79e066d3..519befaf 100644 --- a/.github/workflows/generate-openapi-artifact.yml +++ b/.github/workflows/generate-openapi-artifact.yml @@ -8,6 +8,9 @@ name: Generate OpenAPI artifact on: workflow_call: {} +permissions: + contents: read + jobs: generate: runs-on: ubuntu-latest diff --git a/apps/api/Dockerfile b/apps/api/Dockerfile index 3ccd76c8..e8457f9e 100644 --- a/apps/api/Dockerfile +++ b/apps/api/Dockerfile @@ -1,6 +1,6 @@ FROM node:26-alpine@sha256:2d984a15c9b54fd0aeb608b8e0d0d83529eb34d2966db27a1fb4f1edc3d298a3 AS build -RUN npm install -g corepack && corepack enable && corepack prepare pnpm@11.25.0 --activate +RUN npm install -g corepack@0.36.0 && corepack enable && corepack prepare pnpm@11.25.0 --activate WORKDIR /app # Manifests first for layer caching (all workspace manifests are required diff --git a/apps/web/Dockerfile b/apps/web/Dockerfile index 47e9b75b..ffed217e 100644 --- a/apps/web/Dockerfile +++ b/apps/web/Dockerfile @@ -1,6 +1,6 @@ FROM node:26-alpine@sha256:2d984a15c9b54fd0aeb608b8e0d0d83529eb34d2966db27a1fb4f1edc3d298a3 AS build -RUN npm install -g corepack && corepack enable && corepack prepare pnpm@11.25.0 --activate +RUN npm install -g corepack@0.36.0 && corepack enable && corepack prepare pnpm@11.25.0 --activate WORKDIR /app COPY pnpm-workspace.yaml package.json pnpm-lock.yaml tsconfig.base.json ./ diff --git a/apps/web/src/app.css b/apps/web/src/app.css index 6e2a3588..7b731097 100644 --- a/apps/web/src/app.css +++ b/apps/web/src/app.css @@ -42,32 +42,32 @@ /* Light — "le programme" (warm manila paper). Cards are pure white so they lift cleanly off the cream shell; the border carries the separation, not background saturation — a saturated bg read as dirty/kraft. */ - --bg: #f2ebdc; + --bg: #f7f5f3; --surface: #ffffff; - --surface-2: #ece3ce; - --border: #d9cba9; + --surface-2: #edeae3; + --border: #d3c7a8; --fg: #1c1712; --dim: #6b6354; - --accent: #96570a; + --accent: #8e620b; --accent-fg: #ffffff; --btn: #1c1712; --btn-fg: #ffffff; - --success: #2fa070; - --danger: #c9445e; - --warning: #c1650f; + --success: #257e58; + --danger: #c73c57; + --warning: #ab590d; /* Stats: one hue per domain, fixed order, CVD-validated — never cycled, never reused for anything else. Sub-splits within a domain (e.g. movie/series/anime) tint this hue rather than borrowing another domain's color. */ - --stat-media: #b07e10; - --stat-games: #0e8f9e; - --stat-books: #c7508e; - --stat-music: #3e6fc9; + --stat-media: #91680d; + --stat-games: #0c7a87; + --stat-books: #be3d80; + --stat-music: #3b6dc8; /* Achievement tiers ([G5]). One ring color per tier, never reused for anything else — same rule as the per-domain stat hues above. */ --tier-bronze: #8a5423; - --tier-silver: #6e7683; - --tier-gold: #96570a; + --tier-silver: #68707c; + --tier-gold: #8e620b; color-scheme: light; } diff --git a/apps/web/src/app.d.ts b/apps/web/src/app.d.ts index 0581571b..d93e3cd4 100644 --- a/apps/web/src/app.d.ts +++ b/apps/web/src/app.d.ts @@ -1,3 +1,10 @@ +// Vite's own ambient types (vite/client.d.ts) declare `*.ico` but not the +// `?inline` variant used by favicon.ico's +server.ts route. +declare module "*.ico?inline" { + const src: string; + export default src; +} + // See https://svelte.dev/docs/kit/types#app.d.ts // for information about these interfaces declare global { diff --git a/apps/web/src/hooks.server.ts b/apps/web/src/hooks.server.ts index 844ff276..b89d7255 100644 --- a/apps/web/src/hooks.server.ts +++ b/apps/web/src/hooks.server.ts @@ -1,6 +1,16 @@ import { paraglideMiddleware } from "$lib/paraglide/server.js"; import type { Handle } from "@sveltejs/kit"; +// Caddy (docker/Caddyfile) sets these same two headers at the edge for the +// hosted VPS, but a self-host install running this container directly +// (no reverse proxy, or one without equivalent headers) would otherwise +// ship with neither - set them here too so the app is protected either way. +// CSP stays out of scope here (see docker/Caddyfile's Report-Only comment). +const SECURITY_HEADERS = { + "X-Frame-Options": "DENY", + "X-Content-Type-Options": "nosniff", +}; + export const handle: Handle = ({ event, resolve }) => paraglideMiddleware(event.request, async ({ request, locale }) => { event.request = request; @@ -9,5 +19,10 @@ export const handle: Handle = ({ event, resolve }) => html.replaceAll("%paraglide.lang%", locale), }); response.headers.append("Vary", "Accept-Language, Cookie"); + + for (const [name, value] of Object.entries(SECURITY_HEADERS)) { + response.headers.set(name, value); + } + return response; }); diff --git a/apps/web/static/favicon.ico b/apps/web/src/lib/assets/favicon.ico similarity index 100% rename from apps/web/static/favicon.ico rename to apps/web/src/lib/assets/favicon.ico diff --git a/apps/web/src/routes/favicon.ico/+server.ts b/apps/web/src/routes/favicon.ico/+server.ts new file mode 100644 index 00000000..468decc1 --- /dev/null +++ b/apps/web/src/routes/favicon.ico/+server.ts @@ -0,0 +1,18 @@ +import favicon from "$lib/assets/favicon.ico?inline"; +import type { RequestHandler } from "./$types"; + +// sirv (adapter-node's static file server) resolves content types via +// mrmime, which has no `.ico` entry — a plain static/favicon.ico is served +// with an empty Content-Type (see the ZAP "Content-Type header missing" +// finding). Routing it through a +server.ts instead lets us set it +// explicitly. `?inline` forces Vite to always emit a base64 data: URI for +// this import, regardless of its size vs the default inlining threshold. +const bytes = Buffer.from(favicon.slice(favicon.indexOf(",") + 1), "base64"); + +export const GET: RequestHandler = () => + new Response(bytes, { + headers: { + "Content-Type": "image/vnd.microsoft.icon", + "Cache-Control": "public, max-age=86400", + }, + }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6a3b4e77..6d524d13 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,6 +9,11 @@ overrides: mysql2@<3.22.0: ^3.22.0 nanoid@<3.3.18: ^3.3.18 fastify: ^5.12.3 + fast-uri@3.1.5: ^3.1.6 + fast-uri@4.1.2: ^4.1.3 + deepmerge-ts@<8.0.0: ^8.0.0 + tmp@<0.2.6: ^0.2.6 + uuid@<11.1.1: ^11.1.1 importers: .: @@ -7138,12 +7143,12 @@ packages: integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==, } - deepmerge-ts@7.1.5: + deepmerge-ts@8.0.2: resolution: { - integrity: sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==, + integrity: sha512-uqbvqLUMrc6p0MO+WBRtTxY55hmyh94WRwI5a++PZe54X+bfVh59FSN7uWCBCW1CCVjzjnrwzfI8zidE2obMMw==, } - engines: { node: ">=16.0.0" } + engines: { node: ">=16.9.0" } deepmerge@4.3.1: resolution: @@ -7890,16 +7895,16 @@ packages: integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==, } - fast-uri@3.1.5: + fast-uri@3.1.7: resolution: { - integrity: sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==, + integrity: sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==, } - fast-uri@4.1.2: + fast-uri@4.1.4: resolution: { - integrity: sha512-TyGmBcbDTZXcb2cj5MV89DrF42DKvb3y5DDUNh95iO+IMeAzMkVSxK1PZRrRIpc9yg8U2GhGdbofNa0LS/a4Bw==, + integrity: sha512-dODXrIxlS9JSdgAnhIUKOosKV1oMtU2VtVw87QRaHzyl5jxO290Ii5tEZfCfzfWNHi3jKWwBSdQj0qIyshdZdQ==, } fast-wrap-ansi@0.2.2: @@ -10483,13 +10488,6 @@ packages: } engines: { node: ">=20" } - os-tmpdir@1.0.2: - resolution: - { - integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==, - } - engines: { node: ">=0.10.0" } - otpauth@9.5.2: resolution: { @@ -11519,14 +11517,6 @@ packages: integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==, } - rimraf@2.7.1: - resolution: - { - integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==, - } - deprecated: Rimraf versions prior to v4 are no longer supported - hasBin: true - rimraf@3.0.2: resolution: { @@ -12537,19 +12527,12 @@ packages: integrity: sha512-NFxmRT2lAEMcCOBgeZ0NuM0zsK/xgmNajnY6n4S1mwAKocft2s2ise1O3nQxrH3c+uY6hgHUV9GGNVp7tUE4Sg==, } - tmp@0.0.33: + tmp@0.2.7: resolution: { - integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==, + integrity: sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==, } - engines: { node: ">=0.6.0" } - - tmp@0.1.0: - resolution: - { - integrity: sha512-J7Z2K08jbGcdA1kkQpJSqLF6T0tdQqpR2pnSUXsIchbPdTI9v3e85cLW0d6WDhwuAleOV71j2xWs8qMPfK7nKw==, - } - engines: { node: ">=6" } + engines: { node: ">=14.14" } toad-cache@3.7.4: resolution: @@ -12967,19 +12950,18 @@ packages: } engines: { node: ">= 0.4.0" } - uuid@14.0.1: + uuid@11.1.1: resolution: { - integrity: sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==, + integrity: sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==, } hasBin: true - uuid@8.3.2: + uuid@14.0.1: resolution: { - integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==, + integrity: sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==, } - deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). hasBin: true v8-compile-cache-lib@3.0.1: @@ -14413,7 +14395,7 @@ snapshots: dependencies: ajv: 8.20.0 ajv-formats: 3.0.1(ajv@8.20.0) - fast-uri: 3.1.5 + fast-uri: 3.1.7 "@fastify/cors@11.3.0": dependencies: @@ -14809,8 +14791,8 @@ snapshots: lighthouse-logger: 1.2.0(supports-color@10.2.2) open: 7.4.2 proxy-agent: 6.5.0(supports-color@10.2.2) - tmp: 0.1.0 - uuid: 8.3.2 + tmp: 0.2.7 + uuid: 11.1.1 yargs: 15.4.1 yargs-parser: 13.1.2 transitivePeerDependencies: @@ -15419,7 +15401,7 @@ snapshots: "@prisma/config@7.10.0(magicast@0.5.4)": dependencies: c12: 3.3.4(magicast@0.5.4) - deepmerge-ts: 7.1.5 + deepmerge-ts: 8.0.2 effect: 3.20.0 empathic: 2.0.0 transitivePeerDependencies: @@ -17202,7 +17184,7 @@ snapshots: ajv@8.20.0: dependencies: fast-deep-equal: 3.1.3 - fast-uri: 3.1.5 + fast-uri: 3.1.7 json-schema-traverse: 1.0.0 require-from-string: 2.0.2 @@ -18058,7 +18040,7 @@ snapshots: deep-is@0.1.4: {} - deepmerge-ts@7.1.5: {} + deepmerge-ts@8.0.2: {} deepmerge@4.3.1: {} @@ -18545,7 +18527,7 @@ snapshots: dependencies: chardet: 0.7.0 iconv-lite: 0.4.24 - tmp: 0.0.33 + tmp: 0.2.7 extract-zip@2.0.1(supports-color@10.2.2): dependencies: @@ -18578,7 +18560,7 @@ snapshots: "@fastify/merge-json-schemas": 0.2.1 ajv: 8.20.0 ajv-formats: 3.0.1(ajv@8.20.0) - fast-uri: 4.1.2 + fast-uri: 4.1.4 json-schema-ref-resolver: 3.0.0 rfdc: 1.4.1 @@ -18596,9 +18578,9 @@ snapshots: dependencies: fast-string-truncated-width: 3.0.3 - fast-uri@3.1.5: {} + fast-uri@3.1.7: {} - fast-uri@4.1.2: {} + fast-uri@4.1.4: {} fast-wrap-ansi@0.2.2: dependencies: @@ -19835,7 +19817,7 @@ snapshots: roughjs: 4.6.6 stylis: 4.4.0 ts-dedent: 2.3.0 - uuid: 14.0.1 + uuid: 11.1.1 metaviewport-parser@0.3.0: {} @@ -20098,8 +20080,6 @@ snapshots: stdin-discarder: 0.3.2 string-width: 8.2.2 - os-tmpdir@1.0.2: {} - otpauth@9.5.2: dependencies: "@noble/hashes": 2.4.0 @@ -20748,10 +20728,6 @@ snapshots: rfdc@1.4.1: {} - rimraf@2.7.1: - dependencies: - glob: 7.2.3 - rimraf@3.0.2: dependencies: glob: 7.2.3 @@ -21488,13 +21464,7 @@ snapshots: dependencies: tldts-core: 6.1.86 - tmp@0.0.33: - dependencies: - os-tmpdir: 1.0.2 - - tmp@0.1.0: - dependencies: - rimraf: 2.7.1 + tmp@0.2.7: {} toad-cache@3.7.4: {} @@ -21750,9 +21720,9 @@ snapshots: utils-merge@1.0.1: {} - uuid@14.0.1: {} + uuid@11.1.1: {} - uuid@8.3.2: {} + uuid@14.0.1: {} v8-compile-cache-lib@3.0.1: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 6f451ae0..7c506dc7 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -44,7 +44,6 @@ minimumReleaseAgeExclude: - typescript@7.0.2 - cookie@0.7.0 - multer@2.2.0 - - fast-uri@3.1.4 || 3.1.5 - find-my-way@9.6.1 - js-yaml@4.3.1 || 5.2.2 - brace-expansion@1.1.17 || 1.1.18 @@ -99,3 +98,10 @@ overrides: # independently of the direct 5.12.3 dependency above, so TypeScript sees # two incompatible FastifyInstance types in main.ts. Force one version. fastify: ^5.12.3 + # Transitive-only vulnerable versions (dev tooling: ajv/prisma/lhci chains), + # none reachable from user input — forced to the first patched release. + fast-uri@3.1.5: ^3.1.6 + fast-uri@4.1.2: ^4.1.3 + deepmerge-ts@<8.0.0: ^8.0.0 + tmp@<0.2.6: ^0.2.6 + uuid@<11.1.1: ^11.1.1