diff --git a/.cursor/rules/frontend/e2e-playwright.mdc b/.cursor/rules/frontend/e2e-playwright.mdc
index 3f9616f9..17cc53ff 100644
--- a/.cursor/rules/frontend/e2e-playwright.mdc
+++ b/.cursor/rules/frontend/e2e-playwright.mdc
@@ -18,11 +18,11 @@ alwaysApply: false
- Confirm dialogs use `getByRole('alertdialog')` (shadcn AlertDialog), not `dialog`
## Projects (dependency graph)
-`public` → `auth`; `public` → `setup` → `{chromium, security}`; `security` → `passkey-login`; `chromium` → `chat`
+`public` → `auth` → `setup` → `{chromium, security, passkey-login}`; `chromium` → `chat`
- **public**: no storageState
- **auth**: empty storageState; numbered `01-`…`06-` specs (file-name order is the convention — `testMatch` only selects files, it does not impose execution order)
-- **setup**: `auth.setup.ts` writes `test-results/.auth/user.json` (depends on public only)
+- **setup**: `auth.setup.ts` writes `test-results/.auth/user.json` (depends on auth)
- **chromium** / **security** / **chat**: `storageState` from setup file
- **passkey-login**: empty storageState; dedicated `e2e-passkey@test.ai`
diff --git a/.github/actions/setup-playwright/action.yml b/.github/actions/setup-playwright/action.yml
new file mode 100644
index 00000000..01bba7fb
--- /dev/null
+++ b/.github/actions/setup-playwright/action.yml
@@ -0,0 +1,16 @@
+name: Setup Playwright
+description: Cache and install Chromium with system deps for API and web E2E
+runs:
+ using: composite
+ steps:
+ - uses: actions/cache@v4
+ with:
+ path: ~/.cache/ms-playwright
+ key: ${{ runner.os }}-playwright-${{ hashFiles('**/pnpm-lock.yaml') }}
+ restore-keys: |
+ ${{ runner.os }}-playwright-
+ - name: Install Playwright Chromium
+ shell: bash
+ run: |
+ pnpm --filter @repo/api exec playwright install chromium --with-deps
+ pnpm --filter @repo/web exec playwright install chromium --with-deps
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 95b3badc..96c7498c 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -8,6 +8,10 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
+env:
+ TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
+ TURBO_TEAM: ${{ vars.TURBO_TEAM }}
+
jobs:
lint:
name: Lint Code
diff --git a/.github/workflows/packages-test.yml b/.github/workflows/packages-test.yml
index 195ab5eb..3f82a2f8 100644
--- a/.github/workflows/packages-test.yml
+++ b/.github/workflows/packages-test.yml
@@ -6,6 +6,8 @@ on:
- 'packages/**'
- 'tools/**'
- 'apps/api/openapi/**'
+ - '.github/workflows/packages-test.yml'
+ - '.github/actions/setup-pnpm/**'
- 'pnpm-lock.yaml'
- 'package.json'
- 'turbo.json'
@@ -20,6 +22,8 @@ env:
PGLITE: true
DATABASE_URL: postgresql://localhost/test
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY || 'sk-ant-dummy-for-packages-test' }}
+ TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
+ TURBO_TEAM: ${{ vars.TURBO_TEAM }}
jobs:
test:
@@ -28,6 +32,6 @@ jobs:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup-pnpm
- name: Build packages
- run: pnpm turbo run build --filter='!@repo/web' --filter='!@repo/api' --filter='!@repo/docu'
+ run: pnpm turbo run build --filter=@repo/error... --filter=@repo/core --filter=@repo/react
- name: Unit tests (packages only)
- run: pnpm turbo run test --filter='!@repo/web' --filter='!@repo/api' --filter='!@repo/docu'
+ run: pnpm turbo run test --filter=@repo/core --filter=@repo/react --filter=@repo/error
diff --git a/.github/workflows/web-e2e.yml b/.github/workflows/web-e2e.yml
index dd203cee..56738eff 100644
--- a/.github/workflows/web-e2e.yml
+++ b/.github/workflows/web-e2e.yml
@@ -4,6 +4,10 @@ on:
pull_request:
paths:
- 'apps/web/**'
+ - 'apps/api/src/routes/auth/**'
+ - 'apps/api/src/routes/account/**'
+ - 'apps/api/src/routes/ai/**'
+ - 'apps/api/src/routes/health/**'
- 'apps/api/src/routes/test/**'
- 'scripts/**'
- 'packages/core/**'
@@ -11,9 +15,12 @@ on:
- 'packages/ui/**'
- 'packages/utils/**'
- 'packages/error/**'
- - 'tools/eslint-config/**'
- - 'tools/typescript-config/**'
+ - 'tools/eslint/**'
+ - 'tools/typescript/**'
- 'apps/api/openapi/**'
+ - '.github/workflows/web-e2e.yml'
+ - '.github/actions/setup-pnpm/**'
+ - '.github/actions/setup-playwright/**'
- 'pnpm-lock.yaml'
- 'package.json'
- 'turbo.json'
@@ -34,6 +41,8 @@ env:
NEXT_PUBLIC_API_URL: http://localhost:3001
AI_PROVIDER: anthropic
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
+ TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
+ TURBO_TEAM: ${{ vars.TURBO_TEAM }}
jobs:
build-and-test:
@@ -49,14 +58,7 @@ jobs:
${{ runner.os }}-next-web-
- name: Build
run: pnpm turbo run build --filter=@repo/web...
- - uses: actions/cache@v4
- with:
- path: ~/.cache/ms-playwright
- key: ${{ runner.os }}-playwright-${{ hashFiles('**/pnpm-lock.yaml') }}
- restore-keys: |
- ${{ runner.os }}-playwright-
- - name: Install Playwright
- run: pnpm --filter @repo/web exec playwright install chromium --with-deps
+ - uses: ./.github/actions/setup-playwright
- name: Verify ANTHROPIC_API_KEY for E2E (AI_PROVIDER=anthropic)
shell: bash
env:
diff --git a/README.md b/README.md
index c6bf134e..cb6b2228 100644
--- a/README.md
+++ b/README.md
@@ -53,7 +53,7 @@ Run with `pnpm ')).toBe(false)
+ expect(isAllowedUrl('file:///etc/passwd')).toBe(false)
+ })
+
+ it('rejects invalid URL format', () => {
+ expect(isAllowedUrl('not-a-valid-url')).toBe(false)
+ })
+})
diff --git a/apps/api/src/lib/url.test.ts b/apps/api/src/lib/url.test.ts
deleted file mode 100644
index 9e763ed3..00000000
--- a/apps/api/src/lib/url.test.ts
+++ /dev/null
@@ -1,73 +0,0 @@
-import { describe, expect, it } from 'vitest'
-import { appendCodeToCallbackUrl, isAllowedUrl } from './url.js'
-
-describe('appendCodeToCallbackUrl', () => {
- it('appends code as query param when URL has no query or fragment', () => {
- const result = appendCodeToCallbackUrl('https://example.com/auth/callback', 'abc123')
- expect(result).toBe('https://example.com/auth/callback?code=abc123')
- })
-
- it('appends code with & when URL has existing query', () => {
- const result = appendCodeToCallbackUrl('https://example.com/auth/callback?foo=bar', 'abc123')
- expect(result).toBe('https://example.com/auth/callback?foo=bar&code=abc123')
- })
-
- it('places code in query and reattaches fragment', () => {
- const result = appendCodeToCallbackUrl('https://example.com/auth/callback#section', 'abc123')
- expect(result).toBe('https://example.com/auth/callback?code=abc123#section')
- const parsed = new URL(result)
- expect(parsed.searchParams.get('code')).toBe('abc123')
- expect(parsed.hash).toBe('#section')
- })
-
- it('preserves fragment when URL has both query and fragment', () => {
- const result = appendCodeToCallbackUrl(
- 'https://example.com/auth/callback?x=1#section',
- 'abc123',
- )
- expect(result).toBe('https://example.com/auth/callback?x=1&code=abc123#section')
- })
-
- it('encodes code for URL safety', () => {
- const result = appendCodeToCallbackUrl('https://example.com/auth/callback', 'a+b=c&d')
- expect(result).toBe('https://example.com/auth/callback?code=a%2Bb%3Dc%26d')
- })
-})
-
-describe('isAllowedUrl', () => {
- it('accepts valid https URL when origin is allowlisted', () => {
- expect(isAllowedUrl('https://example.com/callback')).toBe(true)
- })
-
- it('accepts valid http URL when origin is allowlisted', () => {
- expect(isAllowedUrl('http://localhost:3000/auth/callback')).toBe(true)
- })
-
- it('rejects URL whose origin is not allowlisted', () => {
- expect(isAllowedUrl('https://evil.example/callback')).toBe(false)
- })
-
- it('rejects empty string', () => {
- expect(isAllowedUrl('')).toBe(false)
- })
-
- it('rejects whitespace-only string', () => {
- expect(isAllowedUrl(' ')).toBe(false)
- })
-
- it('rejects relative URL', () => {
- expect(isAllowedUrl('/callback')).toBe(false)
- expect(isAllowedUrl('./callback')).toBe(false)
- expect(isAllowedUrl('../auth/callback')).toBe(false)
- })
-
- it('rejects non-HTTP(S) schemes', () => {
- expect(isAllowedUrl('javascript:alert(1)')).toBe(false)
- expect(isAllowedUrl('data:text/html,')).toBe(false)
- expect(isAllowedUrl('file:///etc/passwd')).toBe(false)
- })
-
- it('rejects invalid URL format', () => {
- expect(isAllowedUrl('not-a-valid-url')).toBe(false)
- })
-})
diff --git a/apps/api/src/routes/auth/magiclink.spec.ts b/apps/api/src/routes/auth/magiclink/magiclink.spec.ts
similarity index 60%
rename from apps/api/src/routes/auth/magiclink.spec.ts
rename to apps/api/src/routes/auth/magiclink/magiclink.spec.ts
index c0e809ff..17e32a75 100644
--- a/apps/api/src/routes/auth/magiclink.spec.ts
+++ b/apps/api/src/routes/auth/magiclink/magiclink.spec.ts
@@ -1,7 +1,7 @@
import { afterAll, beforeAll } from 'vitest'
-import { cleanupGroupDatabase, setupGroupDatabase } from '../../../test/utils/db-setup.js'
-import type { TestApp } from '../../../test/utils/fastify.js'
-import { buildTestApp } from '../../../test/utils/fastify.js'
+import { cleanupGroupDatabase, setupGroupDatabase } from '../../../../test/utils/db-setup.js'
+import type { TestApp } from '../../../../test/utils/fastify.js'
+import { buildTestApp } from '../../../../test/utils/fastify.js'
let fastify: TestApp
@@ -17,5 +17,5 @@ afterAll(async () => {
export { fastify }
-import './magiclink/request.test'
-import './magiclink/verify.test'
+import './request.test'
+import './verify.test'
diff --git a/apps/api/src/routes/auth/magiclink/request.test.ts b/apps/api/src/routes/auth/magiclink/request.test.ts
index d594fcc7..60343438 100644
--- a/apps/api/src/routes/auth/magiclink/request.test.ts
+++ b/apps/api/src/routes/auth/magiclink/request.test.ts
@@ -1,5 +1,5 @@
import { beforeEach, describe, expect, it } from 'vitest'
-import { fastify } from '../magiclink.spec.js'
+import { fastify } from './magiclink.spec.js'
describe('POST /auth/magiclink/request', () => {
beforeEach(() => {
diff --git a/apps/api/src/routes/auth/magiclink/verify.test.ts b/apps/api/src/routes/auth/magiclink/verify.test.ts
index c836388e..79526249 100644
--- a/apps/api/src/routes/auth/magiclink/verify.test.ts
+++ b/apps/api/src/routes/auth/magiclink/verify.test.ts
@@ -5,7 +5,7 @@ import { getDb } from '../../../../src/db/index.js'
import { authAttempts, users, verification } from '../../../../src/db/schema/index.js'
import { hashToken } from '../../../../src/lib/jwt.js'
import { getStoredMagicLink } from '../../../../test/utils/auth-helper.js'
-import { fastify } from '../magiclink.spec.js'
+import { fastify } from './magiclink.spec.js'
describe('POST /auth/magiclink/verify', () => {
beforeEach(() => {
diff --git a/apps/api/src/routes/auth/oauth.spec.ts b/apps/api/src/routes/auth/oauth.spec.ts
deleted file mode 100644
index 6a25b4ba..00000000
--- a/apps/api/src/routes/auth/oauth.spec.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-import { afterAll, beforeAll } from 'vitest'
-import { cleanupGroupDatabase, setupGroupDatabase } from '../../../test/utils/db-setup.js'
-import type { TestApp } from '../../../test/utils/fastify.js'
-import { buildTestApp } from '../../../test/utils/fastify.js'
-
-let fastify: TestApp
-
-beforeAll(async () => {
- await setupGroupDatabase()
- fastify = await buildTestApp()
-})
-
-afterAll(async () => {
- if (fastify) await fastify.close()
- await cleanupGroupDatabase()
-})
-
-export { fastify }
-
-import './oauth/github/link-authorize-url.test'
-import './oauth/google/link-authorize-url.test'
-import './oauth/facebook/link-authorize-url.test'
-import './oauth/twitter/link-authorize-url.test'
-import './oauth/facebook/authorize-url.test'
-import './oauth/providers.test'
-import './oauth/facebook/exchange.test'
-import './oauth/github/authorize-url.test'
-import './oauth/github/exchange.test'
-import './oauth/google/authorize-url.test'
-import './oauth/google/exchange.test'
-import './oauth/google/verify-id-token.test'
-import './oauth/twitter/authorize-url.test'
-import './oauth/twitter/exchange.test'
diff --git a/apps/api/src/routes/auth/oauth/facebook/authorize-url.test.ts b/apps/api/src/routes/auth/oauth/facebook/authorize-url.test.ts
index 64a6b02b..5570c512 100644
--- a/apps/api/src/routes/auth/oauth/facebook/authorize-url.test.ts
+++ b/apps/api/src/routes/auth/oauth/facebook/authorize-url.test.ts
@@ -13,7 +13,7 @@ vi.mock('../../../../lib/env.js', async importOriginal => {
}
})
-import { fastify } from '../../oauth.spec.js'
+import { fastify } from '../oauth.spec.js'
describe('GET /auth/oauth/facebook/authorize-url', () => {
it('returns 503 when Facebook OAuth is not configured', async () => {
diff --git a/apps/api/src/routes/auth/oauth/facebook/exchange.test.ts b/apps/api/src/routes/auth/oauth/facebook/exchange.test.ts
index efd61f87..c878615b 100644
--- a/apps/api/src/routes/auth/oauth/facebook/exchange.test.ts
+++ b/apps/api/src/routes/auth/oauth/facebook/exchange.test.ts
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
-import { fastify } from '../../oauth.spec.js'
+import { fastify } from '../oauth.spec.js'
describe('POST /auth/oauth/facebook/exchange', () => {
it('returns 503 when Facebook OAuth is not configured', async () => {
diff --git a/apps/api/src/routes/auth/oauth/facebook/link-authorize-url.test.ts b/apps/api/src/routes/auth/oauth/facebook/link-authorize-url.test.ts
index ec17ccf3..596a012a 100644
--- a/apps/api/src/routes/auth/oauth/facebook/link-authorize-url.test.ts
+++ b/apps/api/src/routes/auth/oauth/facebook/link-authorize-url.test.ts
@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest'
import { getOrCreateSession } from '../../../../../test/utils/auth-helper.js'
-import { fastify } from '../../oauth.spec.js'
+import { fastify } from '../oauth.spec.js'
describe('GET /auth/oauth/facebook/link-authorize-url', () => {
it('returns 401 without Bearer token', async () => {
diff --git a/apps/api/src/routes/auth/oauth/github/authorize-url.test.ts b/apps/api/src/routes/auth/oauth/github/authorize-url.test.ts
index 44c53372..b1989a24 100644
--- a/apps/api/src/routes/auth/oauth/github/authorize-url.test.ts
+++ b/apps/api/src/routes/auth/oauth/github/authorize-url.test.ts
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
-import { fastify } from '../../oauth.spec.js'
+import { fastify } from '../oauth.spec.js'
describe('GET /auth/oauth/github/authorize-url', () => {
it('returns 503 when GitHub OAuth is not configured', async () => {
diff --git a/apps/api/src/routes/auth/oauth/github/exchange.test.ts b/apps/api/src/routes/auth/oauth/github/exchange.test.ts
index c6a8932f..f4ca36fb 100644
--- a/apps/api/src/routes/auth/oauth/github/exchange.test.ts
+++ b/apps/api/src/routes/auth/oauth/github/exchange.test.ts
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
-import { fastify } from '../../oauth.spec.js'
+import { fastify } from '../oauth.spec.js'
import { resolveGitHubVerifiedEmail } from './exchange.js'
describe('resolveGitHubVerifiedEmail', () => {
diff --git a/apps/api/src/routes/auth/oauth/github/link-authorize-url.test.ts b/apps/api/src/routes/auth/oauth/github/link-authorize-url.test.ts
index 112d2d98..e78b891f 100644
--- a/apps/api/src/routes/auth/oauth/github/link-authorize-url.test.ts
+++ b/apps/api/src/routes/auth/oauth/github/link-authorize-url.test.ts
@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest'
import { getOrCreateSession } from '../../../../../test/utils/auth-helper.js'
-import { fastify } from '../../oauth.spec.js'
+import { fastify } from '../oauth.spec.js'
describe('GET /auth/oauth/github/link-authorize-url', () => {
it('returns 401 without Bearer token', async () => {
diff --git a/apps/api/src/routes/auth/oauth/google/authorize-url.test.ts b/apps/api/src/routes/auth/oauth/google/authorize-url.test.ts
index 3f39653c..bf08ebd1 100644
--- a/apps/api/src/routes/auth/oauth/google/authorize-url.test.ts
+++ b/apps/api/src/routes/auth/oauth/google/authorize-url.test.ts
@@ -13,7 +13,7 @@ vi.mock('../../../../lib/env.js', async importOriginal => {
}
})
-import { fastify } from '../../oauth.spec.js'
+import { fastify } from '../oauth.spec.js'
describe('GET /auth/oauth/google/authorize-url', () => {
it('returns 503 when Google OAuth redirect is not configured', async () => {
diff --git a/apps/api/src/routes/auth/oauth/google/exchange.test.ts b/apps/api/src/routes/auth/oauth/google/exchange.test.ts
index f6c09fcf..25f4f55c 100644
--- a/apps/api/src/routes/auth/oauth/google/exchange.test.ts
+++ b/apps/api/src/routes/auth/oauth/google/exchange.test.ts
@@ -13,7 +13,7 @@ vi.mock('../../../../lib/env.js', async importOriginal => {
}
})
-import { fastify } from '../../oauth.spec.js'
+import { fastify } from '../oauth.spec.js'
describe('POST /auth/oauth/google/exchange', () => {
it('returns 503 when Google OAuth redirect is not configured', async () => {
diff --git a/apps/api/src/routes/auth/oauth/google/link-authorize-url.test.ts b/apps/api/src/routes/auth/oauth/google/link-authorize-url.test.ts
index 82946449..eda8b194 100644
--- a/apps/api/src/routes/auth/oauth/google/link-authorize-url.test.ts
+++ b/apps/api/src/routes/auth/oauth/google/link-authorize-url.test.ts
@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest'
import { getOrCreateSession } from '../../../../../test/utils/auth-helper.js'
-import { fastify } from '../../oauth.spec.js'
+import { fastify } from '../oauth.spec.js'
describe('GET /auth/oauth/google/link-authorize-url', () => {
it('returns 401 without Bearer token', async () => {
diff --git a/apps/api/src/routes/auth/oauth/google/verify-id-token.test.ts b/apps/api/src/routes/auth/oauth/google/verify-id-token.test.ts
index 521fb199..0bc4dec2 100644
--- a/apps/api/src/routes/auth/oauth/google/verify-id-token.test.ts
+++ b/apps/api/src/routes/auth/oauth/google/verify-id-token.test.ts
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
-import { fastify } from '../../oauth.spec.js'
+import { fastify } from '../oauth.spec.js'
describe('POST /auth/oauth/google/verify-id-token', () => {
it('returns 503 when Google OAuth is not configured', async () => {
diff --git a/apps/api/src/routes/auth/oauth/oauth.spec.ts b/apps/api/src/routes/auth/oauth/oauth.spec.ts
new file mode 100644
index 00000000..f536571a
--- /dev/null
+++ b/apps/api/src/routes/auth/oauth/oauth.spec.ts
@@ -0,0 +1,33 @@
+import { afterAll, beforeAll } from 'vitest'
+import { cleanupGroupDatabase, setupGroupDatabase } from '../../../../test/utils/db-setup.js'
+import type { TestApp } from '../../../../test/utils/fastify.js'
+import { buildTestApp } from '../../../../test/utils/fastify.js'
+
+let fastify: TestApp
+
+beforeAll(async () => {
+ await setupGroupDatabase()
+ fastify = await buildTestApp()
+})
+
+afterAll(async () => {
+ if (fastify) await fastify.close()
+ await cleanupGroupDatabase()
+})
+
+export { fastify }
+
+import './github/link-authorize-url.test'
+import './google/link-authorize-url.test'
+import './facebook/link-authorize-url.test'
+import './twitter/link-authorize-url.test'
+import './facebook/authorize-url.test'
+import './providers.test'
+import './facebook/exchange.test'
+import './github/authorize-url.test'
+import './github/exchange.test'
+import './google/authorize-url.test'
+import './google/exchange.test'
+import './google/verify-id-token.test'
+import './twitter/authorize-url.test'
+import './twitter/exchange.test'
diff --git a/apps/api/src/routes/auth/oauth/providers.test.ts b/apps/api/src/routes/auth/oauth/providers.test.ts
index 2092752d..52e3bbac 100644
--- a/apps/api/src/routes/auth/oauth/providers.test.ts
+++ b/apps/api/src/routes/auth/oauth/providers.test.ts
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
-import { fastify } from '../oauth.spec.js'
+import { fastify } from './oauth.spec.js'
describe('GET /auth/oauth/providers', () => {
it('returns which OAuth providers are configured', async () => {
diff --git a/apps/api/src/routes/auth/oauth/twitter/authorize-url.test.ts b/apps/api/src/routes/auth/oauth/twitter/authorize-url.test.ts
index 9898e2c5..fcc05c85 100644
--- a/apps/api/src/routes/auth/oauth/twitter/authorize-url.test.ts
+++ b/apps/api/src/routes/auth/oauth/twitter/authorize-url.test.ts
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
-import { fastify } from '../../oauth.spec.js'
+import { fastify } from '../oauth.spec.js'
describe('GET /auth/oauth/twitter/authorize-url', () => {
it('returns 503 when Twitter OAuth is not configured', async () => {
diff --git a/apps/api/src/routes/auth/oauth/twitter/exchange.test.ts b/apps/api/src/routes/auth/oauth/twitter/exchange.test.ts
index 2c66a713..9f3ebffa 100644
--- a/apps/api/src/routes/auth/oauth/twitter/exchange.test.ts
+++ b/apps/api/src/routes/auth/oauth/twitter/exchange.test.ts
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
-import { fastify } from '../../oauth.spec.js'
+import { fastify } from '../oauth.spec.js'
describe('POST /auth/oauth/twitter/exchange', () => {
it('returns 503 when Twitter OAuth is not configured', async () => {
diff --git a/apps/api/src/routes/auth/oauth/twitter/link-authorize-url.test.ts b/apps/api/src/routes/auth/oauth/twitter/link-authorize-url.test.ts
index 8ba086c0..b4018943 100644
--- a/apps/api/src/routes/auth/oauth/twitter/link-authorize-url.test.ts
+++ b/apps/api/src/routes/auth/oauth/twitter/link-authorize-url.test.ts
@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest'
import { getOrCreateSession } from '../../../../../test/utils/auth-helper.js'
-import { fastify } from '../../oauth.spec.js'
+import { fastify } from '../oauth.spec.js'
describe('GET /auth/oauth/twitter/link-authorize-url', () => {
it('returns 401 without Bearer token', async () => {
diff --git a/apps/api/src/routes/auth/passkey/exchange.test.ts b/apps/api/src/routes/auth/passkey/exchange.test.ts
index c8aeafe6..5aee2258 100644
--- a/apps/api/src/routes/auth/passkey/exchange.test.ts
+++ b/apps/api/src/routes/auth/passkey/exchange.test.ts
@@ -5,7 +5,7 @@ import { encryptCallbackTokens } from '../../../db/callback-tokens.js'
import { getDb } from '../../../db/index.js'
import { passkeyCallback } from '../../../db/schema/index.js'
import { generateToken, hashToken } from '../../../lib/jwt.js'
-import { fastify } from '../passkey.spec.js'
+import { fastify } from './passkey.spec.js'
const testCallbackOrigin = 'http://localhost:3000'
diff --git a/apps/api/src/routes/auth/passkey.spec.ts b/apps/api/src/routes/auth/passkey/passkey.spec.ts
similarity index 54%
rename from apps/api/src/routes/auth/passkey.spec.ts
rename to apps/api/src/routes/auth/passkey/passkey.spec.ts
index ad718557..a796f635 100644
--- a/apps/api/src/routes/auth/passkey.spec.ts
+++ b/apps/api/src/routes/auth/passkey/passkey.spec.ts
@@ -1,7 +1,7 @@
import { afterAll, beforeAll } from 'vitest'
-import { cleanupGroupDatabase, setupGroupDatabase } from '../../../test/utils/db-setup.js'
-import type { TestApp } from '../../../test/utils/fastify.js'
-import { buildTestApp } from '../../../test/utils/fastify.js'
+import { cleanupGroupDatabase, setupGroupDatabase } from '../../../../test/utils/db-setup.js'
+import type { TestApp } from '../../../../test/utils/fastify.js'
+import { buildTestApp } from '../../../../test/utils/fastify.js'
let fastify: TestApp
@@ -17,7 +17,7 @@ afterAll(async () => {
export { fastify }
-import './passkey/start.test'
-import './passkey/verify.test'
-import './passkey/exchange.test'
-import './passkey/resolve-user.test'
+import './start.test'
+import './verify.test'
+import './exchange.test'
+import './resolve-user.test'
diff --git a/apps/api/src/routes/auth/passkey/resolve-user.test.ts b/apps/api/src/routes/auth/passkey/resolve-user.test.ts
index 28fee2fa..2a351908 100644
--- a/apps/api/src/routes/auth/passkey/resolve-user.test.ts
+++ b/apps/api/src/routes/auth/passkey/resolve-user.test.ts
@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest'
import { getOrCreateSession, insertTestPasskey } from '../../../../test/utils/auth-helper.js'
-import { fastify } from '../passkey.spec.js'
+import { fastify } from './passkey.spec.js'
describe('POST /auth/passkey/resolve-user', () => {
it('should return 400 for userHandle with no matching passkey user', async () => {
diff --git a/apps/api/src/routes/auth/passkey/start.test.ts b/apps/api/src/routes/auth/passkey/start.test.ts
index 61be68be..f60a8720 100644
--- a/apps/api/src/routes/auth/passkey/start.test.ts
+++ b/apps/api/src/routes/auth/passkey/start.test.ts
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
-import { fastify } from '../passkey.spec.js'
+import { fastify } from './passkey.spec.js'
describe('POST /auth/passkey/start', () => {
it('should return 400 for missing Origin', async () => {
diff --git a/apps/api/src/routes/auth/passkey/verify.test.ts b/apps/api/src/routes/auth/passkey/verify.test.ts
index 2819740a..6a25ff29 100644
--- a/apps/api/src/routes/auth/passkey/verify.test.ts
+++ b/apps/api/src/routes/auth/passkey/verify.test.ts
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
-import { fastify } from '../passkey.spec.js'
+import { fastify } from './passkey.spec.js'
const minimalAssertion = {
id: 'fake',
diff --git a/apps/api/src/routes/auth/session/logout.test.ts b/apps/api/src/routes/auth/session/logout.test.ts
index 9a57e99f..e533b8a3 100644
--- a/apps/api/src/routes/auth/session/logout.test.ts
+++ b/apps/api/src/routes/auth/session/logout.test.ts
@@ -2,7 +2,7 @@ import { beforeEach, describe, expect, it } from 'vitest'
import { getApiKeyToken } from '../../../../test/utils/auth-helper.js'
import { getDb } from '../../../db/index.js'
import { sessions } from '../../../db/schema/index.js'
-import { fastify } from '../session.spec.js'
+import { fastify } from './session.spec.js'
describe('POST /auth/session/logout', () => {
beforeEach(() => {
diff --git a/apps/api/src/routes/auth/session/refresh.test.ts b/apps/api/src/routes/auth/session/refresh.test.ts
index 256d50dc..ef393431 100644
--- a/apps/api/src/routes/auth/session/refresh.test.ts
+++ b/apps/api/src/routes/auth/session/refresh.test.ts
@@ -6,7 +6,7 @@ import { getDb } from '../../../db/index.js'
import { sessions, users } from '../../../db/schema/index.js'
import { env } from '../../../lib/env.js'
import { hashToken } from '../../../lib/jwt.js'
-import { fastify } from '../session.spec.js'
+import { fastify } from './session.spec.js'
function decodeJwtPayload>(token: string) {
const payload = token.split('.')[1]
diff --git a/apps/api/src/routes/auth/session.spec.ts b/apps/api/src/routes/auth/session/session.spec.ts
similarity index 51%
rename from apps/api/src/routes/auth/session.spec.ts
rename to apps/api/src/routes/auth/session/session.spec.ts
index 49a37fad..6738f912 100644
--- a/apps/api/src/routes/auth/session.spec.ts
+++ b/apps/api/src/routes/auth/session/session.spec.ts
@@ -1,7 +1,7 @@
import { afterAll, beforeAll } from 'vitest'
-import { cleanupGroupDatabase, setupGroupDatabase } from '../../../test/utils/db-setup.js'
-import type { TestApp } from '../../../test/utils/fastify.js'
-import { buildTestApp } from '../../../test/utils/fastify.js'
+import { cleanupGroupDatabase, setupGroupDatabase } from '../../../../test/utils/db-setup.js'
+import type { TestApp } from '../../../../test/utils/fastify.js'
+import { buildTestApp } from '../../../../test/utils/fastify.js'
let fastify: TestApp
@@ -17,8 +17,8 @@ afterAll(async () => {
export { fastify }
-import './session/logout.test'
-import './session/refresh.test'
-import './session/sessions-token.test'
-import './session/user.test'
-import './session/validate-tokens.test'
+import './logout.test'
+import './refresh.test'
+import './sessions-token.test'
+import './user.test'
+import './validate-tokens.test'
diff --git a/apps/api/src/routes/auth/session/user.test.ts b/apps/api/src/routes/auth/session/user.test.ts
index 38c931b2..3da44abe 100644
--- a/apps/api/src/routes/auth/session/user.test.ts
+++ b/apps/api/src/routes/auth/session/user.test.ts
@@ -3,7 +3,7 @@ import { beforeEach, describe, expect, it } from 'vitest'
import { createAuthenticatedUser, getWeb3Session } from '../../../../test/utils/auth-helper.js'
import { env } from '../../../lib/env.js'
import { createAccessTokenPayload } from '../../../lib/jwt.js'
-import { fastify } from '../session.spec.js'
+import { fastify } from './session.spec.js'
function decodeJwtPayload>(token: string) {
const payload = token.split('.')[1]
diff --git a/apps/api/src/routes/auth/session/validate-tokens.test.ts b/apps/api/src/routes/auth/session/validate-tokens.test.ts
index 594f06fc..b265fa3e 100644
--- a/apps/api/src/routes/auth/session/validate-tokens.test.ts
+++ b/apps/api/src/routes/auth/session/validate-tokens.test.ts
@@ -3,7 +3,7 @@ import { beforeEach, describe, expect, it } from 'vitest'
import { getDb } from '../../../db/index.js'
import { sessions } from '../../../db/schema/index.js'
import { hashToken } from '../../../lib/jwt.js'
-import { fastify } from '../session.spec.js'
+import { fastify } from './session.spec.js'
async function getTokenPair(email = 'validate-tokens@example.com') {
await fastify.inject({
diff --git a/apps/api/src/routes/auth/web3.spec.ts b/apps/api/src/routes/auth/web3.spec.ts
deleted file mode 100644
index 80646183..00000000
--- a/apps/api/src/routes/auth/web3.spec.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import { afterAll, beforeAll } from 'vitest'
-import { cleanupGroupDatabase, setupGroupDatabase } from '../../../test/utils/db-setup.js'
-import type { TestApp } from '../../../test/utils/fastify.js'
-import { buildTestApp } from '../../../test/utils/fastify.js'
-
-let fastify: TestApp
-
-beforeAll(async () => {
- await setupGroupDatabase()
- fastify = await buildTestApp()
-})
-
-afterAll(async () => {
- if (fastify) await fastify.close()
- await cleanupGroupDatabase()
-})
-
-export { fastify }
-
-import './web3/exchange.test'
-import './web3/nonce.test'
-import './web3/eip155/nonce.test'
-import './web3/eip155/verify.test'
-import './web3/solana/nonce.test'
-import './web3/solana/verify.test'
-import '../../lib/web3/domain.test.js'
diff --git a/apps/api/src/routes/auth/web3/eip155/nonce.test.ts b/apps/api/src/routes/auth/web3/eip155/nonce.test.ts
index 54821c21..92dc619b 100644
--- a/apps/api/src/routes/auth/web3/eip155/nonce.test.ts
+++ b/apps/api/src/routes/auth/web3/eip155/nonce.test.ts
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
-import { fastify } from '../../web3.spec.js'
+import { fastify } from '../web3.spec.js'
describe('GET /auth/web3/eip155/nonce', () => {
it('should return nonce for valid Ethereum address', async () => {
diff --git a/apps/api/src/routes/auth/web3/eip155/verify.test.ts b/apps/api/src/routes/auth/web3/eip155/verify.test.ts
index 7e6c5c3d..da689f18 100644
--- a/apps/api/src/routes/auth/web3/eip155/verify.test.ts
+++ b/apps/api/src/routes/auth/web3/eip155/verify.test.ts
@@ -1,7 +1,7 @@
import { privateKeyToAccount } from 'viem/accounts'
import { createSiweMessage } from 'viem/siwe'
import { describe, expect, it } from 'vitest'
-import { fastify } from '../../web3.spec.js'
+import { fastify } from '../web3.spec.js'
const testPrivateKey =
'0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80' as `0x${string}`
diff --git a/apps/api/src/routes/auth/web3/exchange.test.ts b/apps/api/src/routes/auth/web3/exchange.test.ts
index 5153d57a..cb2465e1 100644
--- a/apps/api/src/routes/auth/web3/exchange.test.ts
+++ b/apps/api/src/routes/auth/web3/exchange.test.ts
@@ -5,7 +5,7 @@ import { encryptCallbackTokens } from '../../../db/callback-tokens.js'
import { getDb } from '../../../db/index.js'
import { web3Callback } from '../../../db/schema/index.js'
import { generateToken, hashToken } from '../../../lib/jwt.js'
-import { fastify } from '../web3.spec.js'
+import { fastify } from './web3.spec.js'
describe('POST /auth/web3/exchange', () => {
it('should return tokens for valid code and delete row', async () => {
diff --git a/apps/api/src/routes/auth/web3/nonce.test.ts b/apps/api/src/routes/auth/web3/nonce.test.ts
index 41739f3f..4d6c4c4b 100644
--- a/apps/api/src/routes/auth/web3/nonce.test.ts
+++ b/apps/api/src/routes/auth/web3/nonce.test.ts
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
-import { fastify } from '../web3.spec.js'
+import { fastify } from './web3.spec.js'
const validEthAddress = '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e'
const validSolanaAddress = '4Cw1koUQtqybLFem7uqhzMBznMPGARbFS4cjaYbM9RnR'
diff --git a/apps/api/src/routes/auth/web3/solana/nonce.test.ts b/apps/api/src/routes/auth/web3/solana/nonce.test.ts
index d8c7ca41..fe9f86d7 100644
--- a/apps/api/src/routes/auth/web3/solana/nonce.test.ts
+++ b/apps/api/src/routes/auth/web3/solana/nonce.test.ts
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
-import { fastify } from '../../web3.spec.js'
+import { fastify } from '../web3.spec.js'
const validSolanaAddress = '4Cw1koUQtqybLFem7uqhzMBznMPGARbFS4cjaYbM9RnR'
diff --git a/apps/api/src/routes/auth/web3/solana/verify.test.ts b/apps/api/src/routes/auth/web3/solana/verify.test.ts
index e5cbf7e6..7734ad8e 100644
--- a/apps/api/src/routes/auth/web3/solana/verify.test.ts
+++ b/apps/api/src/routes/auth/web3/solana/verify.test.ts
@@ -2,7 +2,7 @@ import { Keypair } from '@solana/web3.js'
import bs58 from 'bs58'
import * as nacl from 'tweetnacl'
import { describe, expect, it } from 'vitest'
-import { fastify } from '../../web3.spec.js'
+import { fastify } from '../web3.spec.js'
function buildSiwsMessage({
domain,
diff --git a/apps/api/src/routes/auth/web3/web3.spec.ts b/apps/api/src/routes/auth/web3/web3.spec.ts
new file mode 100644
index 00000000..27621547
--- /dev/null
+++ b/apps/api/src/routes/auth/web3/web3.spec.ts
@@ -0,0 +1,26 @@
+import { afterAll, beforeAll } from 'vitest'
+import { cleanupGroupDatabase, setupGroupDatabase } from '../../../../test/utils/db-setup.js'
+import type { TestApp } from '../../../../test/utils/fastify.js'
+import { buildTestApp } from '../../../../test/utils/fastify.js'
+
+let fastify: TestApp
+
+beforeAll(async () => {
+ await setupGroupDatabase()
+ fastify = await buildTestApp()
+})
+
+afterAll(async () => {
+ if (fastify) await fastify.close()
+ await cleanupGroupDatabase()
+})
+
+export { fastify }
+
+import './exchange.test'
+import './nonce.test'
+import './eip155/nonce.test'
+import './eip155/verify.test'
+import './solana/nonce.test'
+import './solana/verify.test'
+import '../../../lib/web3/domain.test'
diff --git a/apps/api/src/routes/health.spec.ts b/apps/api/src/routes/health.spec.ts
index 48a92e89..9d601277 100644
--- a/apps/api/src/routes/health.spec.ts
+++ b/apps/api/src/routes/health.spec.ts
@@ -21,63 +21,20 @@ describe('GET /health', () => {
await cleanupGroupDatabase()
})
- it('should return 200 status', async () => {
+ it('should return 200 with ok and dbReady fields', async () => {
const response = await fastify.inject({
method: 'GET',
url: '/health',
})
expect(response.statusCode).toBe(200)
- })
-
- it('should return response matching schema structure', async () => {
- const response = await fastify.inject({
- method: 'GET',
- url: '/health',
- })
-
const data = JSON.parse(response.body)
-
expect(data).toMatchObject({
ok: true,
dbReady: expect.any(Boolean),
})
})
- it('should return ok field as true', async () => {
- const response = await fastify.inject({
- method: 'GET',
- url: '/health',
- })
-
- const data = JSON.parse(response.body)
-
- expect(data.ok).toBe(true)
- })
-
- it('should return dbReady field as boolean', async () => {
- const response = await fastify.inject({
- method: 'GET',
- url: '/health',
- })
-
- const data = JSON.parse(response.body)
-
- expect(typeof data.dbReady).toBe('boolean')
- })
-
- it('should validate response against HealthResponseSchema', async () => {
- const response = await fastify.inject({
- method: 'GET',
- url: '/health',
- })
-
- const data = JSON.parse(response.body)
-
- expect(data.ok).toBe(true)
- expect(typeof data.dbReady).toBe('boolean')
- })
-
it('should include security headers but not HSTS in non-production', async () => {
const response = await fastify.inject({
method: 'GET',
diff --git a/apps/api/src/routes/reference.spec.ts b/apps/api/src/routes/reference/reference.spec.ts
similarity index 89%
rename from apps/api/src/routes/reference.spec.ts
rename to apps/api/src/routes/reference/reference.spec.ts
index db81f70a..105a95a7 100644
--- a/apps/api/src/routes/reference.spec.ts
+++ b/apps/api/src/routes/reference/reference.spec.ts
@@ -1,8 +1,8 @@
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
-import { getStoredMagicLink } from '../../test/utils/auth-helper.js'
-import { cleanupGroupDatabase, setupGroupDatabase } from '../../test/utils/db-setup.js'
-import type { TestApp } from '../../test/utils/fastify.js'
-import { buildTestApp } from '../../test/utils/fastify.js'
+import { getStoredMagicLink } from '../../../test/utils/auth-helper.js'
+import { cleanupGroupDatabase, setupGroupDatabase } from '../../../test/utils/db-setup.js'
+import type { TestApp } from '../../../test/utils/fastify.js'
+import { buildTestApp } from '../../../test/utils/fastify.js'
describe('GET /reference', () => {
let fastify: TestApp
diff --git a/apps/api/test/utils/db-setup.ts b/apps/api/test/utils/db-setup.ts
index e328f025..5ad2f0af 100644
--- a/apps/api/test/utils/db-setup.ts
+++ b/apps/api/test/utils/db-setup.ts
@@ -25,9 +25,8 @@
* - `setupGroupDatabase()`: Creates DB instance, runs migrations, returns cleanup function
* - `cleanupGroupDatabase()`: Closes DB instance and cleans up directory
*
- * Each group entry file shares the worker database instance (maxWorkers: 1).
- * Tests within the same group share the same database instance.
- * Different groups run sequentially in the same worker with table truncation between groups.
+ * Each group entry file shares one PGLite instance per inject worker (maxWorkers: 1).
+ * Tables are truncated between group specs; PGLite is not reopened per test.
*/
import { readdir, readFile } from 'node:fs/promises'
diff --git a/apps/api/vitest.global-setup.ts b/apps/api/vitest.global-setup.ts
index b6aea955..8a0add46 100644
--- a/apps/api/vitest.global-setup.ts
+++ b/apps/api/vitest.global-setup.ts
@@ -1,31 +1,18 @@
/**
- * Vitest Global Setup
- *
- * Runs once before ALL test files/suites.
- * Sets up minimal environment variables for tests.
- * Database setup and migrations are handled per-worker in vitest.setup.ts.
- *
- * Note: Most test defaults are now handled by `src/lib/env.ts` which loads `.env.test`
- * and applies test-only defaults. This file only sets minimal, deterministic overrides.
+ * Vitest global setup — minimal env overrides before any worker starts.
+ * Per-group PGLite setup runs in each inject project `.spec.ts` via db-setup.ts.
*/
-// Set NODE_ENV to 'test' to signal test context (env.ts uses this to detect test mode)
process.env.NODE_ENV = 'test'
-// Force ALLOW_TEST for unit tests (fake email + DB-backed token for @test.ai)
process.env.ALLOW_TEST = 'true'
-// Pin explicit origins so passkey origin tests are not no-ops under * or inherited CI values.
-// Include https://example.com for magic-link / auth-helper callback URLs in tests.
process.env.ALLOWED_ORIGINS = 'http://localhost:3000,http://127.0.0.1:3000,https://example.com'
import type { GlobalSetupContext } from 'vitest/node'
export async function setup(_context: GlobalSetupContext) {
- // No global database setup needed
- // Each worker creates its own database in vitest.setup.ts
return
}
export async function teardown() {
- // Workers clean up their own databases in vitest.setup.ts afterAll hook
return
}
diff --git a/apps/api/vitest.setup.ts b/apps/api/vitest.setup.ts
index c065a397..ee77f689 100644
--- a/apps/api/vitest.setup.ts
+++ b/apps/api/vitest.setup.ts
@@ -1,20 +1,10 @@
/**
- * Vitest Test Setup
+ * Vitest per-file setup. Database lifecycle is owned by each group entry `.spec.ts`
+ * via `test/utils/db-setup.ts` (truncate between groups; PGLite stays open per worker).
*
- * Runs before each test file in each worker.
- * Provides global setup for tests.
- *
- * Database lifecycle is managed by each group entry `.spec.ts` file via `test/utils/db-setup.ts`.
- *
- * ## AI Tests
- *
- * Chat tests call a real AI API. Use ANTHROPIC_API_KEY in .env.test or CI secrets.
- * Overrides here: AI_PROVIDER=anthropic, AI_DEFAULT_MODEL=claude-haiku-4-5, Ollama/OpenRouter unset.
- * When the AI provider is unreachable (5xx, ECONNREFUSED, etc.), tests pass gracefully via
- * skipIfProviderUnavailable. Set ANTHROPIC_API_KEY in .env.test or GitHub secrets for CI.
+ * Remote AI tests run only when ANTHROPIC_API_KEY is a real secret (`hasRealAnthropicKey`).
*/
-// Enforce Anthropic for AI tests (ANTHROPIC_API_KEY from .env.test or CI env)
process.env.AI_PROVIDER = 'anthropic'
process.env.AI_DEFAULT_MODEL = 'claude-haiku-4-5'
Reflect.deleteProperty(process.env, 'OPEN_ROUTER_API_KEY')
diff --git a/apps/docu/content/docs/deployment/github-actions.mdx b/apps/docu/content/docs/deployment/github-actions.mdx
index d3d5dcb1..1bebd387 100644
--- a/apps/docu/content/docs/deployment/github-actions.mdx
+++ b/apps/docu/content/docs/deployment/github-actions.mdx
@@ -11,7 +11,7 @@ Path filters are used only for test workflows (app E2E and package unit tests) s
### Web (`web-e2e.yml`)
-Build → E2E. Triggers on pull request when `apps/web/**`, `apps/api/src/routes/test/**`, shared packages, or openapi change. Single job: build (with localhost API URL), then E2E via `test:e2e:local` (spawns Fastify + Next with `ALLOW_TEST=true`, `RATE_LIMIT_MAX=10000`, and `WEBAUTHN_RP_NAME=Test App`). Requires `ANTHROPIC_API_KEY` for the chat E2E project. `cancel-in-progress: true`.
+Build → E2E. Triggers on pull request when `apps/web/**`, API routes the suite exercises (`auth`, `account`, `ai`, `health`, `test`), shared packages, OpenAPI, or the workflow/action files change. Single job: build (with localhost API URL), then E2E via `test:e2e:local` with `SKIP_BUILD=1` (spawns Fastify + Next with `ALLOW_TEST=true`, `RATE_LIMIT_MAX=10000`, and `WEBAUTHN_RP_NAME=Test App`). Requires `ANTHROPIC_API_KEY` for the chat E2E project. `cancel-in-progress: true`. Sets `TURBO_TOKEN` / `TURBO_TEAM` when configured.
### API (`api-e2e.yml`)
@@ -24,7 +24,7 @@ Two parallel jobs when `apps/api/**` or shared packages change:
### Packages (`packages-test.yml`)
-Unit tests for shared packages (`core`, `react`, `error`, `utils`, …). Triggers on pull request when `packages/**`, `tools/**`, or `apps/api/openapi/**` change. Uses committed OpenAPI artifacts (no Fastify boot). Excludes app tests (handled by api-e2e, web-e2e).
+Unit tests for packages that define a `test` script (`core`, `react`, `error`). Triggers on pull request when `packages/**`, `tools/**`, `apps/api/openapi/**`, or the workflow/action files change. Uses committed OpenAPI artifacts (no Fastify boot). Excludes app tests (handled by api-e2e, web-e2e). Sets `TURBO_TOKEN` / `TURBO_TEAM` when configured.
### API drift check (`api-e2e.yml`)
@@ -48,7 +48,7 @@ gh run view --log-failed
### Lint (`lint.yml`)
-Linting and type checking on every PR (`pnpm lint`, then `pnpm exec turbo run checktypes`). Supports `workflow_dispatch` for manual runs.
+Linting and type checking on every PR (`pnpm lint`, then `pnpm exec turbo run checktypes`). Supports `workflow_dispatch` for manual runs. Sets `TURBO_TOKEN` / `TURBO_TEAM` when configured.
### Mobile (`mobile-build.yml`, `mobile-preview.yml`, `mobile-pr-preview.yml`)
@@ -90,10 +90,10 @@ See the complete `.coderabbit.yaml` in the repository root for full configuratio
## Caching
- **pnpm** — `lint`, `packages-test`, `api-e2e`, `web-e2e`, and `security` use `.github/actions/setup-pnpm` (`actions/setup-node` with `cache: pnpm`).
-- **Playwright** — `web-e2e` and `api-e2e` **e2e** jobs cache `~/.cache/ms-playwright`.
+- **Playwright** — `web-e2e` uses `.github/actions/setup-playwright` (caches `~/.cache/ms-playwright`, installs Chromium for `@repo/api` and `@repo/web`). `api-e2e` **e2e** still installs Playwright inline with the same cache key.
- **Next.js** — `web-e2e` caches `apps/web/.next/cache`.
- **Turbo** — caches `build`, `checktypes`, and `lint:eslint`; **not** `test` / `test:unit` / `test:e2e`.
-- **Turbo remote cache** (optional) — set `TURBO_TOKEN` and `TURBO_TEAM` in GitHub Secrets after creating a Vercel Remote Cache token. Vercel deploys can reuse the same token. See [Development Tooling](/docs/development/dev-tooling#turbo-remote-cache-optional).
+- **Turbo remote cache** (optional) — set `TURBO_TOKEN` (secret) and `TURBO_TEAM` (variable) after creating a Vercel Remote Cache token. Wired on `lint.yml`, `packages-test.yml`, and `web-e2e.yml`. Vercel deploys can reuse the same token. See [Development Tooling](/docs/development/dev-tooling#turbo-remote-cache-optional).
## Pre-commit Hooks
diff --git a/apps/docu/content/docs/testing/e2e-testing.mdx b/apps/docu/content/docs/testing/e2e-testing.mdx
index 9a02b6ed..a3dd1a1c 100644
--- a/apps/docu/content/docs/testing/e2e-testing.mdx
+++ b/apps/docu/content/docs/testing/e2e-testing.mdx
@@ -7,9 +7,9 @@ Frontend apps have no separate unit suite. Two Playwright suites: **Web** (`apps
The Scalar page loads `@scalar/api-reference` from jsDelivr (pinned version in `reference/template.ts`). E2E depends on that CDN for the login button script.
-Install Chromium with `pnpm setup:playwright` (or `pnpm setup`) if browsers are missing.
+Install Chromium for both suites with `pnpm setup:playwright` (or `pnpm setup`) if browsers are missing.
-Root `pnpm qa` ends with `pnpm test:e2e` (`scripts/run-e2e.mjs`). That script kills processes on ports 3000 and 3001 unless `SKIP_KILL_PORTS=1`.
+Root `pnpm qa` builds, then ends with `pnpm test:e2e` (`SKIP_BUILD=1`, `scripts/run-e2e.mjs`). That script kills processes on ports 3000 and 3001 unless `SKIP_KILL_PORTS=1`. Shared spawn env lives in `scripts/e2e-local-shared.mjs`.
## Commands
@@ -58,11 +58,11 @@ Local spawn scripts set `ALLOW_TEST`. The API process exits if `ALLOW_TEST` is t
## Playwright projects (order)
1. **public** — legal pages, login smoke, robots/sitemap
-2. **auth** — `01-callbacks` … `06-logout` (empty storage; file-name order)
-3. **setup** — writes `user.json` (depends on public only)
+2. **auth** — `01-callbacks` … `06-logout` (empty storage; depends on public; file-name order)
+3. **setup** — writes `user.json` (depends on auth)
4. **chromium** — dashboard smokes, 404 (storageState)
5. **security** — TOTP, API keys, passkeys CRUD (storageState; never logout)
-6. **passkey-login** — passkey sign-in with `e2e-passkey@test.ai` (empty storage)
+6. **passkey-login** — passkey sign-in with `e2e-passkey@test.ai` (empty storage; depends on setup)
7. **chat** — assistant (storageState; mobile viewport; last)
Dedicated emails: `test@test.ai` (shared), `e2e-passkey@test.ai`, `e2e-email@test.ai` (change-email).
diff --git a/apps/docu/content/docs/testing/index.mdx b/apps/docu/content/docs/testing/index.mdx
index 39d6f8db..6a3f7b8a 100644
--- a/apps/docu/content/docs/testing/index.mdx
+++ b/apps/docu/content/docs/testing/index.mdx
@@ -4,7 +4,7 @@ description: "Vitest for the API and packages; Playwright E2E for web. No fronte
---
- **API** — Vitest + `fastify.inject()`, blackbox HTTP, in-memory PGlite. Group entry files are `*.spec.ts`; route tests are imported `*.test.ts` (see Fastify testing rules).
-- **Packages** — Vitest unit tests (`core`, `react`, `error`, …). CI: `packages-test.yml` when `packages/` or `tools/` change.
+- **Packages** — Vitest unit tests for packages with a `test` script (`core`, `react`, `error`). CI: `packages-test.yml` when `packages/` or `tools/` change.
- **Web** — Playwright E2E only. Commands, fixtures, and specs: [E2E Testing](/docs/testing/e2e-testing).
PGLite does not support concurrent writers. API Vitest runs with `fileParallelism: false`, `maxWorkers: 1`, and `sequence.concurrent: false`. Between group entry files, `cleanupGroupDatabase()` truncates all 15 schema tables and clears the JWT session pool.
@@ -24,16 +24,16 @@ HTTP errors use `{ code, message }` from the app catalog (`getError` / `sendCata
| Group entry | Routes covered |
| --- | --- |
| `account.spec.ts` | Account linking, API keys, email change, profile |
-| `session.spec.ts` | Logout, refresh, validate-tokens, user |
-| `magiclink.spec.ts` | Magic link request/verify |
-| `oauth.spec.ts` | OAuth authorize/exchange and link-authorize-url (unconfigured 503 contract) |
-| `passkey.spec.ts` | Passkey start/verify/exchange/resolve-user |
-| `web3.spec.ts` | EIP-155 and Solana verify + `lib/web3/domain` unit tests |
+| `auth/session/session.spec.ts` | Logout, refresh, validate-tokens, user |
+| `auth/magiclink/magiclink.spec.ts` | Magic link request/verify |
+| `auth/oauth/oauth.spec.ts` | OAuth authorize/exchange and link-authorize-url (unconfigured 503 contract) |
+| `auth/passkey/passkey.spec.ts` | Passkey start/verify/exchange/resolve-user |
+| `auth/web3/web3.spec.ts` | EIP-155 and Solana verify + `lib/web3/domain` unit tests |
| `health.spec.ts` | `/health` + security headers |
-| `reference.spec.ts` | `/reference` HTML + OpenAPI JSON (including `jwtFromServer` on magic-link callback) |
-| `ai.spec.ts` | AI chat + generate (`messages.spec.ts` for `lib/ai` unit tests) |
+| `reference/reference.spec.ts` | `/reference` HTML + OpenAPI JSON (including `jwtFromServer` on magic-link callback) |
+| `ai.spec.ts` | AI chat + generate (`lib/ai/messages.spec.ts` inlines download + message unit tests) |
-Vitest `include` is `**/*.spec.ts` only. Every `*.test.ts` must be imported by a group entry `*.spec.ts` or it never runs.
+Vitest `include` is `**/*.spec.ts` only. Every `*.test.ts` must be imported by a group entry `*.spec.ts` or it never runs. `pnpm --filter @repo/api test:unit` runs `scripts/check-test-imports.mjs` first and fails on orphans.
## Coverage
diff --git a/apps/web/e2e/04-update-tokens.spec.ts b/apps/web/e2e/04-update-tokens.spec.ts
index 1b20cb31..87d3e56c 100644
--- a/apps/web/e2e/04-update-tokens.spec.ts
+++ b/apps/web/e2e/04-update-tokens.spec.ts
@@ -58,8 +58,8 @@ test.describe('POST /api/auth/update-tokens', () => {
expect(sessionHeader).toBeDefined()
const parsed = parseSessionCookieValue(sessionHeader ?? '')
- expect(parsed?.token).toBeTruthy()
- expect(parsed?.refreshToken).toBeTruthy()
+ expect(parsed?.token).toMatch(/^eyJ[\w-]+\.[\w-]+\.[\w-]+$/)
+ expect(parsed?.refreshToken).toMatch(/^eyJ[\w-]+\.[\w-]+\.[\w-]+$/)
})
test('returns 400 for missing fields', async ({ request }) => {
diff --git a/apps/web/e2e/06-logout.spec.ts b/apps/web/e2e/06-logout.spec.ts
index 6134f1c7..d937f4d7 100644
--- a/apps/web/e2e/06-logout.spec.ts
+++ b/apps/web/e2e/06-logout.spec.ts
@@ -4,10 +4,10 @@ import { authHelpers } from './auth-helpers'
test.describe('Logout', () => {
test('header sign out revokes session and returns to login', async ({ page }) => {
await authHelpers.loginAsTestUser(page)
- await expect(page.getByRole('link', { name: 'Sign out' })).toBeVisible({ timeout: 10_000 })
+ await expect(page.getByRole('button', { name: 'Sign out' })).toBeVisible({ timeout: 10_000 })
const token = await authHelpers.extractSessionToken(page)
- expect(token).toBeTruthy()
+ expect(token).toMatch(/^eyJ[\w-]+\.[\w-]+\.[\w-]+$/)
await page.getByRole('button', { name: 'Sign out' }).click()
await expect
diff --git a/apps/web/e2e/chat-assistant.spec.ts b/apps/web/e2e/chat-assistant.spec.ts
index e066a993..85a461ca 100644
--- a/apps/web/e2e/chat-assistant.spec.ts
+++ b/apps/web/e2e/chat-assistant.spec.ts
@@ -1,5 +1,7 @@
import { expect, test } from '@playwright/test'
+const isCi = !!process.env.CI
+
test.describe('Chat Assistant', () => {
test.setTimeout(90_000)
@@ -32,26 +34,7 @@ test.describe('Chat Assistant', () => {
test.skip(true, 'AI provider quota/credits (402)')
return
}
- if (/invalid x-api-key|authentication_error|invalid.*api.*key|401/i.test(errorText)) {
- test.skip(true, 'AI provider auth invalid (401/invalid API key)')
- return
- }
- if (
- /ECONNREFUSED|fetch failed|ENOTFOUND|ETIMEDOUT|ECONNRESET|Connection timed out|Error code 522|status_code: 522|UPSTREAM_TIMEOUT|timed out/i.test(
- errorText,
- )
- ) {
- test.skip(true, 'AI provider unreachable (network/timeout/522)')
- return
- }
- if (
- /AI provider request failed|upstream|UPSTREAM_TIMEOUT|timed out|Try again later|internal server error|status.?5\d{2}/i.test(
- errorText,
- )
- ) {
- test.skip(true, `AI provider upstream error: ${errorText.slice(0, 80)}`)
- return
- }
+ if (isCi) throw new Error(`Chat failed in CI: ${errorText || '(no error text)'}`)
}
await expect(chatError, `Chat failed: ${errorText || '(no error text)'}`).not.toBeVisible()
await expect(assistantLoc).toBeVisible({ timeout: 60_000 })
diff --git a/apps/web/e2e/dashboard.spec.ts b/apps/web/e2e/dashboard.spec.ts
index 6f1eaf7d..bdec600f 100644
--- a/apps/web/e2e/dashboard.spec.ts
+++ b/apps/web/e2e/dashboard.spec.ts
@@ -1,11 +1,12 @@
import { expect, test } from '@playwright/test'
test.describe('Dashboard routes', () => {
- test('home shows news heading or fallback copy', async ({ page }) => {
+ test('home shows news section for configured NewsAPI', async ({ page }) => {
await page.goto('/')
const newsHeading = page.getByRole('heading', { name: 'Latest News' })
const fallback = page.getByText(/Add NEWSAPI_KEY|No headlines available/i)
- await expect(newsHeading.or(fallback).first()).toBeVisible({ timeout: 15_000 })
+ if (process.env.NEWSAPI_KEY) await expect(newsHeading).toBeVisible({ timeout: 15_000 })
+ else await expect(fallback.first()).toBeVisible({ timeout: 15_000 })
await expect(page.locator('text=Signed In')).toBeVisible({ timeout: 15_000 })
await expect(page.locator('text=API OK')).toBeVisible({ timeout: 15_000 })
})
diff --git a/apps/web/package.json b/apps/web/package.json
index 1b81efdb..12025c05 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -24,7 +24,6 @@
"lint:fix": "pnpm run lint:eslint:fix && pnpm run lint:biome:fix",
"format": "biome format . --write",
"checktypes": "node ../../node_modules/@typescript/native/bin/tsc --noEmit",
- "test": "echo 'E2E only'",
"test:e2e": "node scripts/run-e2e.mjs",
"test:e2e:local": "node scripts/run-e2e-local.mjs",
"test:e2e:ui": "pnpm test:e2e -- --ui",
diff --git a/apps/web/playwright.config.ts b/apps/web/playwright.config.ts
index b138aafe..ec8f019f 100644
--- a/apps/web/playwright.config.ts
+++ b/apps/web/playwright.config.ts
@@ -12,7 +12,7 @@ export default defineConfig({
testDir: './e2e',
fullyParallel: false,
forbidOnly: isCi,
- retries: isCi ? 2 : 0,
+ retries: 0,
workers: 1,
reporter: isCi ? [['github'], ['html']] : 'list',
globalSetup: './e2e/playwright-global-setup.ts',
@@ -54,7 +54,7 @@ export default defineConfig({
testMatch: ['**/auth.setup.ts'],
timeout: 60_000,
use: { ...devices['Desktop Chrome'], storageState: emptyStorage },
- dependencies: ['public'],
+ dependencies: ['auth'],
},
{
name: 'chromium',
@@ -79,7 +79,7 @@ export default defineConfig({
testMatch: ['**/passkey-auth.spec.ts'],
timeout: 60_000,
use: { ...devices['Desktop Chrome'], storageState: emptyStorage },
- dependencies: ['security'],
+ dependencies: ['setup'],
},
{
name: 'chat',
diff --git a/apps/web/scripts/run-e2e-local.mjs b/apps/web/scripts/run-e2e-local.mjs
index 102d22fa..16018e04 100644
--- a/apps/web/scripts/run-e2e-local.mjs
+++ b/apps/web/scripts/run-e2e-local.mjs
@@ -1,75 +1,28 @@
#!/usr/bin/env node
/**
* E2E local: build, spawn Fastify + Next, poll until healthy, run Playwright, cleanup on exit.
- * No wait-on. Uses ALLOW_TEST, PGLITE, DB-backed token for @test.ai.
* When SKIP_BUILD=1, skip build step (assumes .next exists with NEXT_PUBLIC_API_URL=http://localhost:3001).
*/
-import { spawn, spawnSync } from 'node:child_process'
-import { existsSync, readFileSync } from 'node:fs'
+import { spawn } from 'node:child_process'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
+import {
+ buildE2eSpawnEnv,
+ killTestServerPorts,
+ loadEnvTest,
+ repoRoot,
+ waitForUrl,
+} from '../../../scripts/e2e-local-shared.mjs'
const scriptDir = dirname(fileURLToPath(import.meta.url))
const nextDir = dirname(scriptDir)
-const repoRoot = dirname(dirname(nextDir))
-
-function loadEnvTest() {
- const path = join(repoRoot, 'apps/api/.env.test')
- if (!existsSync(path)) return {}
- const lines = readFileSync(path, 'utf8').split('\n')
- const out = {}
- for (const line of lines) {
- const idx = line.indexOf('=')
- if (idx < 0 || line.startsWith('#')) continue
- const key = line.slice(0, idx).trim()
- let val = line.slice(idx + 1).trim()
- if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'")))
- val = val.slice(1, -1)
-
- out[key] = val
- }
- return out
-}
-
-function waitForUrl(url, timeoutMs = 60_000) {
- const start = Date.now()
- return new Promise(resolve => {
- const check = async () => {
- try {
- const res = await fetch(url, { signal: AbortSignal.timeout(2000) })
- if (res.ok || res.status === 307) return resolve(true)
- } catch {
- // continue
- }
- if (Date.now() - start > timeoutMs) return resolve(false)
- setTimeout(check, 500)
- }
- check()
- })
-}
async function main() {
- if (!process.env.SKIP_KILL_PORTS)
- try {
- spawnSync('bash', [join(repoRoot, 'scripts/kill-test-servers.sh')], {
- cwd: repoRoot,
- stdio: 'pipe',
- })
- } catch {
- /* ignore - ports may not be in use or bash unavailable */
- }
+ killTestServerPorts()
- // eslint-disable-next-line turbo/no-undeclared-env-vars -- set by root test:e2e or user
if (!process.env.SKIP_BUILD) {
const loadedForBuild = loadEnvTest()
- const buildEnv = {
- ...process.env,
- ...loadedForBuild,
- JWT_SECRET:
- loadedForBuild.JWT_SECRET ??
- process.env.JWT_SECRET ??
- 'e2e-jwt-secret-min-32-chars-for-tests',
- }
+ const buildEnv = buildE2eSpawnEnv({ loaded: loadedForBuild })
const build = spawn('pnpm', ['-F', '@repo/web', 'run', 'build:e2e'], {
cwd: repoRoot,
stdio: 'inherit',
@@ -80,19 +33,13 @@ async function main() {
}
const loaded = loadEnvTest()
- const env = {
- ...process.env,
- ...loaded,
- ALLOW_TEST: 'true',
- PGLITE: 'true',
- NODE_ENV: 'test',
- RATE_LIMIT_MAX: '10000',
- WEBAUTHN_RP_NAME: loaded.WEBAUTHN_RP_NAME ?? process.env.WEBAUTHN_RP_NAME ?? 'Test App',
- NEXT_PUBLIC_API_URL: 'http://localhost:3001',
- AI_PROVIDER: 'anthropic',
- JWT_SECRET:
- loaded.JWT_SECRET ?? process.env.JWT_SECRET ?? 'e2e-jwt-secret-min-32-chars-for-tests',
- }
+ const env = buildE2eSpawnEnv({
+ loaded,
+ extra: {
+ NEXT_PUBLIC_API_URL: 'http://localhost:3001',
+ AI_PROVIDER: 'anthropic',
+ },
+ })
delete env.OPEN_ROUTER_API_KEY
delete env.OLLAMA_BASE_URL
@@ -117,9 +64,7 @@ async function main() {
new Promise(r => setTimeout(r, timeoutMs)),
])
- const cleanup = () => {
- killAll('SIGTERM')
- }
+ const cleanup = () => killAll('SIGTERM')
process.on('SIGINT', () => {
cleanup()
process.exit(130)
diff --git a/package.json b/package.json
index f01fb9be..cc4337ee 100644
--- a/package.json
+++ b/package.json
@@ -36,7 +36,7 @@
"setup:env": "node scripts/setup-env.mjs",
"setup:database": "node scripts/setup-database.mjs",
"setup:deepsec": "pnpm --dir=.deepsec install --frozen-lockfile",
- "setup:playwright": "pnpm --filter @repo/web exec playwright install chromium",
+ "setup:playwright": "pnpm --filter @repo/api exec playwright install chromium && pnpm --filter @repo/web exec playwright install chromium",
"reset": "pnpm --filter @repo/api reset",
"update-deps": "pnpm self-update && pnpm update --latest --recursive"
},
diff --git a/packages/ui/package.json b/packages/ui/package.json
index acfa4801..66be79eb 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -6,8 +6,7 @@
"scripts": {
"lint:eslint": "eslint . --max-warnings 0",
"lint:eslint:fix": "eslint . --fix --max-warnings 0",
- "checktypes": "tsc --noEmit",
- "test": "echo \"no test\""
+ "checktypes": "tsc --noEmit"
},
"dependencies": {
"@hookform/resolvers": "^5.9.1",
diff --git a/packages/utils/package.json b/packages/utils/package.json
index d4fedd67..f84fbb2e 100644
--- a/packages/utils/package.json
+++ b/packages/utils/package.json
@@ -70,8 +70,7 @@
"lint:eslint": "eslint . --max-warnings 0",
"lint:eslint:fix": "eslint . --fix --max-warnings 0",
"prepack": "pnpm build && node ../../scripts/prepare-publish.mjs",
- "postpack": "node ../../scripts/restore-publish.mjs",
- "test": "echo \"no test\""
+ "postpack": "node ../../scripts/restore-publish.mjs"
},
"peerDependencies": {
"ahooks": "*",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index cae9a1f5..26a247d6 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -98,7 +98,7 @@ importers:
version: '@typescript/typescript6@6.0.2'
vercel:
specifier: latest
- version: 59.10.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(bufferutil@4.1.0)(rollup@4.59.0)(supports-color@10.2.2)(utf-8-validate@6.0.6)
+ version: 59.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(bufferutil@4.1.0)(rollup@4.59.0)(supports-color@10.2.2)(utf-8-validate@6.0.6)
vitest:
specifier: ^4.1.11
version: 4.1.11(@edge-runtime/vm@3.2.0)(@opentelemetry/api@1.9.1)(@types/node@24.13.3)(@vitest/coverage-v8@4.1.11)(jsdom@30.0.1(@noble/hashes@2.4.0))(vite@8.2.2(@types/node@24.13.3)(esbuild@0.25.10)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.12)(yaml@2.9.0))
@@ -244,9 +244,6 @@ importers:
'@vitest/coverage-v8':
specifier: ^4.1.11
version: 4.1.11(vitest@4.1.11)
- c8:
- specifier: ^12.0.0
- version: 12.0.0
chokidar-cli:
specifier: ^3.0.0
version: 3.0.0
@@ -2822,10 +2819,6 @@ packages:
resolution: {integrity: sha512-RQgQ4uQ+pLbqXfOmieB91ejmLwvSgv9nLx6sT6sD83s7umBypgg+OIBOBbEUiJXrfpnp9j0mRhYYdzp9uqq3lA==}
engines: {node: '>=12'}
- '@istanbuljs/schema@0.1.6':
- resolution: {integrity: sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==}
- engines: {node: '>=8'}
-
'@jest/schemas@29.6.3':
resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
@@ -6072,20 +6065,20 @@ packages:
'@upsetjs/venn.js@2.0.0':
resolution: {integrity: sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==}
- '@vercel/backends@5.0.0':
- resolution: {integrity: sha512-3gAxIrf2JQHCcy3btGbgHALEI7B/icUy7z4x0uqDKxREwk6Pwee0xfuLBAS1GT++S/kgIRkJtftfK2gDYqQA2A==}
+ '@vercel/backends@7.0.0':
+ resolution: {integrity: sha512-he5zmmtKzzaoizZhPXHxd9bOge3pOL90uz33b9IzmGnImWBlkSOPtGQr5r+NX2ad8HS5HWBdUchwAEcnxYz66w==}
peerDependencies:
- '@vercel/build-utils': 14.7.0
+ '@vercel/build-utils': 14.9.0
'@vercel/blob@2.2.0':
resolution: {integrity: sha512-h9ruqqTyAlLrmLIT55NUDnfc889feZnQXWOBxpXrAiAzWKmHqIokLchuMBKFsT9MDV/jM0HRQlvlTyubMHrOKA==}
engines: {node: '>=20.0.0'}
- '@vercel/build-utils@14.7.0':
- resolution: {integrity: sha512-Raz4NkE5sO83/QKt4pLN6C3vLt50vkOY7cPzxBr2jLI17FPRRpHPuMfjqwGL67s/G4PA3qFG0IxiYHTEUcLEdA==}
+ '@vercel/build-utils@14.9.0':
+ resolution: {integrity: sha512-czxOQSyZgFYZoD72gRSkRSokGghDyh5pMBPiuy0bjq2I4t7vjiENF1FN0NI/em5S/ITh2hKN1kzXHFwQy9jg6g==}
- '@vercel/cervel@0.1.57':
- resolution: {integrity: sha512-IkMXfny5dJkTN3jrRrOD5g4R90ZDwAweAkkRPrGDftouzSZ9jVUEMLdL3/4iPXAeaQ1a4X0tL0fQJp3h66J4dA==}
+ '@vercel/cervel@0.1.59':
+ resolution: {integrity: sha512-zFpD1AWHher/SYpwJAZTnw9ncf3qVdPWGG0fTUz0dTlO7nkdV67zXLej46y+PrbML1LtHXMqBQRauM9Y8uH+wg==}
hasBin: true
'@vercel/cli-auth@0.3.5':
@@ -6094,32 +6087,32 @@ packages:
'@vercel/cli-config@0.2.4':
resolution: {integrity: sha512-kZ5SojbrV06GHoU6QIWGwDXLov+s9rWZ7QqdqKfJfBGCNUieGfgaCjeeenNy8Y+QC0bwC0dZ2B4l5Hvdmrgpdw==}
- '@vercel/container@5.0.0':
- resolution: {integrity: sha512-F3VT8SxDHFArYrtN9w3tWaFGgz38kC/CKcf8FqID0ZLS9dN24ACMP5PFVY4hXa6k93rDS3Ibf95cGsFdUhE5Lg==}
+ '@vercel/container@7.0.0':
+ resolution: {integrity: sha512-VhQAJv8j6/ufedWYAbwjJ94p3R0MfNYiJmamr68NeFVGJlv6sFNm1SJb1Rzl+6udK44BcQp3M64qKFO4ARNLFA==}
peerDependencies:
- '@vercel/build-utils': 14.7.0
+ '@vercel/build-utils': 14.9.0
'@vercel/detect-agent@1.2.5':
resolution: {integrity: sha512-0krENrjuitlW8s6TJu0MlqCevyCU7K7JK63jZAf7xZ6n17tx+vUEwzHT3sTxawtwZxaW21hu+oFUpoOrm49FsQ==}
engines: {node: '>=14'}
- '@vercel/elysia@5.0.0':
- resolution: {integrity: sha512-9p0MJjNrpFVfywaBpXzWW++XwmnJIaj7BlrdDFuYZDiVbhTpKgAvO6211SnGAvXS9PyK9sTGCU6MtVgFelXWtw==}
+ '@vercel/elysia@7.0.0':
+ resolution: {integrity: sha512-EZgu9HXQVf1af7sElg3tws+0TTT/k1DgPyDftJnXfk1L3W8M31pqbBY3a6I1hcnWxNaNceF2VtSWMPNrmsTOVQ==}
peerDependencies:
- '@vercel/build-utils': 14.7.0
+ '@vercel/build-utils': 14.9.0
'@vercel/error-utils@2.2.1':
resolution: {integrity: sha512-9DhP8jP7raLML4hGsBemxX5fXuQnu5xxMV+HjGygGbzEmVK/+KyJ3QP2Cw7PdF0uXdb9N0Qa4c3tRGH34ZX6vw==}
- '@vercel/express@5.0.0':
- resolution: {integrity: sha512-SQetT+JonnYQE3O0yqjslV705OuPNzIYNGrKQMpiwflncB+j8Titx/VMjGCSEIZSrsDR5SvKolm+qbgyBLW6Yw==}
+ '@vercel/express@7.0.0':
+ resolution: {integrity: sha512-dhGOtQk3HJXQBeFyfiDP0/nIWqwzrvN02rr/tF8zEk45Z6xqLlSkvH47lVPlvVQdoPy93MOlwKA7/gYapi1chg==}
peerDependencies:
- '@vercel/build-utils': 14.7.0
+ '@vercel/build-utils': 14.9.0
- '@vercel/fastify@5.0.0':
- resolution: {integrity: sha512-j3jfZEaC61qbaJY6ZnXoX1ItFCYWBYixgLreeqgpatUyRLuI1boz1EWj7XyPlZdUqruRFuPRDmRmXKKAa3cXRA==}
+ '@vercel/fastify@7.0.0':
+ resolution: {integrity: sha512-tcB2pd0DdQls884nS70bkZgdrYRFceST1zZO/073o1iJG0VVoFcIPuDdePGlIIetegGJ+S68Wc47gr4TU/UQcQ==}
peerDependencies:
- '@vercel/build-utils': 14.7.0
+ '@vercel/build-utils': 14.9.0
'@vercel/fun@1.3.0':
resolution: {integrity: sha512-8erw9uPe0dFg45THkNxmjtvMX143SkZebmjgSVbcM3XCkXu3RIiBaJMcMNG8aaS+rnTuw8+d4De9HVT0M/r3wg==}
@@ -6128,53 +6121,53 @@ packages:
'@vercel/gatsby-plugin-vercel-analytics@1.0.12':
resolution: {integrity: sha512-Ejlhwxr7EBYJxtwYnlh6Vm6A2dPsUSPxMdYrfv0koZkgAzVwo7cG4aDQiy7iASMaGzwuI/PAnwlY2sJBF5b4OA==}
- '@vercel/gatsby-plugin-vercel-builder@2.2.50':
- resolution: {integrity: sha512-a6SvWrtqCgL49WWAs8e48mY4Tv5wVIb0JgObSZNFQUEoJsqJ1xpvSB37A36NUTh84c3iZLjKIHM1UuRsXr5g/g==}
+ '@vercel/gatsby-plugin-vercel-builder@2.2.52':
+ resolution: {integrity: sha512-PYl9TBsYsP0a7qB4kgrF0a5YBUt7caiZwrxd4dl/uPdWHZlogTrnw2Cf2GS/kgij4rsew5jhc9xMnE4eHvQ1zg==}
- '@vercel/go@8.0.0':
- resolution: {integrity: sha512-gMJSvaVDnzvV8i987nkhPqAMs6DZDZu2wfjqXCSyHRn+Ix/HExP68/3M0cO7uCXeBu4Mr5CrXaddN/HtFHKeyQ==}
+ '@vercel/go@10.0.0':
+ resolution: {integrity: sha512-G2tHOrb64snuckAdfq+OjMqE8fKIB4rq3FpbmaQ1vjvnyK/PqyWnvX9gCoIigThM49P0RkGQVp76DCt1RFGh4Q==}
peerDependencies:
- '@vercel/build-utils': 14.7.0
+ '@vercel/build-utils': 14.9.0
- '@vercel/h3@5.0.0':
- resolution: {integrity: sha512-eBIxWy6fnIj5J0X/JTdNwX0/AYPWPUWzr5mLp8Rh7eg9q6SnVg3n30fYIaw6kp50P7U64k2y1nW3yHdNmtK8OA==}
+ '@vercel/h3@7.0.0':
+ resolution: {integrity: sha512-GYhMAK/XgDAQmgXSaiokz2kjc8QeE4azNg9aajcAm2q16Yd+t4a+VZutHppfk4I5SaLf5sKAGhGB8hkCHkbTPA==}
peerDependencies:
- '@vercel/build-utils': 14.7.0
+ '@vercel/build-utils': 14.9.0
- '@vercel/hono@5.0.0':
- resolution: {integrity: sha512-gdCxR9GEKuCUCPVYS2hX5RFfY3p1LHbjPOTwGx00/C4cmoArj+OdcRC5+6s+ORUt7vtGEstjTjBeNqe9Sr4nig==}
+ '@vercel/hono@7.0.0':
+ resolution: {integrity: sha512-Bn+illFuXukoI0l2z9Y75IS0c8mnLwy0zf5D0AIC2vegjPy5PnoYZ8c7mnEh88On/2ahMR3KEfocWnDssbpGHQ==}
peerDependencies:
- '@vercel/build-utils': 14.7.0
+ '@vercel/build-utils': 14.9.0
- '@vercel/hydrogen@6.0.0':
- resolution: {integrity: sha512-PtOE7Nb9rpoTM0lLoxozeXrU+etebkSITOSjiiy1oG/dxo4VEoT6owgEjiuv0kRdIOofjMt3sjdxS6ZW6g0F7A==}
+ '@vercel/hydrogen@8.0.0':
+ resolution: {integrity: sha512-Z2CKkTKn2SsqD5ftXXizPJ2HCmUoWvvEL9RnSd/eup1IPpOMK42MjIGyh+aPiiwzOwcPmoANPCmMufqrUOjHzw==}
peerDependencies:
- '@vercel/build-utils': 14.7.0
+ '@vercel/build-utils': 14.9.0
- '@vercel/koa@5.0.0':
- resolution: {integrity: sha512-OF8ofSoLQJ1ewqy8ZDY85Y+JBybpLL2QgH8wLMGYiCWT3eucgWQVGFcj+mrVyHniFA1cFcEqP1T1xNCPO1B5cg==}
+ '@vercel/koa@7.0.0':
+ resolution: {integrity: sha512-UgfQVMc2+hjfoMrGmAFhHmGIf1uDwNL+3Y2B+Ad9Gn4D2wz2gn2Pl5ywy23VOmfSD/aTerbLigTWKT+y9fb9Tg==}
peerDependencies:
- '@vercel/build-utils': 14.7.0
+ '@vercel/build-utils': 14.9.0
- '@vercel/nestjs@5.0.0':
- resolution: {integrity: sha512-Sn2bjU6nbZrOCVoPPi3NEEryiZra4/iBvAy61UisV7sinTBZGPIDywd+FKMIxnOsB46DXSqjxcbeKBerdnzV6Q==}
+ '@vercel/nestjs@7.0.0':
+ resolution: {integrity: sha512-suD7cuigAQlLA/RLAly8234gXXgIC/XG7AGBa1h2Y4vuIwGP43EBHZ4IuojdN6YCKwbD4/2eUMRa/qBs9OpsNQ==}
peerDependencies:
- '@vercel/build-utils': 14.7.0
+ '@vercel/build-utils': 14.9.0
- '@vercel/next@9.0.0':
- resolution: {integrity: sha512-F4547opwU5EENv7THwRNyI9sFkvfXSP+RpeRECfz7fYgB4bLPa6kcM6jPG20G74rLHNuKgZtKHP3YP1VyMX9dw==}
+ '@vercel/next@11.0.0':
+ resolution: {integrity: sha512-eTaJA+6iKLfhRwOl44mAuNj35jnuA5uExpDvy8vN+oWW0F5Ycjc2yoLHffbUO+9x3SWmo9CV5hpkGKfSSrJakg==}
peerDependencies:
- '@vercel/build-utils': 14.7.0
+ '@vercel/build-utils': 14.9.0
'@vercel/nft@1.10.0':
resolution: {integrity: sha512-iLOW4fcsgkipfOh2Bw3wB38YDfxTlxr7+j4uFeui2OswkNT28jIitS/aMce7tS0mef1YPQ8zLIDYr3a0aahNrA==}
engines: {node: '>=20'}
hasBin: true
- '@vercel/node@10.0.0':
- resolution: {integrity: sha512-YmeBOFd9wZ3aNrBZ0Qo68+cW8jug4JWMI4TiF0bkjwW0EcUxpGYT4tleLLgdDuja0THZt6slgfWbmvxxpe/HEg==}
+ '@vercel/node@12.0.0':
+ resolution: {integrity: sha512-F3tbqSdN1Nap1zeZcdfScatAVFUrLLYXNBsHf9wutOMyOjuW7M32NEEp1eXhjHXdrIg9SyChooqvJPmt3iepSw==}
peerDependencies:
- '@vercel/build-utils': 14.7.0
+ '@vercel/build-utils': 14.9.0
'@vercel/oidc@3.2.0':
resolution: {integrity: sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug==}
@@ -6186,59 +6179,59 @@ packages:
'@vercel/python-analysis@0.14.0':
resolution: {integrity: sha512-qyVxbaU14gAi/AsaR8syZLukUsOp69Jkm1xn80rZPy4k9+zRUTIfqs0AOk7L8wwBxDDB6LLjcBUAmafhbJQnUQ==}
- '@vercel/python@11.0.0':
- resolution: {integrity: sha512-1rRnyM6Ls/WoAtdM8hMAytksBNUcnpTffEnrkWH4Re0OFTQo4TsMBciisxxi0dCercge3jgLj914lpnH37MNFQ==}
+ '@vercel/python@13.0.0':
+ resolution: {integrity: sha512-KUqi9G5jx26m10kbpzgd8q2jGlijgX4aawH5aD2J8T7pu4q0dZGTw0DlczdrSFnEDYd8yM3zghGjXjtvcrKeaQ==}
peerDependencies:
- '@vercel/build-utils': 14.7.0
+ '@vercel/build-utils': 14.9.0
- '@vercel/redwood@7.0.0':
- resolution: {integrity: sha512-LgeLFSC3nSw9392JAPq6JocHakL27RYyC8K7wMj/GO5frEmhgawIwU/vZhNWvMpn99Kjq41aZvUdKOq3Ryf1+w==}
+ '@vercel/redwood@9.0.0':
+ resolution: {integrity: sha512-y4YdEsqjGVHFcLTPKZWppTvcgXbq9edxxXl+KBEJvtJpqY7s4ZjFd+BzF8YF6KTC7x18rYOiugBSrbyLyctRtQ==}
peerDependencies:
- '@vercel/build-utils': 14.7.0
+ '@vercel/build-utils': 14.9.0
- '@vercel/remix-builder@10.0.0':
- resolution: {integrity: sha512-81SoDFIJBwDcoLwytsS9Dan6SN8NYyec1EeujhuBOjkh5bk0/DNUYzhw2FrznxASdp5mm6MZMtScOuZLZIPUow==}
+ '@vercel/remix-builder@12.0.0':
+ resolution: {integrity: sha512-SQqmOQSQn44Migiu/xglGZ2EjlE4YfEEi8GpOHfN2iVKZhGqyeA5spTzVuxWAHjxeYbaOarHW7qM/giaFc0e2A==}
peerDependencies:
- '@vercel/build-utils': 14.7.0
+ '@vercel/build-utils': 14.9.0
- '@vercel/ruby@7.0.0':
- resolution: {integrity: sha512-0Lni9tDHdS4SUJGLTKWxa1ylc8zSfdN2I0E3q34ffI5LtCANNszl883v/3k5vVidg+XeNdW5jHLp7qiSZ5WzEA==}
+ '@vercel/ruby@9.0.0':
+ resolution: {integrity: sha512-kTmJZWkZpSEcK1t0FuM1Py1PTLDTgGbmhwXt1B1Tv/ZpJU2UfLK9rxzNUZemy2jdxSRtFWL+D/Ur0j1sF1W7Hg==}
peerDependencies:
- '@vercel/build-utils': 14.7.0
+ '@vercel/build-utils': 14.9.0
- '@vercel/rust@6.0.0':
- resolution: {integrity: sha512-gtVF5DgIBvGuoY+VgVpUuztJfjJQWA80Z3DF9x+/pFIfnZDTOhg3VHsd8E60+/NqmcEABdu24iw8CJZNqkr4Zw==}
+ '@vercel/rust@8.0.0':
+ resolution: {integrity: sha512-Ut16g8BZkwedJ8NN+LlPZPbiy4sC4efYYl2f440A6dg7obW5t4xjt9M1wTkooIgT0w2/j/FVfDdmQNJGfpPCgA==}
peerDependencies:
- '@vercel/build-utils': 14.7.0
+ '@vercel/build-utils': 14.9.0
'@vercel/sandbox@3.1.0':
resolution: {integrity: sha512-z124E4rsmNpwGtTnLImiYzEXk972bWniQyhlvkEt35UtwKTdfBAFXcNG2OvqYqwzAsdQaP3B7teQ2pqA9B5Viw==}
- '@vercel/static-build@7.0.0':
- resolution: {integrity: sha512-dkOC8Ew2VuM4fckFyWUiOj0g24D7UCmk4H4eRfdfO9rYr+BmE1gRD+dxqRzoWDUSddei9WT7zd1kKnC/9vU4vw==}
+ '@vercel/static-build@9.0.0':
+ resolution: {integrity: sha512-JrYaWxWmq83vQofB669VTp9egqoJN4wn3JgduvgCj3mNsdRj7R2wvyMPdGJWNm6t5Jsq3YMikik9VVmxZpctRQ==}
peerDependencies:
- '@vercel/build-utils': 14.7.0
+ '@vercel/build-utils': 14.9.0
'@vercel/static-config@3.4.3':
resolution: {integrity: sha512-BY1sL8rNJvIm3TK/8TQ+Q6jIx7NpO5xckQzv7s6c1ypHvRnTl+vf6rmgY5WFXmoYDeGm3tMy4jiy/5M/5jGr/g==}
- '@vercel/vc-native-darwin-arm64@59.10.0':
- resolution: {integrity: sha512-Zis3NHaorZT7SOR+T+ICPtj7ESbDt/Rrv/NrLO0xEUo5UcnFrq5OgcX8qX/c+YAKUHiGTVI95Gb4y3NbmZo+Yw==}
+ '@vercel/vc-native-darwin-arm64@59.11.1':
+ resolution: {integrity: sha512-Lpxu0C2h3HThwfa7hrQXtzvy0uMAg2XAgMAyS0xwe10LQ5+OiX3nNFQCb4sGKhCBHvtiHv14iSCZRgQo1HtNFQ==}
cpu: [arm64]
os: [darwin]
- '@vercel/vc-native-darwin-x64@59.10.0':
- resolution: {integrity: sha512-ycmJXcXUWlYk5hSxIzATUaX3JLBncIEIXckSwBoVdqBYL9D0FJiB0zFhsGzv9qq9/MpDDw2SYvbCk7dvQIc/HA==}
+ '@vercel/vc-native-darwin-x64@59.11.1':
+ resolution: {integrity: sha512-aAmQaFTC37ZNFWwf+WZ+zSzCnbKXR6UkQC77Zx2sbc/yGsjraBzf44Nza9fL1lLfVsIYs+wixmLKhvig1AQ06A==}
cpu: [x64]
os: [darwin]
- '@vercel/vc-native-linux-arm64@59.10.0':
- resolution: {integrity: sha512-7RbgheQI8CA/d9i6AtZPLryCCaRJ4S9QixQ0j82zuXwmHkslrh45qp1wYonV46HfGs/m4hacTykymtAt6/RkrQ==}
+ '@vercel/vc-native-linux-arm64@59.11.1':
+ resolution: {integrity: sha512-npU8GcrkwqyOotX2txj5TRIC9gof7uFr1Lp5fhzlw7qLoZNTzP/uNu//erNsSi4/LjIGBAD0YAnWVv6XkvlT1Q==}
cpu: [arm64]
os: [linux]
- '@vercel/vc-native-linux-x64@59.10.0':
- resolution: {integrity: sha512-1boYvsz0N8QhsyA2ZkE4TBl307fwX+N/Po9TXiBNro/IB/Ll6Rf6Aa+JthOZYQGIQXWDLOxkH4NxYnFRKX0Qbg==}
+ '@vercel/vc-native-linux-x64@59.11.1':
+ resolution: {integrity: sha512-CV0nod07WyVceW9KucckRZSi4AVuQYz/T+lKjSmnNOFXz9TPi6i0X1R8ObcDfSzi0o6kgVhUGZS2+DXDiUTHqQ==}
cpu: [x64]
os: [linux]
@@ -6940,16 +6933,6 @@ packages:
magicast:
optional: true
- c8@12.0.0:
- resolution: {integrity: sha512-4zpJvrd1nKWutnnKC2pXkFmb6iM1l+ffN//o1CzlTNwW7GSOs9a1xrLqkC48nU8oEkjmPZLPiwMsIaOvoF4Pqg==}
- engines: {node: ^20.19.0 || ^22.12.0 || >=23}
- hasBin: true
- peerDependencies:
- monocart-coverage-reports: ^2
- peerDependenciesMeta:
- monocart-coverage-reports:
- optional: true
-
cac@6.7.14:
resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
engines: {node: '>=8'}
@@ -11625,10 +11608,6 @@ packages:
resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==}
engines: {node: '>=18'}
- string-width@8.2.2:
- resolution: {integrity: sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==}
- engines: {node: '>=20'}
-
string.prototype.matchall@4.1.0:
resolution: {integrity: sha512-tHNHTxInrYLCga9O9YGxWA3G9/nnzQw8UGAyqGx3Ar1pSTTzIuM4woFSq4SowkXCjJIwq5sIiQvEfRI9tCH1qQ==}
engines: {node: '>= 0.4'}
@@ -11809,10 +11788,6 @@ packages:
engines: {node: '>=10'}
hasBin: true
- test-exclude@8.0.0:
- resolution: {integrity: sha512-ZOffsNrXYggvU1mDGHk54I96r26P8SyMjO5slMKSc7+IWmtB/MQKnEC2fP51imB3/pT6YK5cT5E8f+Dd9KdyOQ==}
- engines: {node: 20 || >=22}
-
text-decoder@1.2.7:
resolution: {integrity: sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==}
@@ -12230,10 +12205,6 @@ packages:
v8-compile-cache-lib@3.0.1:
resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==}
- v8-to-istanbul@9.3.0:
- resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==}
- engines: {node: '>=10.12.0'}
-
validate-npm-package-name@5.0.1:
resolution: {integrity: sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
@@ -12251,8 +12222,8 @@ packages:
vconsole@3.15.1:
resolution: {integrity: sha512-KH8XLdrq9T5YHJO/ixrjivHfmF2PC2CdVoK6RWZB4yftMykYIaXY1mxZYAic70vADM54kpMQF+dYmvl5NRNy1g==}
- vercel@59.10.0:
- resolution: {integrity: sha512-esa5wGhYo8EGYg1KDZZslFrITnDKtn+u8ujPG/QuVVoBzZ5hrvuFeIOcUGUZKH9OX0V8r9TTiRbPoAdtYs5qWg==}
+ vercel@59.11.1:
+ resolution: {integrity: sha512-vGmxxo6NcqfMcHMJ2rtTsOLTdYjb/Jp49eAMh/zpTvvDS7BCqQdCLpeMTxxZ+5yezQWDKvYstzueRicEuNfubg==}
engines: {node: '>= 18'}
hasBin: true
@@ -12607,10 +12578,6 @@ packages:
resolution: {integrity: sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==}
engines: {node: ^20.19.0 || ^22.12.0 || >=23}
- yargs@18.1.0:
- resolution: {integrity: sha512-2rAgRKu54VsHkqI0/tYkmluGXHD4KW7yZoycuqDQ15QOTnc2VVfy0nN/1eMhnQLO00A+dwtK20xuCnc1YGeUyg==}
- engines: {node: ^20.19.0 || ^22.12.0 || >=23}
-
yauzl-clone@1.0.4:
resolution: {integrity: sha512-igM2RRCf3k8TvZoxR2oguuw4z1xasOnA31joCqHIyLkeWrvAc2Jgay5ISQ2ZplinkoGaJ6orCz56Ey456c5ESA==}
engines: {node: '>=6'}
@@ -14583,8 +14550,6 @@ snapshots:
'@isaacs/ttlcache@1.4.1': {}
- '@istanbuljs/schema@0.1.6': {}
-
'@jest/schemas@29.6.3':
dependencies:
'@sinclair/typebox': 0.27.10
@@ -17847,9 +17812,9 @@ snapshots:
d3-selection: 3.0.0
d3-transition: 3.0.1(d3-selection@3.0.0)
- '@vercel/backends@5.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(@vercel/build-utils@14.7.0)(rollup@4.59.0)(supports-color@10.2.2)':
+ '@vercel/backends@7.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(@vercel/build-utils@14.9.0)(rollup@4.59.0)(supports-color@10.2.2)':
dependencies:
- '@vercel/build-utils': 14.7.0
+ '@vercel/build-utils': 14.9.0
'@vercel/nft': 1.10.0(rollup@4.59.0)(supports-color@10.2.2)
'@vercel/static-config': 3.4.3(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)
execa: 3.2.0
@@ -17878,14 +17843,14 @@ snapshots:
throttleit: 2.1.0
undici: 7.29.0
- '@vercel/build-utils@14.7.0':
+ '@vercel/build-utils@14.9.0':
dependencies:
cjs-module-lexer: 1.2.3
es-module-lexer: 1.5.0
- '@vercel/cervel@0.1.57(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(@vercel/build-utils@14.7.0)(rollup@4.59.0)(supports-color@10.2.2)':
+ '@vercel/cervel@0.1.59(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(@vercel/build-utils@14.9.0)(rollup@4.59.0)(supports-color@10.2.2)':
dependencies:
- '@vercel/backends': 5.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(@vercel/build-utils@14.7.0)(rollup@4.59.0)(supports-color@10.2.2)
+ '@vercel/backends': 7.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(@vercel/build-utils@14.9.0)(rollup@4.59.0)(supports-color@10.2.2)
transitivePeerDependencies:
- '@emnapi/core'
- '@emnapi/runtime'
@@ -17907,16 +17872,16 @@ snapshots:
xdg-app-paths: 5.5.1
zod: 4.1.11
- '@vercel/container@5.0.0(@vercel/build-utils@14.7.0)':
+ '@vercel/container@7.0.0(@vercel/build-utils@14.9.0)':
dependencies:
- '@vercel/build-utils': 14.7.0
+ '@vercel/build-utils': 14.9.0
'@vercel/detect-agent@1.2.5': {}
- '@vercel/elysia@5.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(@vercel/build-utils@14.7.0)(rollup@4.59.0)(supports-color@10.2.2)':
+ '@vercel/elysia@7.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(@vercel/build-utils@14.9.0)(rollup@4.59.0)(supports-color@10.2.2)':
dependencies:
- '@vercel/build-utils': 14.7.0
- '@vercel/node': 10.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(@vercel/build-utils@14.7.0)(rollup@4.59.0)(supports-color@10.2.2)
+ '@vercel/build-utils': 14.9.0
+ '@vercel/node': 12.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(@vercel/build-utils@14.9.0)(rollup@4.59.0)(supports-color@10.2.2)
'@vercel/static-config': 3.4.3(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)
transitivePeerDependencies:
- '@emnapi/core'
@@ -17927,12 +17892,12 @@ snapshots:
'@vercel/error-utils@2.2.1': {}
- '@vercel/express@5.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(@vercel/build-utils@14.7.0)(rollup@4.59.0)(supports-color@10.2.2)':
+ '@vercel/express@7.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(@vercel/build-utils@14.9.0)(rollup@4.59.0)(supports-color@10.2.2)':
dependencies:
- '@vercel/build-utils': 14.7.0
- '@vercel/cervel': 0.1.57(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(@vercel/build-utils@14.7.0)(rollup@4.59.0)(supports-color@10.2.2)
+ '@vercel/build-utils': 14.9.0
+ '@vercel/cervel': 0.1.59(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(@vercel/build-utils@14.9.0)(rollup@4.59.0)(supports-color@10.2.2)
'@vercel/nft': 1.10.0(rollup@4.59.0)(supports-color@10.2.2)
- '@vercel/node': 10.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(@vercel/build-utils@14.7.0)(rollup@4.59.0)(supports-color@10.2.2)
+ '@vercel/node': 12.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(@vercel/build-utils@14.9.0)(rollup@4.59.0)(supports-color@10.2.2)
'@vercel/static-config': 3.4.3(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)
fs-extra: 11.1.0
path-to-regexp: 6.3.0
@@ -17945,10 +17910,10 @@ snapshots:
- rollup
- supports-color
- '@vercel/fastify@5.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(@vercel/build-utils@14.7.0)(rollup@4.59.0)(supports-color@10.2.2)':
+ '@vercel/fastify@7.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(@vercel/build-utils@14.9.0)(rollup@4.59.0)(supports-color@10.2.2)':
dependencies:
- '@vercel/build-utils': 14.7.0
- '@vercel/node': 10.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(@vercel/build-utils@14.7.0)(rollup@4.59.0)(supports-color@10.2.2)
+ '@vercel/build-utils': 14.9.0
+ '@vercel/node': 12.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(@vercel/build-utils@14.9.0)(rollup@4.59.0)(supports-color@10.2.2)
'@vercel/static-config': 3.4.3(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)
transitivePeerDependencies:
- '@emnapi/core'
@@ -17985,22 +17950,22 @@ snapshots:
dependencies:
web-vitals: 0.2.4
- '@vercel/gatsby-plugin-vercel-builder@2.2.50':
+ '@vercel/gatsby-plugin-vercel-builder@2.2.52':
dependencies:
'@sinclair/typebox': 0.25.24
- '@vercel/build-utils': 14.7.0
+ '@vercel/build-utils': 14.9.0
esbuild: 0.25.10
etag: 1.8.1
fs-extra: 11.1.0
- '@vercel/go@8.0.0(@vercel/build-utils@14.7.0)':
+ '@vercel/go@10.0.0(@vercel/build-utils@14.9.0)':
dependencies:
- '@vercel/build-utils': 14.7.0
+ '@vercel/build-utils': 14.9.0
- '@vercel/h3@5.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(@vercel/build-utils@14.7.0)(rollup@4.59.0)(supports-color@10.2.2)':
+ '@vercel/h3@7.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(@vercel/build-utils@14.9.0)(rollup@4.59.0)(supports-color@10.2.2)':
dependencies:
- '@vercel/build-utils': 14.7.0
- '@vercel/node': 10.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(@vercel/build-utils@14.7.0)(rollup@4.59.0)(supports-color@10.2.2)
+ '@vercel/build-utils': 14.9.0
+ '@vercel/node': 12.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(@vercel/build-utils@14.9.0)(rollup@4.59.0)(supports-color@10.2.2)
'@vercel/static-config': 3.4.3(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)
transitivePeerDependencies:
- '@emnapi/core'
@@ -18009,11 +17974,11 @@ snapshots:
- rollup
- supports-color
- '@vercel/hono@5.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(@vercel/build-utils@14.7.0)(rollup@4.59.0)(supports-color@10.2.2)':
+ '@vercel/hono@7.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(@vercel/build-utils@14.9.0)(rollup@4.59.0)(supports-color@10.2.2)':
dependencies:
- '@vercel/build-utils': 14.7.0
+ '@vercel/build-utils': 14.9.0
'@vercel/nft': 1.10.0(rollup@4.59.0)(supports-color@10.2.2)
- '@vercel/node': 10.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(@vercel/build-utils@14.7.0)(rollup@4.59.0)(supports-color@10.2.2)
+ '@vercel/node': 12.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(@vercel/build-utils@14.9.0)(rollup@4.59.0)(supports-color@10.2.2)
'@vercel/static-config': 3.4.3(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)
fs-extra: 11.1.0
path-to-regexp: 6.3.0
@@ -18026,19 +17991,19 @@ snapshots:
- rollup
- supports-color
- '@vercel/hydrogen@6.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(@vercel/build-utils@14.7.0)':
+ '@vercel/hydrogen@8.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(@vercel/build-utils@14.9.0)':
dependencies:
- '@vercel/build-utils': 14.7.0
+ '@vercel/build-utils': 14.9.0
'@vercel/static-config': 3.4.3(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)
ts-morph: 12.0.0
transitivePeerDependencies:
- '@emnapi/core'
- '@emnapi/runtime'
- '@vercel/koa@5.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(@vercel/build-utils@14.7.0)(rollup@4.59.0)(supports-color@10.2.2)':
+ '@vercel/koa@7.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(@vercel/build-utils@14.9.0)(rollup@4.59.0)(supports-color@10.2.2)':
dependencies:
- '@vercel/build-utils': 14.7.0
- '@vercel/node': 10.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(@vercel/build-utils@14.7.0)(rollup@4.59.0)(supports-color@10.2.2)
+ '@vercel/build-utils': 14.9.0
+ '@vercel/node': 12.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(@vercel/build-utils@14.9.0)(rollup@4.59.0)(supports-color@10.2.2)
'@vercel/static-config': 3.4.3(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)
transitivePeerDependencies:
- '@emnapi/core'
@@ -18047,10 +18012,10 @@ snapshots:
- rollup
- supports-color
- '@vercel/nestjs@5.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(@vercel/build-utils@14.7.0)(rollup@4.59.0)(supports-color@10.2.2)':
+ '@vercel/nestjs@7.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(@vercel/build-utils@14.9.0)(rollup@4.59.0)(supports-color@10.2.2)':
dependencies:
- '@vercel/build-utils': 14.7.0
- '@vercel/node': 10.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(@vercel/build-utils@14.7.0)(rollup@4.59.0)(supports-color@10.2.2)
+ '@vercel/build-utils': 14.9.0
+ '@vercel/node': 12.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(@vercel/build-utils@14.9.0)(rollup@4.59.0)(supports-color@10.2.2)
'@vercel/static-config': 3.4.3(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)
transitivePeerDependencies:
- '@emnapi/core'
@@ -18059,9 +18024,9 @@ snapshots:
- rollup
- supports-color
- '@vercel/next@9.0.0(@vercel/build-utils@14.7.0)(rollup@4.59.0)(supports-color@10.2.2)':
+ '@vercel/next@11.0.0(@vercel/build-utils@14.9.0)(rollup@4.59.0)(supports-color@10.2.2)':
dependencies:
- '@vercel/build-utils': 14.7.0
+ '@vercel/build-utils': 14.9.0
'@vercel/nft': 1.10.0(rollup@4.59.0)(supports-color@10.2.2)
transitivePeerDependencies:
- encoding
@@ -18087,13 +18052,13 @@ snapshots:
- rollup
- supports-color
- '@vercel/node@10.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(@vercel/build-utils@14.7.0)(rollup@4.59.0)(supports-color@10.2.2)':
+ '@vercel/node@12.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(@vercel/build-utils@14.9.0)(rollup@4.59.0)(supports-color@10.2.2)':
dependencies:
'@edge-runtime/node-utils': 2.3.0
'@edge-runtime/primitives': 4.1.0
'@edge-runtime/vm': 3.2.0
'@types/node': 24.13.3
- '@vercel/build-utils': 14.7.0
+ '@vercel/build-utils': 14.9.0
'@vercel/error-utils': 2.2.1
'@vercel/nft': 1.10.0(rollup@4.59.0)(supports-color@10.2.2)
'@vercel/static-config': 3.4.3(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)
@@ -18132,14 +18097,14 @@ snapshots:
smol-toml: 1.6.1
zod: 3.22.4
- '@vercel/python@11.0.0(@vercel/build-utils@14.7.0)':
+ '@vercel/python@13.0.0(@vercel/build-utils@14.9.0)':
dependencies:
- '@vercel/build-utils': 14.7.0
+ '@vercel/build-utils': 14.9.0
'@vercel/python-analysis': 0.14.0
- '@vercel/redwood@7.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(@vercel/build-utils@14.7.0)(rollup@4.59.0)(supports-color@10.2.2)':
+ '@vercel/redwood@9.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(@vercel/build-utils@14.9.0)(rollup@4.59.0)(supports-color@10.2.2)':
dependencies:
- '@vercel/build-utils': 14.7.0
+ '@vercel/build-utils': 14.9.0
'@vercel/nft': 1.10.0(rollup@4.59.0)(supports-color@10.2.2)
'@vercel/static-config': 3.4.3(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)
semver: 6.3.1
@@ -18151,9 +18116,9 @@ snapshots:
- rollup
- supports-color
- '@vercel/remix-builder@10.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(@vercel/build-utils@14.7.0)(rollup@4.59.0)(supports-color@10.2.2)':
+ '@vercel/remix-builder@12.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(@vercel/build-utils@14.9.0)(rollup@4.59.0)(supports-color@10.2.2)':
dependencies:
- '@vercel/build-utils': 14.7.0
+ '@vercel/build-utils': 14.9.0
'@vercel/error-utils': 2.2.1
'@vercel/nft': 1.10.0(rollup@4.59.0)(supports-color@10.2.2)
'@vercel/static-config': 3.4.3(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)
@@ -18167,13 +18132,13 @@ snapshots:
- rollup
- supports-color
- '@vercel/ruby@7.0.0(@vercel/build-utils@14.7.0)':
+ '@vercel/ruby@9.0.0(@vercel/build-utils@14.9.0)':
dependencies:
- '@vercel/build-utils': 14.7.0
+ '@vercel/build-utils': 14.9.0
- '@vercel/rust@6.0.0(@vercel/build-utils@14.7.0)':
+ '@vercel/rust@8.0.0(@vercel/build-utils@14.9.0)':
dependencies:
- '@vercel/build-utils': 14.7.0
+ '@vercel/build-utils': 14.9.0
execa: 5.1.1
get-port: 5.1.1
smol-toml: 1.6.1
@@ -18196,11 +18161,11 @@ snapshots:
- bare-abort-controller
- react-native-b4a
- '@vercel/static-build@7.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(@vercel/build-utils@14.7.0)':
+ '@vercel/static-build@9.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(@vercel/build-utils@14.9.0)':
dependencies:
- '@vercel/build-utils': 14.7.0
+ '@vercel/build-utils': 14.9.0
'@vercel/gatsby-plugin-vercel-analytics': 1.0.12
- '@vercel/gatsby-plugin-vercel-builder': 2.2.50
+ '@vercel/gatsby-plugin-vercel-builder': 2.2.52
'@vercel/static-config': 3.4.3(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)
ts-morph: 12.0.0
transitivePeerDependencies:
@@ -18217,16 +18182,16 @@ snapshots:
- '@emnapi/core'
- '@emnapi/runtime'
- '@vercel/vc-native-darwin-arm64@59.10.0':
+ '@vercel/vc-native-darwin-arm64@59.11.1':
optional: true
- '@vercel/vc-native-darwin-x64@59.10.0':
+ '@vercel/vc-native-darwin-x64@59.11.1':
optional: true
- '@vercel/vc-native-linux-arm64@59.10.0':
+ '@vercel/vc-native-linux-arm64@59.11.1':
optional: true
- '@vercel/vc-native-linux-x64@59.10.0':
+ '@vercel/vc-native-linux-x64@59.11.1':
optional: true
'@vitejs/plugin-react@6.1.1(babel-plugin-react-compiler@1.0.0)(vite@8.2.2(@types/node@24.13.3)(esbuild@0.25.10)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.12)(yaml@2.9.0))':
@@ -18978,20 +18943,6 @@ snapshots:
optionalDependencies:
magicast: 0.5.4
- c8@12.0.0:
- dependencies:
- '@bcoe/v8-coverage': 1.0.2
- '@istanbuljs/schema': 0.1.6
- find-up: 5.0.0
- foreground-child: 3.3.1
- istanbul-lib-coverage: 3.2.2
- istanbul-lib-report: 3.0.1
- istanbul-reports: 3.2.0
- test-exclude: 8.0.0
- v8-to-istanbul: 9.3.0
- yargs: 18.1.0
- yargs-parser: 21.1.1
-
cac@6.7.14: {}
call-bind-apply-helpers@1.0.2:
@@ -24786,11 +24737,6 @@ snapshots:
get-east-asian-width: 1.6.0
strip-ansi: 7.2.0
- string-width@8.2.2:
- dependencies:
- get-east-asian-width: 1.6.0
- strip-ansi: 7.2.0
-
string.prototype.matchall@4.1.0:
dependencies:
call-bind: 1.0.9
@@ -24986,12 +24932,6 @@ snapshots:
commander: 2.20.3
source-map-support: 0.5.21
- test-exclude@8.0.0:
- dependencies:
- '@istanbuljs/schema': 0.1.6
- glob: 10.5.0
- minimatch: 10.2.3
-
text-decoder@1.2.7:
dependencies:
b4a: 1.8.1
@@ -25506,12 +25446,6 @@ snapshots:
v8-compile-cache-lib@3.0.1: {}
- v8-to-istanbul@9.3.0:
- dependencies:
- '@jridgewell/trace-mapping': 0.3.31
- '@types/istanbul-lib-coverage': 2.0.6
- convert-source-map: 2.0.0
-
validate-npm-package-name@5.0.1: {}
vary@1.1.2: {}
@@ -25541,35 +25475,35 @@ snapshots:
core-js: 3.50.0
mutation-observer: 1.0.3
- vercel@59.10.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(bufferutil@4.1.0)(rollup@4.59.0)(supports-color@10.2.2)(utf-8-validate@6.0.6):
+ vercel@59.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(bufferutil@4.1.0)(rollup@4.59.0)(supports-color@10.2.2)(utf-8-validate@6.0.6):
dependencies:
- '@vercel/backends': 5.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(@vercel/build-utils@14.7.0)(rollup@4.59.0)(supports-color@10.2.2)
+ '@vercel/backends': 7.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(@vercel/build-utils@14.9.0)(rollup@4.59.0)(supports-color@10.2.2)
'@vercel/blob': 2.2.0
- '@vercel/build-utils': 14.7.0
+ '@vercel/build-utils': 14.9.0
'@vercel/cli-auth': 0.3.5
'@vercel/cli-config': 0.2.4
- '@vercel/container': 5.0.0(@vercel/build-utils@14.7.0)
+ '@vercel/container': 7.0.0(@vercel/build-utils@14.9.0)
'@vercel/detect-agent': 1.2.5
- '@vercel/elysia': 5.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(@vercel/build-utils@14.7.0)(rollup@4.59.0)(supports-color@10.2.2)
- '@vercel/express': 5.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(@vercel/build-utils@14.7.0)(rollup@4.59.0)(supports-color@10.2.2)
- '@vercel/fastify': 5.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(@vercel/build-utils@14.7.0)(rollup@4.59.0)(supports-color@10.2.2)
+ '@vercel/elysia': 7.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(@vercel/build-utils@14.9.0)(rollup@4.59.0)(supports-color@10.2.2)
+ '@vercel/express': 7.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(@vercel/build-utils@14.9.0)(rollup@4.59.0)(supports-color@10.2.2)
+ '@vercel/fastify': 7.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(@vercel/build-utils@14.9.0)(rollup@4.59.0)(supports-color@10.2.2)
'@vercel/fun': 1.3.0(supports-color@10.2.2)
- '@vercel/go': 8.0.0(@vercel/build-utils@14.7.0)
- '@vercel/h3': 5.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(@vercel/build-utils@14.7.0)(rollup@4.59.0)(supports-color@10.2.2)
- '@vercel/hono': 5.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(@vercel/build-utils@14.7.0)(rollup@4.59.0)(supports-color@10.2.2)
- '@vercel/hydrogen': 6.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(@vercel/build-utils@14.7.0)
- '@vercel/koa': 5.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(@vercel/build-utils@14.7.0)(rollup@4.59.0)(supports-color@10.2.2)
- '@vercel/nestjs': 5.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(@vercel/build-utils@14.7.0)(rollup@4.59.0)(supports-color@10.2.2)
- '@vercel/next': 9.0.0(@vercel/build-utils@14.7.0)(rollup@4.59.0)(supports-color@10.2.2)
- '@vercel/node': 10.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(@vercel/build-utils@14.7.0)(rollup@4.59.0)(supports-color@10.2.2)
+ '@vercel/go': 10.0.0(@vercel/build-utils@14.9.0)
+ '@vercel/h3': 7.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(@vercel/build-utils@14.9.0)(rollup@4.59.0)(supports-color@10.2.2)
+ '@vercel/hono': 7.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(@vercel/build-utils@14.9.0)(rollup@4.59.0)(supports-color@10.2.2)
+ '@vercel/hydrogen': 8.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(@vercel/build-utils@14.9.0)
+ '@vercel/koa': 7.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(@vercel/build-utils@14.9.0)(rollup@4.59.0)(supports-color@10.2.2)
+ '@vercel/nestjs': 7.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(@vercel/build-utils@14.9.0)(rollup@4.59.0)(supports-color@10.2.2)
+ '@vercel/next': 11.0.0(@vercel/build-utils@14.9.0)(rollup@4.59.0)(supports-color@10.2.2)
+ '@vercel/node': 12.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(@vercel/build-utils@14.9.0)(rollup@4.59.0)(supports-color@10.2.2)
'@vercel/prepare-flags-definitions': 0.3.0
- '@vercel/python': 11.0.0(@vercel/build-utils@14.7.0)
+ '@vercel/python': 13.0.0(@vercel/build-utils@14.9.0)
'@vercel/python-analysis': 0.14.0
- '@vercel/redwood': 7.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(@vercel/build-utils@14.7.0)(rollup@4.59.0)(supports-color@10.2.2)
- '@vercel/remix-builder': 10.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(@vercel/build-utils@14.7.0)(rollup@4.59.0)(supports-color@10.2.2)
- '@vercel/ruby': 7.0.0(@vercel/build-utils@14.7.0)
- '@vercel/rust': 6.0.0(@vercel/build-utils@14.7.0)
- '@vercel/static-build': 7.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(@vercel/build-utils@14.7.0)
+ '@vercel/redwood': 9.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(@vercel/build-utils@14.9.0)(rollup@4.59.0)(supports-color@10.2.2)
+ '@vercel/remix-builder': 12.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(@vercel/build-utils@14.9.0)(rollup@4.59.0)(supports-color@10.2.2)
+ '@vercel/ruby': 9.0.0(@vercel/build-utils@14.9.0)
+ '@vercel/rust': 8.0.0(@vercel/build-utils@14.9.0)
+ '@vercel/static-build': 9.0.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(@vercel/build-utils@14.9.0)
chokidar: 4.0.0
esbuild: 0.25.10
jose: 5.9.6
@@ -25581,10 +25515,10 @@ snapshots:
uuid: 14.0.1
zod: 4.1.11
optionalDependencies:
- '@vercel/vc-native-darwin-arm64': 59.10.0
- '@vercel/vc-native-darwin-x64': 59.10.0
- '@vercel/vc-native-linux-arm64': 59.10.0
- '@vercel/vc-native-linux-x64': 59.10.0
+ '@vercel/vc-native-darwin-arm64': 59.11.1
+ '@vercel/vc-native-darwin-x64': 59.11.1
+ '@vercel/vc-native-linux-arm64': 59.11.1
+ '@vercel/vc-native-linux-x64': 59.11.1
transitivePeerDependencies:
- '@emnapi/core'
- '@emnapi/runtime'
@@ -26016,15 +25950,6 @@ snapshots:
y18n: 5.0.8
yargs-parser: 22.0.0
- yargs@18.1.0:
- dependencies:
- cliui: 9.0.1
- escalade: 3.2.0
- get-caller-file: 2.0.5
- string-width: 8.2.2
- y18n: 5.0.8
- yargs-parser: 22.0.0
-
yauzl-clone@1.0.4:
dependencies:
events-intercept: 2.0.0
diff --git a/scripts/README.md b/scripts/README.md
index 569fdd99..0b9fb499 100644
--- a/scripts/README.md
+++ b/scripts/README.md
@@ -6,7 +6,7 @@ Utility scripts for this monorepo.
### `run-qa.mjs`
-Runs the full QA pipeline sequentially: install, checktypes, lint:fix, build, test, test:e2e. Stops immediately on the first failure and prints a clear error banner.
+Runs the full QA pipeline sequentially: install (skipped when `node_modules` exists), checktypes, lint, OpenAPI generate + drift check, build, test, test:e2e (`SKIP_BUILD=1`). Stops immediately on the first failure and prints a clear error banner.
**Usage**: Via pnpm at repository root:
```bash
@@ -182,7 +182,7 @@ pnpm setup:deepsec
### `setup:playwright` (package.json)
-Installs Playwright Chromium for web and API E2E tests. Default browser cache when `PLAYWRIGHT_BROWSERS_PATH` is unset: Linux `~/.cache/ms-playwright`, macOS `~/Library/Caches/ms-playwright`, Windows `%USERPROFILE%\AppData\Local\ms-playwright`.
+Installs Playwright Chromium for `@repo/api` and `@repo/web` E2E tests. Default browser cache when `PLAYWRIGHT_BROWSERS_PATH` is unset: Linux `~/.cache/ms-playwright`, macOS `~/Library/Caches/ms-playwright`, Windows `%USERPROFILE%\AppData\Local\ms-playwright`.
**Usage**: Automatically runs during `pnpm setup`. Can be run manually:
```bash
diff --git a/scripts/e2e-local-shared.mjs b/scripts/e2e-local-shared.mjs
new file mode 100644
index 00000000..be1925b9
--- /dev/null
+++ b/scripts/e2e-local-shared.mjs
@@ -0,0 +1,78 @@
+#!/usr/bin/env node
+import { spawnSync } from 'node:child_process'
+import { existsSync, readFileSync } from 'node:fs'
+import { dirname, join } from 'node:path'
+import { fileURLToPath } from 'node:url'
+
+const scriptDir = dirname(fileURLToPath(import.meta.url))
+export const repoRoot = dirname(scriptDir)
+
+export const defaultE2eEnv = {
+ ALLOW_TEST: 'true',
+ PGLITE: 'true',
+ NODE_ENV: 'test',
+ RATE_LIMIT_MAX: '10000',
+ WEBAUTHN_RP_NAME: 'Test App',
+ TOTP_ISSUER: 'Test App',
+}
+
+export const defaultE2eJwt = 'e2e-jwt-secret-min-32-chars-for-tests'
+
+export function loadEnvTest() {
+ const path = join(repoRoot, 'apps/api/.env.test')
+ if (!existsSync(path)) return {}
+ const lines = readFileSync(path, 'utf8').split('\n')
+ const out = {}
+ for (const line of lines) {
+ const idx = line.indexOf('=')
+ if (idx < 0 || line.startsWith('#')) continue
+ const key = line.slice(0, idx).trim()
+ let val = line.slice(idx + 1).trim()
+ if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'")))
+ val = val.slice(1, -1)
+ out[key] = val
+ }
+ return out
+}
+
+export function waitForUrl(url, timeoutMs = 60_000) {
+ const start = Date.now()
+ return new Promise(resolve => {
+ const check = async () => {
+ try {
+ const res = await fetch(url, { signal: AbortSignal.timeout(2000) })
+ if (res.ok || res.status === 307) return resolve(true)
+ } catch {
+ // continue polling
+ }
+ if (Date.now() - start > timeoutMs) return resolve(false)
+ setTimeout(check, 500)
+ }
+ check()
+ })
+}
+
+export function killTestServerPorts() {
+ if (process.env.SKIP_KILL_PORTS) return
+ try {
+ spawnSync('bash', [join(repoRoot, 'scripts/kill-test-servers.sh')], {
+ cwd: repoRoot,
+ stdio: 'pipe',
+ })
+ } catch {
+ // ports may not be in use
+ }
+}
+
+export function buildE2eSpawnEnv({ loaded = loadEnvTest(), extra = {} } = {}) {
+ const jwt = loaded.JWT_SECRET ?? process.env.JWT_SECRET ?? defaultE2eJwt
+ return {
+ ...process.env,
+ ...loaded,
+ ...defaultE2eEnv,
+ WEBAUTHN_RP_NAME:
+ loaded.WEBAUTHN_RP_NAME ?? process.env.WEBAUTHN_RP_NAME ?? defaultE2eEnv.WEBAUTHN_RP_NAME,
+ JWT_SECRET: jwt,
+ ...extra,
+ }
+}
diff --git a/scripts/run-qa.mjs b/scripts/run-qa.mjs
index 3ab46531..fca8b9af 100644
--- a/scripts/run-qa.mjs
+++ b/scripts/run-qa.mjs
@@ -1,11 +1,11 @@
#!/usr/bin/env node
/**
- * Run QA pipeline: install, checktypes, lint:fix, build, test, test:e2e.
+ * Run QA pipeline: install (if needed), checktypes, lint, OpenAPI drift, build, test, e2e.
* Stops immediately on first failure and reports which phase failed.
- * Used by pnpm qa.
*/
import { spawnSync } from 'node:child_process'
-import { dirname } from 'node:path'
+import { existsSync } from 'node:fs'
+import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
const scriptDir = dirname(fileURLToPath(import.meta.url))
@@ -16,21 +16,42 @@ const qaBuildEnv = process.env.JWT_SECRET
: { JWT_SECRET: 'qa-build-placeholder-min-32-chars-to-pass-validation' }
const skipTests = process.env.QA_SKIP_TESTS === '1' || process.env.QA_SKIP_TESTS === 'true'
+const hasNodeModules = existsSync(join(repoRoot, 'node_modules'))
const phases = [
- { name: 'install', cmd: 'pnpm', args: ['i', '--no-frozen-lockfile'] },
+ ...(hasNodeModules ? [] : [{ name: 'install', cmd: 'pnpm', args: ['i', '--frozen-lockfile'] }]),
{
name: 'checktypes',
cmd: 'pnpm',
args: ['exec', 'turbo', 'run', 'checktypes', '--concurrency=100%'],
},
- { name: 'lint:fix', cmd: 'pnpm', args: ['lint:fix'] },
+ { name: 'lint', cmd: 'pnpm', args: ['lint'] },
+ {
+ name: 'openapi-drift',
+ cmd: 'pnpm',
+ args: ['generate'],
+ env: qaBuildEnv,
+ },
+ {
+ name: 'openapi-drift-check',
+ cmd: 'git',
+ args: ['diff', '--exit-code', '--', 'apps/api/openapi/openapi.json', 'packages/core/src/gen'],
+ },
{ name: 'build', cmd: 'pnpm', args: ['build'], env: qaBuildEnv },
...(skipTests
? []
: [
- { name: 'test', cmd: 'pnpm', args: ['exec', 'turbo', 'run', 'test', '--concurrency=100%'] },
- { name: 'test:e2e', cmd: 'pnpm', args: ['test:e2e'] },
+ {
+ name: 'test',
+ cmd: 'pnpm',
+ args: ['exec', 'turbo', 'run', 'test', '--concurrency=100%'],
+ },
+ {
+ name: 'test:e2e',
+ cmd: 'pnpm',
+ args: ['test:e2e'],
+ env: { SKIP_BUILD: '1', ...qaBuildEnv, NEXT_PUBLIC_API_URL: 'http://localhost:3001' },
+ },
]),
]
diff --git a/turbo.json b/turbo.json
index 777282f2..d4cb300e 100644
--- a/turbo.json
+++ b/turbo.json
@@ -1,7 +1,7 @@
{
"$schema": "https://turbo.build/schema.json",
"ui": "tui",
- "globalPassThroughEnv": ["QA_SKIP_TESTS", "SKIP_KILL_PORTS"],
+ "globalPassThroughEnv": ["QA_SKIP_TESTS", "SKIP_KILL_PORTS", "SKIP_BUILD"],
"tasks": {
"generate": {
"dependsOn": [],
@@ -138,7 +138,8 @@
"WEBAUTHN_RP_NAME",
"TOTP_ISSUER",
"RATE_LIMIT_MAX",
- "TEST_LOG_LEVEL"
+ "TEST_LOG_LEVEL",
+ "SKIP_BUILD"
]
},
"lint:eslint:fix": {
@@ -191,7 +192,8 @@
"WEBAUTHN_RP_NAME",
"TOTP_ISSUER",
"RATE_LIMIT_MAX",
- "TEST_LOG_LEVEL"
+ "TEST_LOG_LEVEL",
+ "SKIP_BUILD"
]
},
"checktypes": { "env": ["NODE_ENV"] },
@@ -202,39 +204,14 @@
"DATABASE_URL",
"PGLITE",
"RUN_PG_MIGRATE",
- "AI_DEFAULT_MODEL",
- "AI_PROVIDER",
- "ANTHROPIC_API_KEY",
- "OLLAMA_BASE_URL",
- "OPEN_ROUTER_API_KEY",
- "CI",
- "VITEST_WORKER_ID",
- "ALLOW_TEST",
- "ALLOWED_ORIGINS",
- "VITEST",
- "AUTH_COOKIE_NAME",
- "NEXT_PUBLIC_AUTH_COOKIE_NAME"
- ]
- },
- "@repo/web#test": {
- "cache": false,
- "env": [
- "NODE_ENV",
- "DATABASE_URL",
- "PGLITE",
- "RUN_PG_MIGRATE",
- "AI_DEFAULT_MODEL",
"AI_PROVIDER",
"ANTHROPIC_API_KEY",
- "OLLAMA_BASE_URL",
- "OPEN_ROUTER_API_KEY",
"CI",
"VITEST_WORKER_ID",
"ALLOW_TEST",
- "ALLOWED_ORIGINS",
- "VITEST",
- "AUTH_COOKIE_NAME",
- "NEXT_PUBLIC_AUTH_COOKIE_NAME"
+ "WEBAUTHN_RP_NAME",
+ "TOTP_ISSUER",
+ "RATE_LIMIT_MAX"
]
},
"test:unit": {
@@ -284,7 +261,8 @@
"PLAYWRIGHT_API_URL",
"VERCEL_AUTOMATION_BYPASS_SECRET",
"AUTH_COOKIE_NAME",
- "NEXT_PUBLIC_AUTH_COOKIE_NAME"
+ "NEXT_PUBLIC_AUTH_COOKIE_NAME",
+ "SKIP_BUILD"
]
},
"dev": {