diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2c6c9bf4..5a02f3b4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -43,7 +43,9 @@ Example client config: The dev server supports the same [query params as the hosted endpoint](https://supabase.com/docs/guides/ai-tools/mcp#configuration-options). The access token comes from the client's `Authorization` header on each request. Restart the server in your MCP client after each change. -Flags: `--http`, `--port` (default 3111), `--api-url`, `--content-api-url`, `--version`. +Add `--oauth` to sign in with Supabase OAuth in the browser instead. The server then attaches the token to every request itself and the client config needs no `headers`. The session is saved to `~/.supabase/mcp-oauth.json`, next to the Supabase CLI's files (`SUPABASE_HOME` overrides the directory for both). Delete that file to sign out. + +Flags: `--http`, `--port` (default 3111), `--oauth`, `--api-url`, `--content-api-url`, `--version`. To try the HTTP entry from a PR without cloning, run the preview build published by pkg.pr.new: diff --git a/packages/mcp-server-supabase/package.json b/packages/mcp-server-supabase/package.json index 67dd3693..cf89f3af 100644 --- a/packages/mcp-server-supabase/package.json +++ b/packages/mcp-server-supabase/package.json @@ -53,6 +53,7 @@ }, "dependencies": { "@mjackson/multipart-parser": "^0.10.1", + "@modelcontextprotocol/client": "catalog:", "@modelcontextprotocol/node": "catalog:", "@supabase/mcp-utils": "workspace:^", "common-tags": "^1.8.2", @@ -68,7 +69,6 @@ "@ai-sdk/anthropic": "catalog:", "@ai-sdk/mcp": "catalog:", "@electric-sql/pglite": "^0.2.17", - "@modelcontextprotocol/client": "catalog:", "@modelcontextprotocol/server": "catalog:", "@total-typescript/tsconfig": "^1.0.4", "@types/common-tags": "^1.8.4", diff --git a/packages/mcp-server-supabase/src/cli.ts b/packages/mcp-server-supabase/src/cli.ts index 92f742fc..58bfb984 100755 --- a/packages/mcp-server-supabase/src/cli.ts +++ b/packages/mcp-server-supabase/src/cli.ts @@ -6,6 +6,7 @@ import packageJson from '../package.json' with { type: 'json' }; import { createSupabaseApiPlatform } from './platform/api-platform.js'; import { createSupabaseMcpServer } from './server.js'; import { startLocalHttpEntry } from './transports/local-http-entry.js'; +import { login } from './transports/oauth-client.js'; import { parseList } from './transports/util.js'; import { parseFeatureGroups } from './util.js'; @@ -23,6 +24,7 @@ async function main() { ['features']: cliFeatures, ['http']: http, ['port']: cliPort, + ['oauth']: oauth, }, } = parseArgs({ options: { @@ -56,6 +58,10 @@ async function main() { type: 'string', default: '3111', }, + ['oauth']: { + type: 'boolean', + default: false, + }, }, }); @@ -70,10 +76,15 @@ async function main() { cliContentApiUrl ?? process.env.SUPABASE_CONTENT_API_URL; if (http) { + // The hosted MCP server's OAuth discovery points at the matching Management API. + const mcpUrl = new URL(apiUrl ?? 'https://api.supabase.com'); + mcpUrl.host = mcpUrl.host.replace(/^api\./, 'mcp.'); + const accessToken = oauth ? await login(`${mcpUrl.origin}/mcp`) : undefined; const entry = await startLocalHttpEntry({ port: Number(cliPort), apiUrl, contentApiUrl, + accessToken, }); console.error(`Supabase MCP server listening on ${entry.url}`); return; diff --git a/packages/mcp-server-supabase/src/transports/local-http-entry.test.ts b/packages/mcp-server-supabase/src/transports/local-http-entry.test.ts index 7943fffc..205ece31 100644 --- a/packages/mcp-server-supabase/src/transports/local-http-entry.test.ts +++ b/packages/mcp-server-supabase/src/transports/local-http-entry.test.ts @@ -9,7 +9,7 @@ import type { InputRequiredResult, VersionNegotiationMode, } from '@modelcontextprotocol/client'; -import { http, passthrough } from 'msw'; +import { http, HttpResponse, passthrough } from 'msw'; import type { SetupServer } from 'msw/node'; import { afterEach, beforeEach, describe, expect, test } from 'vitest'; @@ -141,6 +141,60 @@ describe('startLocalHttpEntry', () => { }); }); + test('rejects a browser Origin', async () => { + const response = await fetch(entry.url, { + method: 'POST', + headers: { + ...AUTH_HEADERS, + 'content-type': 'application/json', + origin: 'https://evil.example', + }, + body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/list' }), + }); + + expect(response.status).toBe(403); + }); + + test('OAuth mode reads the token source on every request', async () => { + const issued: string[] = []; + const oauthEntry = await startLocalHttpEntry({ + port: 0, + apiUrl: API_URL, + accessToken: async () => { + const token = `${ACCESS_TOKEN}-${issued.length + 1}`; + issued.push(token); + return token; + }, + log: () => {}, + }); + cleanups.push(() => oauthEntry.close()); + const seen: Array = []; + mockServer.use( + http.all(`${new URL(oauthEntry.url).origin}/*`, () => passthrough()), + http.get(`${API_URL}/v1/projects`, ({ request }) => { + seen.push(request.headers.get('authorization')); + return HttpResponse.json([]); + }) + ); + const client = new Client( + { name: MCP_CLIENT_NAME, version: MCP_CLIENT_VERSION }, + { versionNegotiation: { mode: { pin: MODERN_PROTOCOL_VERSION } } } + ); + await client.connect( + new StreamableHTTPClientTransport(new URL(oauthEntry.url)) + ); + cleanups.push(() => client.close()); + + await client.callTool({ name: 'list_projects', arguments: {} }); + await client.callTool({ name: 'list_projects', arguments: {} }); + + expect(seen).toHaveLength(2); + expect(seen[0]).not.toBe(seen[1]); + expect(issued.map((token) => `Bearer ${token}`)).toEqual( + expect.arrayContaining(seen) + ); + }); + test('sends a form-capable client a cost elicitation', async () => { const client = await connect( { pin: MODERN_PROTOCOL_VERSION }, diff --git a/packages/mcp-server-supabase/src/transports/local-http-entry.ts b/packages/mcp-server-supabase/src/transports/local-http-entry.ts index 7723f379..6151a676 100644 --- a/packages/mcp-server-supabase/src/transports/local-http-entry.ts +++ b/packages/mcp-server-supabase/src/transports/local-http-entry.ts @@ -11,6 +11,7 @@ import { isJSONRPCRequest, isSpecType, localhostAllowedHostnames, + originValidationResponse, PROTOCOL_VERSION_META_KEY, } from '@modelcontextprotocol/server'; import { z } from 'zod/v4'; @@ -24,6 +25,8 @@ export type LocalHttpEntryOptions = { port: number; apiUrl?: string; contentApiUrl?: string; + /** OAuth mode. Supplies the token for every request. */ + accessToken?: () => Promise; log?: (line: string) => void; }; @@ -72,25 +75,29 @@ export async function startLocalHttpEntry({ port, apiUrl, contentApiUrl, + accessToken: tokenSource, log = (line) => console.error(`[${new Date().toLocaleTimeString('en-GB')}] ${line}`), }: LocalHttpEntryOptions) { const requestStateKey = randomBytes(32); + // OAuth tokens refresh, so the principal is a per-process value instead of a token hash. + const processPrincipal = randomBytes(16).toString('hex'); const allowedHostnames = localhostAllowedHostnames(); const server = createServer( toNodeHandler( { fetch: async (request) => { - const rejected = hostHeaderValidationResponse( - request, - allowedHostnames - ); + const rejected = + hostHeaderValidationResponse(request, allowedHostnames) ?? + originValidationResponse(request, []); if (rejected) return rejected; - const accessToken = request.headers - .get('authorization') - ?.match(/^Bearer (.+)$/i)?.[1]; + const accessToken = tokenSource + ? await tokenSource() + : request.headers + .get('authorization') + ?.match(/^Bearer (.+)$/i)?.[1]; if (!accessToken) { return Response.json( { error: 'missing bearer token' }, @@ -136,9 +143,9 @@ export async function startLocalHttpEntry({ costConfirmation: { requestStateKey, // One process can serve several PATs, so the principal is the token's hash. - principal: createHash('sha256') - .update(accessToken) - .digest('hex'), + principal: tokenSource + ? processPrincipal + : createHash('sha256').update(accessToken).digest('hex'), enabledTools: ['create_project', 'create_branch'], }, }), diff --git a/packages/mcp-server-supabase/src/transports/oauth-client.ts b/packages/mcp-server-supabase/src/transports/oauth-client.ts new file mode 100644 index 00000000..1e02f79a --- /dev/null +++ b/packages/mcp-server-supabase/src/transports/oauth-client.ts @@ -0,0 +1,114 @@ +import { spawn } from 'node:child_process'; +import { once } from 'node:events'; +import { mkdir, readFile, writeFile } from 'node:fs/promises'; +import { createServer } from 'node:http'; +import { homedir } from 'node:os'; +import { dirname, join } from 'node:path'; +import { + auth, + type OAuthClientProvider, + type StoredOAuthClientInformation, + type StoredOAuthTokens, +} from '@modelcontextprotocol/client'; + +// Same directory the Supabase CLI keeps its login in. +export const STORE_PATH = join( + process.env.SUPABASE_HOME ?? join(homedir(), '.supabase'), + 'mcp-oauth.json' +); +const REDIRECT_URL = 'http://127.0.0.1:3112/callback'; + +type Stored = { + client?: StoredOAuthClientInformation; + tokens?: StoredOAuthTokens; + codeVerifier?: string; + expiresAt?: number; +}; + +async function read(): Promise { + try { + return JSON.parse(await readFile(STORE_PATH, 'utf8')); + } catch { + return {}; + } +} + +async function write(patch: Stored) { + await mkdir(dirname(STORE_PATH), { recursive: true }); + await writeFile(STORE_PATH, JSON.stringify({ ...(await read()), ...patch }), { + mode: 0o600, + }); +} + +const provider: OAuthClientProvider = { + redirectUrl: REDIRECT_URL, + clientMetadata: { + client_name: 'Supabase MCP local dev server', + redirect_uris: [REDIRECT_URL], + grant_types: ['authorization_code', 'refresh_token'], + response_types: ['code'], + token_endpoint_auth_method: 'client_secret_post', + }, + clientInformation: async () => (await read()).client, + saveClientInformation: (client) => write({ client }), + tokens: async () => (await read()).tokens, + saveTokens: (tokens) => + write({ + tokens, + expiresAt: Date.now() + (tokens.expires_in ?? 3600) * 1000, + }), + saveCodeVerifier: (codeVerifier) => write({ codeVerifier }), + codeVerifier: async () => { + const { codeVerifier } = await read(); + if (!codeVerifier) throw new Error('No pending sign-in'); + return codeVerifier; + }, + invalidateCredentials: (scope) => + write({ + ...(scope === 'all' || scope === 'client' ? { client: undefined } : {}), + ...(scope === 'all' || scope === 'tokens' ? { tokens: undefined } : {}), + }), + redirectToAuthorization: (url) => { + console.error(`Sign in to Supabase in your browser:\n${url}`); + const openers: Partial> = { + darwin: 'open', + win32: 'start', + }; + const opener = openers[process.platform] ?? 'xdg-open'; + spawn(opener, [url.href], { stdio: 'ignore', detached: true }).unref(); + }, +}; + +async function waitForCode() { + const server = createServer((req, res) => { + const code = new URL(req.url ?? '/', REDIRECT_URL).searchParams.get('code'); + res.end(code ? 'Signed in. You can close this tab.' : 'Missing code.'); + if (code) server.emit('code', code); + }); + server.listen(3112, '127.0.0.1'); + await once(server, 'listening'); + const [code] = await once(server, 'code'); + server.close(); + return String(code); +} + +/** Signs in if needed and returns a token getter that refreshes on expiry. */ +export async function login(serverUrl: string) { + if ((await auth(provider, { serverUrl })) === 'REDIRECT') { + await auth(provider, { serverUrl, authorizationCode: await waitForCode() }); + } + // Refresh tokens are single use, so concurrent callers share one refresh. + let refreshing: Promise | undefined; + return async () => { + let { tokens, expiresAt = 0 } = await read(); + if (!tokens || expiresAt < Date.now() + 60_000) { + refreshing ??= auth(provider, { serverUrl }).finally(() => { + refreshing = undefined; + }); + await refreshing; + ({ tokens } = await read()); + } + if (!tokens) throw new Error('Not signed in'); + return tokens.access_token; + }; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0bb40548..992e1fa0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -40,7 +40,7 @@ importers: version: 0.0.68 supabase: specifier: ^2.1.1 - version: 2.34.3(supports-color@10.2.0) + version: 2.34.3 packages/mcp-server-postgrest: dependencies: @@ -71,7 +71,7 @@ importers: version: 3.6.2 tsup: specifier: ^8.3.5 - version: 8.5.0(postcss@8.5.6)(supports-color@10.2.0)(tsx@4.20.4)(typescript@5.9.2) + version: 8.5.0(postcss@8.5.6)(tsx@4.20.4)(typescript@5.9.2) tsx: specifier: ^4.19.2 version: 4.20.4 @@ -80,7 +80,7 @@ importers: version: 5.9.2 vitest: specifier: ^2.1.9 - version: 2.1.9(@types/node@22.17.2)(msw@2.10.5(@types/node@22.17.2)(typescript@5.9.2))(supports-color@10.2.0) + version: 2.1.9(@types/node@22.17.2)(msw@2.10.5(@types/node@22.17.2)(typescript@5.9.2)) zod: specifier: 'catalog:' version: 4.2.1 @@ -90,6 +90,9 @@ importers: '@mjackson/multipart-parser': specifier: ^0.10.1 version: 0.10.1 + '@modelcontextprotocol/client': + specifier: 'catalog:' + version: 2.0.0 '@modelcontextprotocol/node': specifier: 'catalog:' version: 2.0.0(@modelcontextprotocol/server@2.0.0)(hono@4.13.5) @@ -118,9 +121,6 @@ importers: '@electric-sql/pglite': specifier: ^0.2.17 version: 0.2.17 - '@modelcontextprotocol/client': - specifier: 'catalog:' - version: 2.0.0 '@modelcontextprotocol/server': specifier: 'catalog:' version: 2.0.0 @@ -135,7 +135,7 @@ importers: version: 22.17.2 '@vitest/coverage-v8': specifier: ^2.1.9 - version: 2.1.9(supports-color@10.2.0)(vitest@2.1.9(@types/node@22.17.2)(msw@2.10.5(@types/node@22.17.2)(typescript@5.9.2))(supports-color@10.2.0)) + version: 2.1.9(vitest@2.1.9(@types/node@22.17.2)(msw@2.10.5(@types/node@22.17.2)(typescript@5.9.2))) ai: specifier: 'catalog:' version: 6.0.100(zod@4.2.1) @@ -162,7 +162,7 @@ importers: version: 3.6.2 tsup: specifier: ^8.3.5 - version: 8.5.0(postcss@8.5.6)(supports-color@10.2.0)(tsx@4.20.4)(typescript@5.9.2) + version: 8.5.0(postcss@8.5.6)(tsx@4.20.4)(typescript@5.9.2) tsx: specifier: ^4.19.2 version: 4.20.4 @@ -174,7 +174,7 @@ importers: version: 5.4.19(@types/node@22.17.2) vitest: specifier: ^2.1.9 - version: 2.1.9(@types/node@22.17.2)(msw@2.10.5(@types/node@22.17.2)(typescript@5.9.2))(supports-color@10.2.0) + version: 2.1.9(@types/node@22.17.2)(msw@2.10.5(@types/node@22.17.2)(typescript@5.9.2)) zod: specifier: 'catalog:' version: 4.2.1 @@ -198,13 +198,13 @@ importers: version: 3.6.2 tsup: specifier: ^8.3.5 - version: 8.5.0(postcss@8.5.6)(supports-color@10.2.0)(tsx@4.20.4)(typescript@5.9.2) + version: 8.5.0(postcss@8.5.6)(tsx@4.20.4)(typescript@5.9.2) typescript: specifier: ^5.6.3 version: 5.9.2 vitest: specifier: ^2.1.9 - version: 2.1.9(@types/node@22.17.2)(msw@2.10.5(@types/node@22.17.2)(typescript@5.9.2))(supports-color@10.2.0) + version: 2.1.9(@types/node@22.17.2)(msw@2.10.5(@types/node@22.17.2)(typescript@5.9.2)) zod: specifier: 'catalog:' version: 4.2.1 @@ -301,28 +301,24 @@ packages: engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] - libc: [musl] '@biomejs/cli-linux-arm64@1.9.4': resolution: {integrity: sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] - libc: [glibc] '@biomejs/cli-linux-x64-musl@1.9.4': resolution: {integrity: sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] - libc: [musl] '@biomejs/cli-linux-x64@1.9.4': resolution: {integrity: sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] - libc: [glibc] '@biomejs/cli-win32-arm64@1.9.4': resolution: {integrity: sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==} @@ -860,67 +856,56 @@ packages: resolution: {integrity: sha512-HZZBXJL1udxlCVvoVadstgiU26seKkHbbAMLg7680gAcMnRNP9SAwTMVet02ANA94kXEI2VhBnXs4e5nf7KG2A==} cpu: [arm] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.47.1': resolution: {integrity: sha512-sZ5p2I9UA7T950JmuZ3pgdKA6+RTBr+0FpK427ExW0t7n+QwYOcmDTK/aRlzoBrWyTpJNlS3kacgSlSTUg6P/Q==} cpu: [arm] os: [linux] - libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.47.1': resolution: {integrity: sha512-3hBFoqPyU89Dyf1mQRXCdpc6qC6At3LV6jbbIOZd72jcx7xNk3aAp+EjzAtN6sDlmHFzsDJN5yeUySvorWeRXA==} cpu: [arm64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.47.1': resolution: {integrity: sha512-49J4FnMHfGodJWPw73Ve+/hsPjZgcXQGkmqBGZFvltzBKRS+cvMiWNLadOMXKGnYRhs1ToTGM0sItKISoSGUNA==} cpu: [arm64] os: [linux] - libc: [musl] '@rollup/rollup-linux-loongarch64-gnu@4.47.1': resolution: {integrity: sha512-4yYU8p7AneEpQkRX03pbpLmE21z5JNys16F1BZBZg5fP9rIlb0TkeQjn5du5w4agConCCEoYIG57sNxjryHEGg==} cpu: [loong64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-ppc64-gnu@4.47.1': resolution: {integrity: sha512-fAiq+J28l2YMWgC39jz/zPi2jqc0y3GSRo1yyxlBHt6UN0yYgnegHSRPa3pnHS5amT/efXQrm0ug5+aNEu9UuQ==} cpu: [ppc64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.47.1': resolution: {integrity: sha512-daoT0PMENNdjVYYU9xec30Y2prb1AbEIbb64sqkcQcSaR0zYuKkoPuhIztfxuqN82KYCKKrj+tQe4Gi7OSm1ow==} cpu: [riscv64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.47.1': resolution: {integrity: sha512-JNyXaAhWtdzfXu5pUcHAuNwGQKevR+6z/poYQKVW+pLaYOj9G1meYc57/1Xv2u4uTxfu9qEWmNTjv/H/EpAisw==} cpu: [riscv64] os: [linux] - libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.47.1': resolution: {integrity: sha512-U/CHbqKSwEQyZXjCpY43/GLYcTVKEXeRHw0rMBJP7fP3x6WpYG4LTJWR3ic6TeYKX6ZK7mrhltP4ppolyVhLVQ==} cpu: [s390x] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.47.1': resolution: {integrity: sha512-uTLEakjxOTElfeZIGWkC34u2auLHB1AYS6wBjPGI00bWdxdLcCzK5awjs25YXpqB9lS8S0vbO0t9ZcBeNibA7g==} cpu: [x64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-musl@4.47.1': resolution: {integrity: sha512-Ft+d/9DXs30BK7CHCTX11FtQGHUdpNDLJW0HHLign4lgMgBcPFN3NkdIXhC5r9iwsMwYreBBc4Rho5ieOmKNVQ==} cpu: [x64] os: [linux] - libc: [musl] '@rollup/rollup-win32-arm64-msvc@4.47.1': resolution: {integrity: sha512-N9X5WqGYzZnjGAFsKSfYFtAShYjwOmFJoWbLg3dYixZOZqU7hdMq+/xyS14zKLhFhZDhP9VfkzQnsdk0ZDS9IA==} @@ -2577,21 +2562,21 @@ snapshots: '@vercel/oidc@3.1.0': {} - '@vitest/coverage-v8@2.1.9(supports-color@10.2.0)(vitest@2.1.9(@types/node@22.17.2)(msw@2.10.5(@types/node@22.17.2)(typescript@5.9.2))(supports-color@10.2.0))': + '@vitest/coverage-v8@2.1.9(vitest@2.1.9(@types/node@22.17.2)(msw@2.10.5(@types/node@22.17.2)(typescript@5.9.2)))': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 0.2.3 debug: 4.4.1(supports-color@10.2.0) istanbul-lib-coverage: 3.2.2 istanbul-lib-report: 3.0.1 - istanbul-lib-source-maps: 5.0.6(supports-color@10.2.0) + istanbul-lib-source-maps: 5.0.6 istanbul-reports: 3.2.0 magic-string: 0.30.17 magicast: 0.3.5 std-env: 3.9.0 test-exclude: 7.0.1 tinyrainbow: 1.2.0 - vitest: 2.1.9(@types/node@22.17.2)(msw@2.10.5(@types/node@22.17.2)(typescript@5.9.2))(supports-color@10.2.0) + vitest: 2.1.9(@types/node@22.17.2)(msw@2.10.5(@types/node@22.17.2)(typescript@5.9.2)) transitivePeerDependencies: - supports-color @@ -2932,7 +2917,7 @@ snapshots: make-dir: 4.0.0 supports-color: 7.2.0 - istanbul-lib-source-maps@5.0.6(supports-color@10.2.0): + istanbul-lib-source-maps@5.0.6: dependencies: '@jridgewell/trace-mapping': 0.3.30 debug: 4.4.1(supports-color@10.2.0) @@ -3283,7 +3268,7 @@ snapshots: pirates: 4.0.7 ts-interface-checker: 0.1.13 - supabase@2.34.3(supports-color@10.2.0): + supabase@2.34.3: dependencies: bin-links: 5.0.0 https-proxy-agent: 7.0.6(supports-color@10.2.0) @@ -3353,7 +3338,7 @@ snapshots: ts-interface-checker@0.1.13: {} - tsup@8.5.0(postcss@8.5.6)(supports-color@10.2.0)(tsx@4.20.4)(typescript@5.9.2): + tsup@8.5.0(postcss@8.5.6)(tsx@4.20.4)(typescript@5.9.2): dependencies: bundle-require: 5.1.0(esbuild@0.25.9) cac: 6.7.14 @@ -3419,7 +3404,7 @@ snapshots: validate-npm-package-name@5.0.1: {} - vite-node@2.1.9(@types/node@22.17.2)(supports-color@10.2.0): + vite-node@2.1.9(@types/node@22.17.2): dependencies: cac: 6.7.14 debug: 4.4.1(supports-color@10.2.0) @@ -3446,7 +3431,7 @@ snapshots: '@types/node': 22.17.2 fsevents: 2.3.3 - vitest@2.1.9(@types/node@22.17.2)(msw@2.10.5(@types/node@22.17.2)(typescript@5.9.2))(supports-color@10.2.0): + vitest@2.1.9(@types/node@22.17.2)(msw@2.10.5(@types/node@22.17.2)(typescript@5.9.2)): dependencies: '@vitest/expect': 2.1.9 '@vitest/mocker': 2.1.9(msw@2.10.5(@types/node@22.17.2)(typescript@5.9.2))(vite@5.4.19(@types/node@22.17.2)) @@ -3466,7 +3451,7 @@ snapshots: tinypool: 1.1.1 tinyrainbow: 1.2.0 vite: 5.4.19(@types/node@22.17.2) - vite-node: 2.1.9(@types/node@22.17.2)(supports-color@10.2.0) + vite-node: 2.1.9(@types/node@22.17.2) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 22.17.2