From 1d7ea35a23b7466cdb697a54efe1f4db6bc27f2e Mon Sep 17 00:00:00 2001 From: Denozordec Date: Fri, 23 Oct 2026 16:55:59 +0700 Subject: [PATCH] =?UTF-8?q?perf(api):=20=D0=BA=D1=8D=D1=88-=D0=B7=D0=B0?= =?UTF-8?q?=D0=B3=D0=BE=D0=BB=D0=BE=D0=B2=D0=BA=D0=B8=20=D1=81=D1=82=D0=B0?= =?UTF-8?q?=D1=82=D0=B8=D0=BA=D0=B8,=20CORS=20=D0=BF=D0=BE=20allowlist,=20?= =?UTF-8?q?=D1=80=D0=B0=D0=B7=D0=B4=D0=B5=D0=BB=D1=8C=D0=BD=D1=8B=D0=B5=20?= =?UTF-8?q?health/ready,=20compress=20=D0=B2=20Traefik?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/api/package.json | 2 - apps/api/src/app.ts | 55 +++++++++--- apps/api/src/config.ts | 3 + apps/api/src/lib/cors.ts | 57 ++++++++++++ apps/api/src/lib/oidc/keys.ts | 5 ++ apps/api/src/routes/auth.ts | 20 +++-- apps/api/test/http-layer.test.ts | 145 ++++++++++++++++++++++++++++++ deploy/docker-compose.traefik.yml | 4 + pnpm-lock.yaml | 61 ------------- 9 files changed, 269 insertions(+), 83 deletions(-) create mode 100644 apps/api/src/lib/cors.ts create mode 100644 apps/api/test/http-layer.test.ts diff --git a/apps/api/package.json b/apps/api/package.json index 9f320ea..e573a2a 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -19,12 +19,10 @@ "@fastify/helmet": "^13.0.1", "@fastify/jwt": "^9.1.0", "@fastify/rate-limit": "^10.3.0", - "@fastify/sensible": "^6.0.3", "@fastify/static": "^8.2.0", "@node-rs/argon2": "^2.0.2", "@simplewebauthn/server": "^13.3.2", "fastify": "^5.4.0", - "fastify-plugin": "^5.0.1", "jose": "^6.2.8", "zod": "catalog:" }, diff --git a/apps/api/src/app.ts b/apps/api/src/app.ts index 6e39bc9..3f35dc0 100644 --- a/apps/api/src/app.ts +++ b/apps/api/src/app.ts @@ -3,7 +3,6 @@ import Fastify from 'fastify' import cors from '@fastify/cors' import helmet from '@fastify/helmet' import rateLimit from '@fastify/rate-limit' -import sensible from '@fastify/sensible' import formbody from '@fastify/formbody' import fjwt from '@fastify/jwt' import fastifyStatic from '@fastify/static' @@ -16,6 +15,7 @@ import { import { closeDb, createDb, + getAppSwitcherConfig, getUserApps, healthCheck, migrateSchema, @@ -35,10 +35,17 @@ import { oidcRoutes } from './routes/oidc.js' import { webauthnRoutes } from './routes/webauthn.js' import { startAuditRetentionJob } from './services/audit-retention.js' import { startHousekeepingJob } from './services/housekeeping.js' -import { ensureOidcSigningKey, resetOidcKeyCache } from './lib/oidc/keys.js' +import { + ensureOidcSigningKey, + isOidcKeyReady, + resetOidcKeyCache, +} from './lib/oidc/keys.js' +import { createOriginChecker } from './lib/cors.js' /** Пути сервера: для них SPA-fallback не применяется (404 JSON вместо index.html). */ const API_PREFIXES = ['/api', '/oauth', '/.well-known'] +const HASHED_ASSET_RE = /[\\/]assets[\\/]/ +const INDEX_HTML_RE = /[\\/]index\.html$/ declare module 'fastify' { interface FastifyInstance { @@ -140,9 +147,14 @@ export async function buildApp(opts: { }) - await app.register(sensible) + const originAllowed = createOriginChecker(config, () => + getAppSwitcherConfig(db).apps.map((a) => a.url), + ) await app.register(formbody) - await app.register(cors, { origin: true, credentials: true }) + await app.register(cors, { + origin: (origin, cb) => cb(null, !origin || originAllowed(origin)), + credentials: true, + }) await app.register(helmet, { contentSecurityPolicy: false }) await app.register(rateLimit, { max: 200, @@ -152,13 +164,20 @@ export async function buildApp(opts: { }) await app.register(fjwt, { secret: config.jwtSecret }) - app.get('/health', async () => { - healthCheck(sqlite) - return { ok: true } - }) + // liveness: процесс жив и отвечает; БД не трогаем, чтобы пробы не рестартили + // контейнер из-за временной блокировки. + app.get('/health', async () => ({ ok: true })) - app.get('/ready', async () => { - healthCheck(sqlite) + // readiness: БД отвечает и ключ подписи OIDC загружен. + app.get('/ready', async (_request, reply) => { + try { + healthCheck(sqlite) + } catch { + return reply.status(503).send({ ok: false, reason: 'db' }) + } + if (!isOidcKeyReady()) { + return reply.status(503).send({ ok: false, reason: 'oidc_key' }) + } return { ok: true } }) @@ -187,9 +206,21 @@ export async function buildApp(opts: { await app.register(fastifyStatic, { root: resolve(config.staticDir), wildcard: false, - // Traefik перед приложением не сжимает; .gz/.br для статики готовит - // Dockerfile на этапе сборки (find … gzip/brotli), здесь только отдаём. + // .gz/.br для статики готовит Dockerfile на этапе сборки, здесь только отдаём. preCompressed: true, + // Cache-Control выставляем сами (иначе плагин перезаписывает заголовок). + cacheControl: false, + setHeaders(res, filePath) { + if (HASHED_ASSET_RE.test(filePath)) { + // Vite кладёт хеш в имя файла → кэш на год без ревалидации. + res.setHeader('Cache-Control', 'public, max-age=31536000, immutable') + } else if (INDEX_HTML_RE.test(filePath)) { + // Точка входа должна ревалидироваться, иначе клиент застрянет на старых чанках. + res.setHeader('Cache-Control', 'no-cache') + } else { + res.setHeader('Cache-Control', 'public, max-age=0') + } + }, }) } const spa = Boolean(config.staticDir && existsSync(config.staticDir)) diff --git a/apps/api/src/config.ts b/apps/api/src/config.ts index 1a87712..c33bf50 100644 --- a/apps/api/src/config.ts +++ b/apps/api/src/config.ts @@ -33,6 +33,8 @@ export const configSchema = z.object({ serverPort: z.coerce.number().int().positive().default(8080), staticDir: z.string().optional(), logLevel: z.string().default('info'), + /** Доп. разрешённые CORS-origins через запятую ('*' — все). */ + corsOrigins: z.string().default(''), /** Одновременных argon2-хешей (каждый ~19 МиБ). */ argon2Concurrency: z.coerce.number().int().positive().default(2), /** 'true' | 'false' | список IP/CIDR доверенных прокси. */ @@ -77,6 +79,7 @@ export function loadConfig(env: NodeJS.ProcessEnv = process.env): AppConfig { logLevel: env.LOG_LEVEL ?? 'info', shutdownTimeoutMs: env.SHUTDOWN_TIMEOUT_MS ?? 8000, sessionRetentionDays: env.SESSION_RETENTION_DAYS ?? 7, + corsOrigins: env.CORS_ORIGINS ?? '', argon2Concurrency: env.ARGON2_CONCURRENCY ?? 2, trustProxy: env.TRUST_PROXY ?? DEFAULT_TRUST_PROXY, isProd: diff --git a/apps/api/src/lib/cors.ts b/apps/api/src/lib/cors.ts new file mode 100644 index 0000000..097551e --- /dev/null +++ b/apps/api/src/lib/cors.ts @@ -0,0 +1,57 @@ +import type { AppConfig } from '../config.js' +import { oidcIssuerFromConfig } from '../config.js' + +function originOf(url: string): string | null { + try { + return new URL(url).origin + } catch { + return null + } +} + +/** + * Проверка Origin для CORS с credentials. Разрешены: CORS_ORIGINS, issuer, + * приложения из App Switcher и хосты из RETURN_TO_ALLOWLIST + * (".suffix" — суффикс хоста, "localhost", точные хосты). '*' в CORS_ORIGINS — + * разрешить всё (только осознанно). + */ +export function createOriginChecker( + config: AppConfig, + switcherUrls: () => string[], +): (origin: string) => boolean { + const explicit = new Set() + let allowAll = false + for (const raw of config.corsOrigins.split(',')) { + const v = raw.trim() + if (!v) continue + if (v === '*') allowAll = true + else { + const o = originOf(v) + if (o) explicit.add(o) + } + } + for (const u of [config.issuer, oidcIssuerFromConfig(config)]) { + const o = originOf(u) + if (o) explicit.add(o) + } + const tokens = config.returnToAllowlist + .split(',') + .map((t) => t.trim().toLowerCase()) + .filter(Boolean) + + return (origin: string) => { + if (allowAll) return true + const o = originOf(origin) + if (!o) return false + if (explicit.has(o)) return true + const host = new URL(o).hostname.toLowerCase() + for (const t of tokens) { + if (t === 'private') continue + if (t.startsWith('.') ? host.endsWith(t) : host === t) return true + } + for (const u of switcherUrls()) { + if (originOf(u) === o) return true + } + return false + } +} diff --git a/apps/api/src/lib/oidc/keys.ts b/apps/api/src/lib/oidc/keys.ts index 9720309..6c6a1f4 100644 --- a/apps/api/src/lib/oidc/keys.ts +++ b/apps/api/src/lib/oidc/keys.ts @@ -113,6 +113,11 @@ export async function ensureOidcSigningKey( return cached } +/** Ключ подписи загружен — приложение готово выдавать токены. */ +export function isOidcKeyReady(): boolean { + return cached !== null +} + /** Reset cache (tests). */ export function resetOidcKeyCache(): void { cached = null diff --git a/apps/api/src/routes/auth.ts b/apps/api/src/routes/auth.ts index b14cef7..9de692c 100644 --- a/apps/api/src/routes/auth.ts +++ b/apps/api/src/routes/auth.ts @@ -23,11 +23,14 @@ import { clientUserAgent, targetAppFromReturnTo } from '../lib/target-app.js' export async function authRoutes(app: FastifyInstance): Promise { /** Public — SPA reads allowlist at runtime (Docker-friendly). */ - app.get('/api/v1/auth/config', async () => ({ - return_to_allowlist: app.config.returnToAllowlist, - issuer: app.config.issuer, - webauthn: true, - })) + app.get('/api/v1/auth/config', async (_request, reply) => { + reply.header('Cache-Control', 'public, max-age=60') + return { + return_to_allowlist: app.config.returnToAllowlist, + issuer: app.config.issuer, + webauthn: true, + } + }) app.post('/api/v1/auth/login', { config: { rateLimit: { max: 20, timeWindow: '1 minute' } }, @@ -213,9 +216,10 @@ export async function authRoutes(app: FastifyInstance): Promise { ) /** Public — apps chrome (CFDM/VPS) fetch switcher URLs without portal JWT. */ - app.get('/api/v1/app-switcher', async () => - publicAppSwitcherConfig(getAppSwitcherConfig(app.db)), - ) + app.get('/api/v1/app-switcher', async (_request, reply) => { + reply.header('Cache-Control', 'public, max-age=30') + return publicAppSwitcherConfig(getAppSwitcherConfig(app.db)) + }) app.get( '/api/v1/auth/me', diff --git a/apps/api/test/http-layer.test.ts b/apps/api/test/http-layer.test.ts new file mode 100644 index 0000000..8423243 --- /dev/null +++ b/apps/api/test/http-layer.test.ts @@ -0,0 +1,145 @@ +import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, describe, expect, it } from 'vitest' +import { createOriginChecker } from '../src/lib/cors.js' +import { loadConfig } from '../src/config.js' +import { buildTestApp } from './helpers.js' + +let app: Awaited> | undefined +afterEach(async () => { + await app?.close() + app = undefined +}) + +function staticDir(): string { + const dir = mkdtempSync(join(tmpdir(), 'ap-static-')) + mkdirSync(join(dir, 'assets')) + writeFileSync(join(dir, 'index.html'), 'spa') + writeFileSync(join(dir, 'assets', 'app-abc123.js'), 'console.log(1)') + return dir +} + +describe('статика', () => { + it('hashed-ассеты immutable, index.html no-cache', async () => { + app = await buildTestApp({ STATIC_DIR: staticDir() }) + const asset = await app.inject({ method: 'GET', url: '/assets/app-abc123.js' }) + expect(asset.statusCode).toBe(200) + expect(asset.headers['cache-control']).toBe( + 'public, max-age=31536000, immutable', + ) + const index = await app.inject({ method: 'GET', url: '/index.html' }) + expect(index.headers['cache-control']).toBe('no-cache') + const spa = await app.inject({ method: 'GET', url: '/users/42' }) + expect(spa.statusCode).toBe(200) + expect(spa.headers['cache-control']).toBe('no-cache') + }) + + it('отсутствующий ассет → не отдаёт index.html как JS (404 JSON или html, но не 200 js)', async () => { + app = await buildTestApp({ STATIC_DIR: staticDir() }) + const res = await app.inject({ method: 'GET', url: '/assets/missing.js' }) + // SPA-fallback допустим только как html; кэшироваться как immutable он не должен + expect(res.headers['cache-control']).not.toContain('immutable') + }) + + it('отдаёт предсжатый .br по Accept-Encoding', async () => { + const dir = staticDir() + writeFileSync(join(dir, 'assets', 'app-abc123.js.br'), 'BR-BYTES') + app = await buildTestApp({ STATIC_DIR: dir }) + const res = await app.inject({ + method: 'GET', + url: '/assets/app-abc123.js', + headers: { 'accept-encoding': 'br' }, + }) + expect(res.headers['content-encoding']).toBe('br') + expect(res.body).toBe('BR-BYTES') + }) +}) + +describe('кэш-заголовки API', () => { + it('app-switcher и auth/config кэшируются коротко', async () => { + app = await buildTestApp() + const sw = await app.inject({ method: 'GET', url: '/api/v1/app-switcher' }) + expect(sw.headers['cache-control']).toBe('public, max-age=30') + const cfg = await app.inject({ method: 'GET', url: '/api/v1/auth/config' }) + expect(cfg.headers['cache-control']).toBe('public, max-age=60') + }) +}) + +describe('health / ready', () => { + it('/health не трогает БД и живёт после закрытия БД', async () => { + app = await buildTestApp() + app.sqlite.close() + const res = await app.inject({ method: 'GET', url: '/health' }) + expect(res.statusCode).toBe(200) + }) + + it('/ready 200 в норме и 503 при недоступной БД', async () => { + app = await buildTestApp() + const ok = await app.inject({ method: 'GET', url: '/ready' }) + expect(ok.statusCode).toBe(200) + app.sqlite.close() + const bad = await app.inject({ method: 'GET', url: '/ready' }) + expect(bad.statusCode).toBe(503) + expect(bad.json()).toEqual({ ok: false, reason: 'db' }) + }) +}) + +describe('CORS', () => { + const origin = (url: string) => ({ origin: url }) + + it('разрешает issuer/allowlist, блокирует чужие', async () => { + app = await buildTestApp({ RETURN_TO_ALLOWLIST: '.test.local,localhost' }) + const allowed = await app.inject({ + method: 'GET', + url: '/api/v1/auth/config', + headers: origin('https://auth.test.local'), + }) + expect(allowed.headers['access-control-allow-origin']).toBe( + 'https://auth.test.local', + ) + expect(allowed.headers['access-control-allow-credentials']).toBe('true') + + const foreign = await app.inject({ + method: 'GET', + url: '/api/v1/auth/config', + headers: origin('https://evil.example'), + }) + expect(foreign.headers['access-control-allow-origin']).toBeUndefined() + }) + + it('CORS_ORIGINS добавляет origin; запрос без Origin проходит', async () => { + app = await buildTestApp({ CORS_ORIGINS: 'https://extra.example' }) + const res = await app.inject({ + method: 'GET', + url: '/api/v1/auth/config', + headers: origin('https://extra.example'), + }) + expect(res.headers['access-control-allow-origin']).toBe( + 'https://extra.example', + ) + const none = await app.inject({ method: 'GET', url: '/api/v1/auth/config' }) + expect(none.statusCode).toBe(200) + }) + + it('createOriginChecker: суффикс, localhost, switcher, *', () => { + const base = { + JWT_SECRET: 'test-secret-at-least-8', + ISSUER: 'https://auth.test.local', + RETURN_TO_ALLOWLIST: '.shnt.top,localhost,private', + } + const check = createOriginChecker(loadConfig(base), () => [ + 'https://dns.custom.io/path', + ]) + expect(check('https://cfdm.shnt.top')).toBe(true) + expect(check('http://localhost:5173')).toBe(true) + expect(check('https://dns.custom.io')).toBe(true) + expect(check('https://evilshnt.top')).toBe(false) + expect(check('not a url')).toBe(false) + const all = createOriginChecker( + loadConfig({ ...base, CORS_ORIGINS: '*' }), + () => [], + ) + expect(all('https://anything.example')).toBe(true) + }) +}) diff --git a/deploy/docker-compose.traefik.yml b/deploy/docker-compose.traefik.yml index f750721..ca2cdee 100644 --- a/deploy/docker-compose.traefik.yml +++ b/deploy/docker-compose.traefik.yml @@ -88,6 +88,10 @@ services: - traefik.http.routers.auth-portal.tls=true - traefik.http.routers.auth-portal.tls.certresolver=letsencrypt - traefik.http.services.auth-portal.loadbalancer.server.port=8080 + # Сжатие JSON/текста на стороне Traefik (ноль CPU/RAM в Node); + # предсжатые .br/.gz статики Traefik не пересжимает (есть Content-Encoding). + - traefik.http.middlewares.auth-portal-compress.compress=true + - traefik.http.routers.auth-portal.middlewares=auth-portal-compress healthcheck: test: [ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 491a0a4..e013885 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -78,9 +78,6 @@ importers: '@fastify/rate-limit': specifier: ^10.3.0 version: 10.3.0 - '@fastify/sensible': - specifier: ^6.0.3 - version: 6.0.4 '@fastify/static': specifier: ^8.2.0 version: 8.3.0 @@ -93,9 +90,6 @@ importers: fastify: specifier: ^5.4.0 version: 5.10.0 - fastify-plugin: - specifier: ^5.0.1 - version: 5.1.0 jose: specifier: ^6.2.8 version: 6.2.8 @@ -1295,9 +1289,6 @@ packages: '@fastify/send@4.1.0': resolution: {integrity: sha512-TMYeQLCBSy2TOFmV95hQWkiTYgC/SEx7vMdV+wnZVX4tt8VBLKzmH8vV9OzJehV0+XBfg+WxPMt5wp+JBUKsVw==} - '@fastify/sensible@6.0.4': - resolution: {integrity: sha512-1vxcCUlPMew6WroK8fq+LVOwbsLtX+lmuRuqpcp6eYqu6vmkLwbKTdBWAZwbeaSgCfW4tzUpTIHLLvTiQQ1BwQ==} - '@fastify/static@8.3.0': resolution: {integrity: sha512-yKxviR5PH1OKNnisIzZKmgZSus0r2OZb8qCSbqmw34aolT4g3UlzYfeBRym+HJ1J471CR8e2ldNub4PubD1coA==} @@ -3122,10 +3113,6 @@ packages: resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==} engines: {node: '>= 6'} - forwarded@0.2.0: - resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} - engines: {node: '>= 0.6'} - fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} @@ -3707,10 +3694,6 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} - media-typer@1.1.0: - resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} - engines: {node: '>= 0.8'} - meow@12.1.1: resolution: {integrity: sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==} engines: {node: '>=16.10'} @@ -3734,18 +3717,10 @@ packages: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} - mime-db@1.54.0: - resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} - engines: {node: '>= 0.6'} - mime-types@2.1.35: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} - mime-types@3.0.2: - resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} - engines: {node: '>=18'} - mime@3.0.0: resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} engines: {node: '>=10.0.0'} @@ -4748,10 +4723,6 @@ packages: resolution: {integrity: sha512-YGYEVz3Fm5iy/AybuA0oyNFq7H4CgQNfRp/qfe8nurE1kuCeNm3/vfm9X4Mtl+qLyaKJUh5xrFZwogr41SMjYA==} engines: {node: '>=20'} - type-is@2.1.0: - resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==} - engines: {node: '>= 18'} - typescript-eslint@8.64.0: resolution: {integrity: sha512-0qg+pDNMnqYzqH9AnNK+39tejHvsShUOUUoRUgtnTGE7QuMZhiFDnozq8nHJVq+Wae6NMLKNWLg5WmkcC/ndyQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -4870,10 +4841,6 @@ packages: validate-npm-package-license@3.0.4: resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} - vary@1.1.2: - resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} - engines: {node: '>= 0.8'} - vite@8.1.5: resolution: {integrity: sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -5779,16 +5746,6 @@ snapshots: http-errors: 2.0.1 mime: 3.0.0 - '@fastify/sensible@6.0.4': - dependencies: - '@lukeed/ms': 2.0.2 - dequal: 2.0.3 - fastify-plugin: 5.1.0 - forwarded: 0.2.0 - http-errors: 2.0.1 - type-is: 2.1.0 - vary: 1.1.2 - '@fastify/static@8.3.0': dependencies: '@fastify/accept-negotiator': 2.0.1 @@ -7735,8 +7692,6 @@ snapshots: hasown: 2.0.4 mime-types: 2.1.35 - forwarded@0.2.0: {} - fs-constants@1.0.0: {} fs-extra@11.4.0: @@ -8242,8 +8197,6 @@ snapshots: math-intrinsics@1.1.0: {} - media-typer@1.1.0: {} - meow@12.1.1: {} meow@13.2.0: {} @@ -8259,16 +8212,10 @@ snapshots: mime-db@1.52.0: {} - mime-db@1.54.0: {} - mime-types@2.1.35: dependencies: mime-db: 1.52.0 - mime-types@3.0.2: - dependencies: - mime-db: 1.54.0 - mime@3.0.0: {} mime@4.1.0: {} @@ -9180,12 +9127,6 @@ snapshots: dependencies: tagged-tag: 1.0.0 - type-is@2.1.0: - dependencies: - content-type: 2.0.0 - media-typer: 1.1.0 - mime-types: 3.0.2 - typescript-eslint@8.64.0(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3): dependencies: '@typescript-eslint/eslint-plugin': 8.64.0(@typescript-eslint/parser@8.64.0(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3))(eslint@10.7.0(jiti@2.7.0))(typescript@6.0.3) @@ -9262,8 +9203,6 @@ snapshots: spdx-correct: 3.2.0 spdx-expression-parse: 3.0.1 - vary@1.1.2: {} - vite@8.1.5(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1): dependencies: lightningcss: 1.32.0