fix(api): CORS через isReturnToAllowed, мягкая проверка JWT_SECRET, 404 для /assets, better-sqlite3 в api, healthcheck на /ready
This commit is contained in:
1 parent
5cbf4d29d7
commit
08be310e6b
12 files changed
+93
-91
No files matched your search
@@ -22,6 +22,7 @@
|
|||||||
"@fastify/rate-limit": "^10.3.0",
|
"@fastify/rate-limit": "^10.3.0",
|
||||||
"@fastify/static": "^8.2.0",
|
"@fastify/static": "^8.2.0",
|
||||||
"@node-rs/argon2": "^2.0.2",
|
"@node-rs/argon2": "^2.0.2",
|
||||||
|
"better-sqlite3": "^11.10.0",
|
||||||
"@simplewebauthn/server": "^13.3.2",
|
"@simplewebauthn/server": "^13.3.2",
|
||||||
"fastify": "^5.4.0",
|
"fastify": "^5.4.0",
|
||||||
"jose": "^6.2.8",
|
"jose": "^6.2.8",
|
||||||
|
|||||||
+1
-1
@@ -50,7 +50,7 @@ import {
|
|||||||
import { createOriginChecker } from './lib/cors.js'
|
import { createOriginChecker } from './lib/cors.js'
|
||||||
|
|
||||||
/** Пути сервера: для них SPA-fallback не применяется (404 JSON вместо index.html). */
|
/** Пути сервера: для них SPA-fallback не применяется (404 JSON вместо index.html). */
|
||||||
const API_PREFIXES = ['/api', '/oauth', '/.well-known']
|
const API_PREFIXES = ['/api', '/oauth', '/.well-known', '/assets/']
|
||||||
const HASHED_ASSET_RE = /[\\/]assets[\\/]/
|
const HASHED_ASSET_RE = /[\\/]assets[\\/]/
|
||||||
const INDEX_HTML_RE = /[\\/]index\.html$/
|
const INDEX_HTML_RE = /[\\/]index\.html$/
|
||||||
|
|
||||||
|
|||||||
@@ -65,12 +65,18 @@ const WEAK_JWT_SECRETS = new Set([
|
|||||||
'changeme',
|
'changeme',
|
||||||
])
|
])
|
||||||
|
|
||||||
/** В production запрещены известные/слабые значения JWT_SECRET. */
|
/** В production запрещены известные дефолты JWT_SECRET; короткий секрет — только предупреждение. */
|
||||||
function assertProductionSecrets(config: AppConfig): void {
|
function assertProductionSecrets(config: AppConfig): void {
|
||||||
if (!config.isProd) return
|
if (!config.isProd) return
|
||||||
if (WEAK_JWT_SECRETS.has(config.jwtSecret) || config.jwtSecret.length < 16) {
|
if (WEAK_JWT_SECRETS.has(config.jwtSecret)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'JWT_SECRET: в production нужен уникальный секрет длиной не менее 16 символов',
|
'JWT_SECRET: в production нельзя использовать значение по умолчанию — задайте уникальный секрет',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (config.jwtSecret.length < 16) {
|
||||||
|
// Не роняем старые установки с коротким, но уникальным секретом.
|
||||||
|
process.emitWarning(
|
||||||
|
'JWT_SECRET короче 16 символов — смените на более длинный (смена разлогинит пользователей)',
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { isReturnToAllowed } from '@authportal/shared'
|
||||||
import type { AppConfig } from '../config.js'
|
import type { AppConfig } from '../config.js'
|
||||||
import { oidcIssuerFromConfig } from '../config.js'
|
import { oidcIssuerFromConfig } from '../config.js'
|
||||||
|
|
||||||
@@ -11,8 +12,8 @@ function originOf(url: string): string | null {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Проверка Origin для CORS с credentials. Разрешены: CORS_ORIGINS, issuer,
|
* Проверка Origin для CORS с credentials. Разрешены: CORS_ORIGINS, issuer,
|
||||||
* приложения из App Switcher и хосты из RETURN_TO_ALLOWLIST
|
* приложения из App Switcher и всё, что разрешает RETURN_TO_ALLOWLIST
|
||||||
* (".suffix" — суффикс хоста, "localhost", точные хосты). '*' в CORS_ORIGINS —
|
* (та же isReturnToAllowed, что для return_to: ".suffix", localhost, полные origin, private). '*' в CORS_ORIGINS —
|
||||||
* разрешить всё (только осознанно).
|
* разрешить всё (только осознанно).
|
||||||
*/
|
*/
|
||||||
export function createOriginChecker(
|
export function createOriginChecker(
|
||||||
@@ -34,21 +35,13 @@ export function createOriginChecker(
|
|||||||
const o = originOf(u)
|
const o = originOf(u)
|
||||||
if (o) explicit.add(o)
|
if (o) explicit.add(o)
|
||||||
}
|
}
|
||||||
const tokens = config.returnToAllowlist
|
|
||||||
.split(',')
|
|
||||||
.map((t) => t.trim().toLowerCase())
|
|
||||||
.filter(Boolean)
|
|
||||||
|
|
||||||
return (origin: string) => {
|
return (origin: string) => {
|
||||||
if (allowAll) return true
|
if (allowAll) return true
|
||||||
const o = originOf(origin)
|
const o = originOf(origin)
|
||||||
if (!o) return false
|
if (!o) return false
|
||||||
if (explicit.has(o)) return true
|
if (explicit.has(o)) return true
|
||||||
const host = new URL(o).hostname.toLowerCase()
|
// Тот же allowlist, что у return_to (.suffix, localhost, полные origin, private).
|
||||||
for (const t of tokens) {
|
if (isReturnToAllowed(o, config.returnToAllowlist)) return true
|
||||||
if (t === 'private') continue
|
|
||||||
if (t.startsWith('.') ? host.endsWith(t) : host === t) return true
|
|
||||||
}
|
|
||||||
for (const u of switcherUrls()) {
|
for (const u of switcherUrls()) {
|
||||||
if (originOf(u) === o) return true
|
if (originOf(u) === o) return true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -216,10 +216,10 @@ export async function authRoutes(app: FastifyInstance): Promise<void> {
|
|||||||
)
|
)
|
||||||
|
|
||||||
/** Public — apps chrome (CFDM/VPS) fetch switcher URLs without portal JWT. */
|
/** Public — apps chrome (CFDM/VPS) fetch switcher URLs without portal JWT. */
|
||||||
app.get('/api/v1/app-switcher', async (_request, reply) => {
|
// Без Cache-Control: после сохранения в админке SPA должна сразу видеть новый конфиг.
|
||||||
reply.header('Cache-Control', 'public, max-age=30')
|
app.get('/api/v1/app-switcher', async () =>
|
||||||
return publicAppSwitcherConfig(getAppSwitcherConfig(app.db))
|
publicAppSwitcherConfig(getAppSwitcherConfig(app.db)),
|
||||||
})
|
)
|
||||||
|
|
||||||
app.get(
|
app.get(
|
||||||
'/api/v1/auth/me',
|
'/api/v1/auth/me',
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { afterEach, describe, expect, it } from 'vitest'
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||||
import { Writable } from 'node:stream'
|
import { Writable } from 'node:stream'
|
||||||
import Fastify from 'fastify'
|
import Fastify from 'fastify'
|
||||||
import { assertBootstrapPassword, loadConfig } from '../src/config.js'
|
import { assertBootstrapPassword, loadConfig } from '../src/config.js'
|
||||||
@@ -52,8 +52,8 @@ describe('loadConfig', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('production guard', () => {
|
describe('production guard', () => {
|
||||||
it.each(['change-me-in-production', 'dev-secret-change-me', 'short-secret'])(
|
it.each(['change-me-in-production', 'dev-secret-change-me'])(
|
||||||
'слабый JWT_SECRET "%s" отклоняется',
|
'известный дефолт JWT_SECRET "%s" отклоняется',
|
||||||
(secret) => {
|
(secret) => {
|
||||||
expect(() =>
|
expect(() =>
|
||||||
loadConfig({ NODE_ENV: 'production', JWT_SECRET: secret }),
|
loadConfig({ NODE_ENV: 'production', JWT_SECRET: secret }),
|
||||||
@@ -61,13 +61,25 @@ describe('production guard', () => {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
it('нормальный секрет в production принимается; в test слабый разрешён', () => {
|
it('короткий, но уникальный секрет в production не роняет старт (только предупреждение)', () => {
|
||||||
|
const warn = vi.spyOn(process, 'emitWarning').mockImplementation(() => {})
|
||||||
|
expect(() =>
|
||||||
|
loadConfig({ NODE_ENV: 'production', JWT_SECRET: 'short-secret' }),
|
||||||
|
).not.toThrow()
|
||||||
|
expect(warn).toHaveBeenCalledWith(expect.stringContaining('JWT_SECRET'))
|
||||||
|
warn.mockRestore()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('нормальный секрет в production принимается без предупреждений; в test слабый разрешён', () => {
|
||||||
|
const warn = vi.spyOn(process, 'emitWarning').mockImplementation(() => {})
|
||||||
expect(() =>
|
expect(() =>
|
||||||
loadConfig({ NODE_ENV: 'production', JWT_SECRET: base.JWT_SECRET }),
|
loadConfig({ NODE_ENV: 'production', JWT_SECRET: base.JWT_SECRET }),
|
||||||
).not.toThrow()
|
).not.toThrow()
|
||||||
|
expect(warn).not.toHaveBeenCalled()
|
||||||
expect(() =>
|
expect(() =>
|
||||||
loadConfig({ NODE_ENV: 'test', JWT_SECRET: 'short-secret' }),
|
loadConfig({ NODE_ENV: 'test', JWT_SECRET: 'short-secret' }),
|
||||||
).not.toThrow()
|
).not.toThrow()
|
||||||
|
warn.mockRestore()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('пароль первого админа: слабый запрещён только в production', () => {
|
it('пароль первого админа: слабый запрещён только в production', () => {
|
||||||
|
|||||||
@@ -35,11 +35,11 @@ describe('статика', () => {
|
|||||||
expect(spa.headers['cache-control']).toBe('no-cache')
|
expect(spa.headers['cache-control']).toBe('no-cache')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('отсутствующий ассет → не отдаёт index.html как JS (404 JSON или html, но не 200 js)', async () => {
|
it('отсутствующий ассет → 404, а не index.html (иначе после деплоя чанк-JS = HTML)', async () => {
|
||||||
app = await buildTestApp({ STATIC_DIR: staticDir() })
|
app = await buildTestApp({ STATIC_DIR: staticDir() })
|
||||||
const res = await app.inject({ method: 'GET', url: '/assets/missing.js' })
|
const res = await app.inject({ method: 'GET', url: '/assets/missing.js' })
|
||||||
// SPA-fallback допустим только как html; кэшироваться как immutable он не должен
|
expect(res.statusCode).toBe(404)
|
||||||
expect(res.headers['cache-control']).not.toContain('immutable')
|
expect(res.body).not.toContain('<html>')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('отдаёт предсжатый .br по Accept-Encoding', async () => {
|
it('отдаёт предсжатый .br по Accept-Encoding', async () => {
|
||||||
@@ -57,10 +57,10 @@ describe('статика', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('кэш-заголовки API', () => {
|
describe('кэш-заголовки API', () => {
|
||||||
it('app-switcher и auth/config кэшируются коротко', async () => {
|
it('auth/config кэшируется, app-switcher — нет (админка должна видеть правки сразу)', async () => {
|
||||||
app = await buildTestApp()
|
app = await buildTestApp()
|
||||||
const sw = await app.inject({ method: 'GET', url: '/api/v1/app-switcher' })
|
const sw = await app.inject({ method: 'GET', url: '/api/v1/app-switcher' })
|
||||||
expect(sw.headers['cache-control']).toBe('public, max-age=30')
|
expect(sw.headers['cache-control']).toBeUndefined()
|
||||||
const cfg = await app.inject({ method: 'GET', url: '/api/v1/auth/config' })
|
const cfg = await app.inject({ method: 'GET', url: '/api/v1/auth/config' })
|
||||||
expect(cfg.headers['cache-control']).toBe('public, max-age=60')
|
expect(cfg.headers['cache-control']).toBe('public, max-age=60')
|
||||||
})
|
})
|
||||||
@@ -122,6 +122,41 @@ describe('CORS', () => {
|
|||||||
expect(none.statusCode).toBe(200)
|
expect(none.statusCode).toBe(200)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('боевой RETURN_TO_ALLOWLIST из env.traefik.example не теряет клиентов', () => {
|
||||||
|
const list =
|
||||||
|
'.shnt.top,https://vps.shnt.top,https://cfdm.shnt.top,https://bgp.shnt.top,https://fw.shnt.top,https://dns.shnt.top,https://cdn.shnt.top'
|
||||||
|
const check = createOriginChecker(
|
||||||
|
loadConfig({
|
||||||
|
JWT_SECRET: 'test-secret-at-least-8',
|
||||||
|
ISSUER: 'https://auth.shnt.top',
|
||||||
|
RETURN_TO_ALLOWLIST: list,
|
||||||
|
}),
|
||||||
|
() => [],
|
||||||
|
)
|
||||||
|
for (const o of [
|
||||||
|
'https://vps.shnt.top',
|
||||||
|
'https://dns.shnt.top',
|
||||||
|
'https://shnt.top',
|
||||||
|
'https://anything.shnt.top',
|
||||||
|
]) {
|
||||||
|
expect(check(o)).toBe(true)
|
||||||
|
}
|
||||||
|
expect(check('https://evil.example')).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('private в allowlist пускает LAN-хосты', () => {
|
||||||
|
const check = createOriginChecker(
|
||||||
|
loadConfig({
|
||||||
|
JWT_SECRET: 'test-secret-at-least-8',
|
||||||
|
RETURN_TO_ALLOWLIST: '.shnt.top,private',
|
||||||
|
}),
|
||||||
|
() => [],
|
||||||
|
)
|
||||||
|
expect(check('http://192.168.1.10:8080')).toBe(true)
|
||||||
|
expect(check('http://10.0.0.5')).toBe(true)
|
||||||
|
expect(check('https://8.8.8.8')).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
it('createOriginChecker: суффикс, localhost, switcher, *', () => {
|
it('createOriginChecker: суффикс, localhost, switcher, *', () => {
|
||||||
const base = {
|
const base = {
|
||||||
JWT_SECRET: 'test-secret-at-least-8',
|
JWT_SECRET: 'test-secret-at-least-8',
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ services:
|
|||||||
- traefik.http.middlewares.auth-portal-compress.compress=true
|
- traefik.http.middlewares.auth-portal-compress.compress=true
|
||||||
- traefik.http.routers.auth-portal.middlewares=auth-portal-compress
|
- traefik.http.routers.auth-portal.middlewares=auth-portal-compress
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1:8080/health"]
|
test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1:8080/ready"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 3
|
retries: 3
|
||||||
|
|||||||
@@ -69,5 +69,5 @@ STOPSIGNAL SIGTERM
|
|||||||
VOLUME ["/data"]
|
VOLUME ["/data"]
|
||||||
# busybox wget вместо запуска второго процесса node каждые 30 с.
|
# busybox wget вместо запуска второго процесса node каждые 30 с.
|
||||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
||||||
CMD ["wget", "-q", "-O", "/dev/null", "http://127.0.0.1:8080/health"]
|
CMD ["wget", "-q", "-O", "/dev/null", "http://127.0.0.1:8080/ready"]
|
||||||
CMD ["node", "dist/server.js"]
|
CMD ["node", "dist/server.js"]
|
||||||
+1
-1
@@ -38,7 +38,7 @@ services:
|
|||||||
security_opt:
|
security_opt:
|
||||||
- no-new-privileges:true
|
- no-new-privileges:true
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1:8080/health"]
|
test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1:8080/ready"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 3
|
retries: 3
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ export function createMemoryDb(): { db: AppDb; sqlite: Sqlite } {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Версия схемы в PRAGMA user_version; при совпадении migrateSchema ничего не делает. */
|
/** Версия схемы в PRAGMA user_version; при совпадении migrateSchema ничего не делает. */
|
||||||
|
// ВАЖНО: любое изменение baseMigrate()/migrateV1() требует увеличить SCHEMA_VERSION
|
||||||
|
// (+ новая migrateVN), иначе существующие БД со скипом по user_version его не применят.
|
||||||
export const SCHEMA_VERSION = 1
|
export const SCHEMA_VERSION = 1
|
||||||
|
|
||||||
export function migrateSchema(sqlite: Sqlite): void {
|
export function migrateSchema(sqlite: Sqlite): void {
|
||||||
|
|||||||
Generated
+12
-59
@@ -87,6 +87,9 @@ importers:
|
|||||||
'@simplewebauthn/server':
|
'@simplewebauthn/server':
|
||||||
specifier: ^13.3.2
|
specifier: ^13.3.2
|
||||||
version: 13.3.2
|
version: 13.3.2
|
||||||
|
better-sqlite3:
|
||||||
|
specifier: ^11.10.0
|
||||||
|
version: 11.10.0
|
||||||
fastify:
|
fastify:
|
||||||
specifier: ^5.4.0
|
specifier: ^5.4.0
|
||||||
version: 5.10.0
|
version: 5.10.0
|
||||||
@@ -126,7 +129,7 @@ importers:
|
|||||||
version: 8.64.0([email protected]([email protected]))([email protected])
|
version: 8.64.0([email protected]([email protected]))([email protected])
|
||||||
vitest:
|
vitest:
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 4.1.10(@types/[email protected])(@vitest/[email protected])([email protected](@types/[email protected])([email protected]8.1)([email protected])([email protected]))
|
version: 4.1.10(@types/[email protected])(@vitest/[email protected])([email protected](@types/[email protected])([email protected]7.7)([email protected])([email protected]))
|
||||||
|
|
||||||
apps/web:
|
apps/web:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -302,7 +305,7 @@ importers:
|
|||||||
version: 8.64.0([email protected]([email protected]))([email protected])
|
version: 8.64.0([email protected]([email protected]))([email protected])
|
||||||
vitest:
|
vitest:
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 4.1.10(@types/[email protected])(@vitest/[email protected])([email protected](@types/[email protected])([email protected]7.7)([email protected])([email protected]))
|
version: 4.1.10(@types/[email protected])(@vitest/[email protected])([email protected](@types/[email protected])([email protected]8.1)([email protected])([email protected]))
|
||||||
|
|
||||||
packages/shared:
|
packages/shared:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -6813,7 +6816,7 @@ snapshots:
|
|||||||
obug: 2.1.4
|
obug: 2.1.4
|
||||||
std-env: 4.2.0
|
std-env: 4.2.0
|
||||||
tinyrainbow: 3.1.0
|
tinyrainbow: 3.1.0
|
||||||
vitest: 4.1.10(@types/node@24.13.3)(@vitest/[email protected])([email protected](@types/node@24.13.3)([email protected])([email protected])([email protected]))
|
vitest: 4.1.10(@types/node@22.20.1)(@vitest/[email protected])([email protected](@types/node@22.20.1)([email protected])([email protected])([email protected]))
|
||||||
|
|
||||||
'@vitest/[email protected]':
|
'@vitest/[email protected]':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -6824,21 +6827,13 @@ snapshots:
|
|||||||
chai: 6.2.2
|
chai: 6.2.2
|
||||||
tinyrainbow: 3.1.0
|
tinyrainbow: 3.1.0
|
||||||
|
|
||||||
'@vitest/[email protected]([email protected](@types/[email protected])([email protected]8.1)([email protected])([email protected]))':
|
'@vitest/[email protected]([email protected](@types/[email protected])([email protected]7.7)([email protected])([email protected]))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vitest/spy': 4.1.10
|
'@vitest/spy': 4.1.10
|
||||||
estree-walker: 3.0.3
|
estree-walker: 3.0.3
|
||||||
magic-string: 0.30.21
|
magic-string: 0.30.21
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
vite: 8.1.5(@types/[email protected])([email protected]8.1)([email protected])([email protected])
|
vite: 8.1.5(@types/[email protected])([email protected]7.7)([email protected])([email protected])
|
||||||
|
|
||||||
'@vitest/[email protected]([email protected](@types/[email protected])([email protected])([email protected])([email protected]))':
|
|
||||||
dependencies:
|
|
||||||
'@vitest/spy': 4.1.10
|
|
||||||
estree-walker: 3.0.3
|
|
||||||
magic-string: 0.30.21
|
|
||||||
optionalDependencies:
|
|
||||||
vite: 8.1.5(@types/[email protected])([email protected])([email protected])([email protected])
|
|
||||||
|
|
||||||
'@vitest/[email protected]([email protected](@types/[email protected])([email protected])([email protected])([email protected]))':
|
'@vitest/[email protected]([email protected](@types/[email protected])([email protected])([email protected])([email protected]))':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -9302,7 +9297,7 @@ snapshots:
|
|||||||
spdx-correct: 3.2.0
|
spdx-correct: 3.2.0
|
||||||
spdx-expression-parse: 3.0.1
|
spdx-expression-parse: 3.0.1
|
||||||
|
|
||||||
[email protected](@types/[email protected])([email protected]8.1)([email protected])([email protected]):
|
[email protected](@types/[email protected])([email protected]7.7)([email protected])([email protected]):
|
||||||
dependencies:
|
dependencies:
|
||||||
lightningcss: 1.32.0
|
lightningcss: 1.32.0
|
||||||
picomatch: 4.0.5
|
picomatch: 4.0.5
|
||||||
@@ -9311,20 +9306,6 @@ snapshots:
|
|||||||
tinyglobby: 0.2.17
|
tinyglobby: 0.2.17
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/node': 22.20.1
|
'@types/node': 22.20.1
|
||||||
esbuild: 0.28.1
|
|
||||||
fsevents: 2.3.3
|
|
||||||
jiti: 2.7.0
|
|
||||||
tsx: 4.23.1
|
|
||||||
|
|
||||||
[email protected](@types/[email protected])([email protected])([email protected])([email protected]):
|
|
||||||
dependencies:
|
|
||||||
lightningcss: 1.32.0
|
|
||||||
picomatch: 4.0.5
|
|
||||||
postcss: 8.5.19
|
|
||||||
rolldown: 1.1.5
|
|
||||||
tinyglobby: 0.2.17
|
|
||||||
optionalDependencies:
|
|
||||||
'@types/node': 24.13.3
|
|
||||||
esbuild: 0.27.7
|
esbuild: 0.27.7
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
jiti: 2.7.0
|
jiti: 2.7.0
|
||||||
@@ -9344,10 +9325,10 @@ snapshots:
|
|||||||
jiti: 2.7.0
|
jiti: 2.7.0
|
||||||
tsx: 4.23.1
|
tsx: 4.23.1
|
||||||
|
|
||||||
[email protected](@types/[email protected])(@vitest/[email protected])([email protected](@types/[email protected])([email protected]8.1)([email protected])([email protected])):
|
[email protected](@types/[email protected])(@vitest/[email protected])([email protected](@types/[email protected])([email protected]7.7)([email protected])([email protected])):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vitest/expect': 4.1.10
|
'@vitest/expect': 4.1.10
|
||||||
'@vitest/mocker': 4.1.10([email protected](@types/[email protected])([email protected]8.1)([email protected])([email protected]))
|
'@vitest/mocker': 4.1.10([email protected](@types/[email protected])([email protected]7.7)([email protected])([email protected]))
|
||||||
'@vitest/pretty-format': 4.1.10
|
'@vitest/pretty-format': 4.1.10
|
||||||
'@vitest/runner': 4.1.10
|
'@vitest/runner': 4.1.10
|
||||||
'@vitest/snapshot': 4.1.10
|
'@vitest/snapshot': 4.1.10
|
||||||
@@ -9364,7 +9345,7 @@ snapshots:
|
|||||||
tinyexec: 1.2.4
|
tinyexec: 1.2.4
|
||||||
tinyglobby: 0.2.17
|
tinyglobby: 0.2.17
|
||||||
tinyrainbow: 3.1.0
|
tinyrainbow: 3.1.0
|
||||||
vite: 8.1.5(@types/[email protected])([email protected]8.1)([email protected])([email protected])
|
vite: 8.1.5(@types/[email protected])([email protected]7.7)([email protected])([email protected])
|
||||||
why-is-node-running: 2.3.0
|
why-is-node-running: 2.3.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/node': 22.20.1
|
'@types/node': 22.20.1
|
||||||
@@ -9372,34 +9353,6 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- msw
|
- msw
|
||||||
|
|
||||||
[email protected](@types/[email protected])(@vitest/[email protected])([email protected](@types/[email protected])([email protected])([email protected])([email protected])):
|
|
||||||
dependencies:
|
|
||||||
'@vitest/expect': 4.1.10
|
|
||||||
'@vitest/mocker': 4.1.10([email protected](@types/[email protected])([email protected])([email protected])([email protected]))
|
|
||||||
'@vitest/pretty-format': 4.1.10
|
|
||||||
'@vitest/runner': 4.1.10
|
|
||||||
'@vitest/snapshot': 4.1.10
|
|
||||||
'@vitest/spy': 4.1.10
|
|
||||||
'@vitest/utils': 4.1.10
|
|
||||||
es-module-lexer: 2.3.1
|
|
||||||
expect-type: 1.4.0
|
|
||||||
magic-string: 0.30.21
|
|
||||||
obug: 2.1.4
|
|
||||||
pathe: 2.0.3
|
|
||||||
picomatch: 4.0.5
|
|
||||||
std-env: 4.2.0
|
|
||||||
tinybench: 2.9.0
|
|
||||||
tinyexec: 1.2.4
|
|
||||||
tinyglobby: 0.2.17
|
|
||||||
tinyrainbow: 3.1.0
|
|
||||||
vite: 8.1.5(@types/[email protected])([email protected])([email protected])([email protected])
|
|
||||||
why-is-node-running: 2.3.0
|
|
||||||
optionalDependencies:
|
|
||||||
'@types/node': 24.13.3
|
|
||||||
'@vitest/coverage-v8': 4.1.10([email protected])
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- msw
|
|
||||||
|
|
||||||
[email protected](@types/[email protected])(@vitest/[email protected])([email protected](@types/[email protected])([email protected])([email protected])([email protected])):
|
[email protected](@types/[email protected])(@vitest/[email protected])([email protected](@types/[email protected])([email protected])([email protected])([email protected])):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vitest/expect': 4.1.10
|
'@vitest/expect': 4.1.10
|
||||||
|
|||||||
Reference in new issue
Block a user