diff --git a/packages/app/control/package.json b/packages/app/control/package.json index b0b74e4b5..f26bbe2b6 100644 --- a/packages/app/control/package.json +++ b/packages/app/control/package.json @@ -36,6 +36,7 @@ "dependencies": { "@auth/core": "^0.40.0", "@auth/prisma-adapter": "^2.10.0", + "@coinbase/x402": "^0.6.4", "@hookform/resolvers": "^5.2.1", "@icons-pack/react-simple-icons": "^13.7.0", "@merit-systems/sdk": "0.0.8", @@ -64,6 +65,7 @@ "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-switch": "^1.2.5", "@radix-ui/react-tabs": "^1.1.13", + "@radix-ui/react-tooltip": "^1.2.8", "@react-email/render": "^1.2.3", "@shikijs/core": "^3.12.2", "@shikijs/engine-javascript": "^3.12.2", @@ -87,7 +89,6 @@ "autonumeric": "^4.10.8", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", - "@coinbase/x402": "^0.6.4", "cors": "^2.8.5", "date-fns": "^4.1.0", "dotenv": "^16.4.5", @@ -136,9 +137,9 @@ "devDependencies": { "@eslint/eslintrc": "^3", "@faker-js/faker": "^9.9.0", + "@next/eslint-plugin-next": "^15.5.3", "@types/cors": "^2.8.17", "@types/express": "^5.0.0", - "@next/eslint-plugin-next": "^15.5.3", "@types/node": "^20", "@types/react": "19.1.10", "@types/react-dom": "19.1.7", diff --git a/packages/app/control/src/app/(app)/_components/keys/table/table.tsx b/packages/app/control/src/app/(app)/_components/keys/table/table.tsx index ab117ad68..bac75a515 100644 --- a/packages/app/control/src/app/(app)/_components/keys/table/table.tsx +++ b/packages/app/control/src/app/(app)/_components/keys/table/table.tsx @@ -1,6 +1,6 @@ 'use client'; -import { Loader2 } from 'lucide-react'; +import { Loader2, Trash2 } from 'lucide-react'; import { format } from 'date-fns'; @@ -14,11 +14,25 @@ import { TableCell, TableEmpty, } from '@/components/ui/table'; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from '@/components/ui/alert-dialog'; import { KeyStatus, LoadingKeyStatus } from './status'; import { Skeleton } from '@/components/ui/skeleton'; import { UserAvatar } from '@/components/utils/user-avatar'; +import { api } from '@/trpc/client'; +import { useState } from 'react'; +import { toast } from 'sonner'; interface Key { id: string; @@ -50,7 +64,7 @@ export const KeysTable: React.FC = ({ keys, pagination }) => { {keys.length > 0 ? ( ) : ( - No keys found + No keys found )} ); @@ -70,6 +84,23 @@ const KeyRows = ({ keys }: { keys: Key[] }) => { }; const KeyRow = ({ apiKey }: { apiKey: Key }) => { + const [isOpen, setIsOpen] = useState(false); + const utils = api.useUtils(); + const deleteApiKey = api.user.apiKeys.delete.useMutation({ + onSuccess: () => { + toast.success('API key revoked successfully'); + void utils.user.apiKeys.list.invalidate(); + }, + onError: error => { + toast.error(error.message || 'Failed to revoke API key'); + }, + }); + + const handleRevoke = () => { + deleteApiKey.mutate(apiKey.id); + setIsOpen(false); + }; + return ( {apiKey.name} @@ -89,6 +120,48 @@ const KeyRow = ({ apiKey }: { apiKey: Key }) => { + + + + + + + + Revoke API Key + + Are you sure you want to revoke this API key? This action cannot + be undone. Any applications using this key will no longer be + able to authenticate. + + + + Cancel + + {deleteApiKey.isPending ? ( + <> + + Revoking... + + ) : ( + 'Revoke' + )} + + + + + ); }; @@ -111,6 +184,9 @@ const LoadingKeyRow = () => { + + + ); }; @@ -131,6 +207,7 @@ const BaseKeysTable = ({ children, pagination }: BaseKeysTableProps) => { Last Used Created At Status + {children} diff --git a/packages/app/control/src/app/(auth)/(oauth)/(authorize)/oauth/authorize/_components/existing-user/scopes.tsx b/packages/app/control/src/app/(auth)/(oauth)/(authorize)/oauth/authorize/_components/existing-user/scopes.tsx index 7f612fc19..239f664ab 100644 --- a/packages/app/control/src/app/(auth)/(oauth)/(authorize)/oauth/authorize/_components/existing-user/scopes.tsx +++ b/packages/app/control/src/app/(auth)/(oauth)/(authorize)/oauth/authorize/_components/existing-user/scopes.tsx @@ -1,4 +1,9 @@ -import { BrainCircuit, User } from 'lucide-react'; +import { BrainCircuit, Info, Key, User } from 'lucide-react'; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from '@/components/ui/tooltip'; interface Props { scopes: string[]; @@ -20,11 +25,24 @@ const Scope = ({ scope }: { scope: string }) => { if (!data) { return null; } - return ( -
  • +
  • {data.name} + {data.description && ( + + + + + + + +

    {data.description}

    +
    +
    + )}
  • ); }; @@ -33,9 +51,22 @@ const scopeData = { 'llm:invoke': { name: 'Make AI requests', icon: BrainCircuit, + level: 'info', + description: + 'You are allowing this app to make AI requests on your behalf.', }, offline_access: { name: 'Connect your user profile', icon: User, + level: 'info', + description: + 'You are allowing this app to connect your user profile to your account.', + }, + 'api_key:create': { + name: 'Create API keys', + icon: Key, + level: 'warn', + description: + 'You are allowing this app to create a long lived access token, which can be revoked at any time in your Echo dashboard.', }, }; diff --git a/packages/app/control/src/app/(auth)/(oauth)/api/oauth/token/_lib/issue.ts b/packages/app/control/src/app/(auth)/(oauth)/api/oauth/token/_lib/issue.ts index 4e83d9e94..d0d673372 100644 --- a/packages/app/control/src/app/(auth)/(oauth)/api/oauth/token/_lib/issue.ts +++ b/packages/app/control/src/app/(auth)/(oauth)/api/oauth/token/_lib/issue.ts @@ -25,6 +25,7 @@ import { getApp } from '@/services/db/apps/get'; import { createAppMembership } from '@/services/db/apps/membership'; import { issueOAuthToken } from '@/services/db/auth/oauth-token'; +import { createApiKey } from '@/services/db/api-keys'; import type { TokenMetadata } from '@/types/token-metadata'; @@ -178,6 +179,10 @@ export async function handleIssueToken( } } + /* 🔟 Check if api_key:create scope is present */ + const scopes = scope.split(' '); + const shouldCreateApiKey = scopes.includes('api_key:create'); + const { session, refreshToken } = await issueOAuthToken({ userId: user.id, appId: app.id, @@ -185,6 +190,47 @@ export async function handleIssueToken( metadata, }); + if (shouldCreateApiKey) { + logger.emit({ + severityText: 'INFO', + body: 'Creating API key for user with api_key:create scope', + attributes: { + userId: user.id, + echoAppId: app.id, + function: 'handleInitialTokenIssuance', + }, + }); + + /* Generate an API key instead of a temporary JWT token */ + const apiKey = await createApiKey(user.id, { + echoAppId: app.id, + name: 'OAuth Generated API Key', + }); + + logger.emit({ + severityText: 'INFO', + body: 'API key generated for OAuth flow', + attributes: { + userId: user.id, + echoAppId: app.id, + apiKeyId: apiKey.id, + function: 'handleInitialTokenIssuance', + }, + }); + + /* Return API key as access token with a very long expiration (100 years) */ + return tokenResponse({ + accessToken: { + access_token: apiKey.key, + scope, + access_token_expiry: new Date( + Date.now() + 100 * 365 * 24 * 60 * 60 * 1000 + ), // 100 years + }, + refreshToken, + }); + } + const accessToken = await createEchoAccessJwt({ user_id: user.id, app_id: app.id, diff --git a/packages/app/control/src/components/ui/alert-dialog.tsx b/packages/app/control/src/components/ui/alert-dialog.tsx index e2e20e992..f4a651e13 100644 --- a/packages/app/control/src/components/ui/alert-dialog.tsx +++ b/packages/app/control/src/components/ui/alert-dialog.tsx @@ -12,13 +12,19 @@ function AlertDialog({ return ; } -// function AlertDialogTrigger({ -// ...props -// }: React.ComponentProps) { -// return ( -// -// ); -// } +const AlertDialogTrigger = React.forwardRef< + React.ComponentRef, + React.ComponentPropsWithoutRef +>(({ ...props }, ref) => { + return ( + + ); +}); +AlertDialogTrigger.displayName = 'AlertDialogTrigger'; function AlertDialogPortal({ ...props @@ -130,24 +136,28 @@ function AlertDialogAction({ ); } -// function AlertDialogCancel({ -// className, -// ...props -// }: React.ComponentProps) { -// return ( -// -// ); -// } +const AlertDialogCancel = React.forwardRef< + React.ComponentRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + return ( + + ); +}); +AlertDialogCancel.displayName = 'AlertDialogCancel'; export { AlertDialog, + AlertDialogTrigger, AlertDialogContent, AlertDialogHeader, AlertDialogFooter, AlertDialogTitle, AlertDialogDescription, AlertDialogAction, + AlertDialogCancel, }; diff --git a/packages/app/control/src/components/ui/tooltip.tsx b/packages/app/control/src/components/ui/tooltip.tsx new file mode 100644 index 000000000..b86605bb4 --- /dev/null +++ b/packages/app/control/src/components/ui/tooltip.tsx @@ -0,0 +1,61 @@ +'use client'; + +import * as React from 'react'; +import * as TooltipPrimitive from '@radix-ui/react-tooltip'; + +import { cn } from '@/lib/utils'; + +function TooltipProvider({ + delayDuration = 0, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function Tooltip({ + ...props +}: React.ComponentProps) { + return ( + + + + ); +} + +function TooltipTrigger({ + ...props +}: React.ComponentProps) { + return ; +} + +function TooltipContent({ + className, + sideOffset = 0, + children, + ...props +}: React.ComponentProps) { + return ( + + + {children} + + + + ); +} + +export { Tooltip, TooltipTrigger, TooltipContent }; diff --git a/packages/tests/integration/tests/oauth-protocol/oauth-api-key-scope.test.ts b/packages/tests/integration/tests/oauth-protocol/oauth-api-key-scope.test.ts new file mode 100644 index 000000000..0e02439fd --- /dev/null +++ b/packages/tests/integration/tests/oauth-protocol/oauth-api-key-scope.test.ts @@ -0,0 +1,177 @@ +import { describe, test, expect, beforeAll } from 'vitest'; +import { + TEST_CLIENT_IDS, + TEST_USER_IDS, + generateCodeVerifier, + generateCodeChallenge, + generateState, + echoControlApi, + TEST_CONFIG, +} from '../../utils/index.js'; + +describe('OAuth API Key Creation with api_key:create Scope', () => { + beforeAll(async () => { + // Verify test environment + expect(TEST_CONFIG.services.echoControl).toBeTruthy(); + expect(process.env.INTEGRATION_TEST_JWT).toBeTruthy(); + }); + + describe('Token Exchange with api_key:create scope', () => { + test('should return an API key as access token when api_key:create scope is requested', async () => { + const codeVerifier = generateCodeVerifier(); + const codeChallenge = generateCodeChallenge(codeVerifier); + const state = generateState(); + + console.log('Getting authorization code with api_key:create scope...'); + + const redirectUrl = await echoControlApi.validateOAuthAuthorizeRequest({ + client_id: TEST_CLIENT_IDS.primary, + redirect_uri: 'http://localhost:3000/callback', + state, + code_challenge: codeChallenge, + code_challenge_method: 'S256', + scope: 'llm:invoke offline_access api_key:create', + prompt: 'none', // Skip consent page for automated testing + }); + + // Extract authorization code from callback URL + const callbackUrl = new URL(redirectUrl); + const authCode = callbackUrl.searchParams.get('code'); + const returnedState = callbackUrl.searchParams.get('state'); + + expect(authCode).toBeTruthy(); + expect(returnedState).toBe(state); + + console.log('✅ Got authorization code with api_key:create scope'); + + // Exchange authorization code for tokens + console.log('Exchanging auth code for API key...'); + + const tokenResponse = await echoControlApi.exchangeCodeForToken({ + code: authCode!, + client_id: TEST_CLIENT_IDS.primary, + redirect_uri: 'http://localhost:3000/callback', + code_verifier: codeVerifier, + }); + + console.log('Token response:', { + access_token_preview: + tokenResponse.access_token.substring(0, 50) + '...', + token_type: tokenResponse.token_type, + expires_in: tokenResponse.expires_in, + scope: tokenResponse.scope, + has_refresh_token: !!tokenResponse.refresh_token, + }); + + // Verify token response structure + expect(tokenResponse.access_token).toBeTruthy(); + expect(tokenResponse.token_type).toBe('Bearer'); + expect(tokenResponse.scope).toContain('api_key:create'); + expect(tokenResponse.refresh_token).toBeTruthy(); + + // Verify the access token is an API key (not a JWT) + // API keys should NOT have JWT structure (3 parts separated by dots) + const accessTokenParts = tokenResponse.access_token.split('.'); + expect(accessTokenParts.length).not.toBe(3); // Not a JWT + + // API keys should start with the API key prefix (typically 'echo_') + expect(tokenResponse.access_token).toMatch(/^echo_/); + + // Verify the expires_in is very long (100 years = ~3.15B seconds) + // We'll check it's greater than 10 years (315M seconds) + expect(tokenResponse.expires_in).toBeGreaterThan(315_000_000); + + console.log('✅ Received API key as access token'); + console.log(`API key length: ${tokenResponse.access_token.length}`); + console.log( + `Expires in: ${tokenResponse.expires_in} seconds (~${Math.floor(tokenResponse.expires_in / 31536000)} years)` + ); + + // Verify the API key can be used to authenticate + console.log('Testing API key authentication...'); + const balance = await echoControlApi.getBalance( + tokenResponse.access_token + ); + + expect(balance).toBeDefined(); + expect(typeof balance.balance).toBe('number'); + + console.log('✅ API key successfully used for authentication'); + }); + + test('should return normal JWT when api_key:create scope is NOT requested', async () => { + const codeVerifier = generateCodeVerifier(); + const codeChallenge = generateCodeChallenge(codeVerifier); + const state = generateState(); + + console.log('Getting authorization code WITHOUT api_key:create scope...'); + + const redirectUrl = await echoControlApi.validateOAuthAuthorizeRequest({ + client_id: TEST_CLIENT_IDS.primary, + redirect_uri: 'http://localhost:3000/callback', + state, + code_challenge: codeChallenge, + code_challenge_method: 'S256', + scope: 'llm:invoke offline_access', // No api_key:create + prompt: 'none', + }); + + const callbackUrl = new URL(redirectUrl); + const authCode = callbackUrl.searchParams.get('code'); + + expect(authCode).toBeTruthy(); + + // Exchange authorization code for tokens + const tokenResponse = await echoControlApi.exchangeCodeForToken({ + code: authCode!, + client_id: TEST_CLIENT_IDS.primary, + redirect_uri: 'http://localhost:3000/callback', + code_verifier: codeVerifier, + }); + + // Verify the access token IS a JWT (has 3 parts) + const accessTokenParts = tokenResponse.access_token.split('.'); + expect(accessTokenParts.length).toBe(3); // Is a JWT + + // JWT expiration should be short (not 100 years) + expect(tokenResponse.expires_in).toBeLessThan(31_536_000); // Less than 1 year + + console.log('✅ Received normal JWT token without api_key:create scope'); + }); + + test('should handle mixed scopes correctly (llm:invoke + api_key:create)', async () => { + const codeVerifier = generateCodeVerifier(); + const codeChallenge = generateCodeChallenge(codeVerifier); + const state = generateState(); + + const redirectUrl = await echoControlApi.validateOAuthAuthorizeRequest({ + client_id: TEST_CLIENT_IDS.primary, + redirect_uri: 'http://localhost:3000/callback', + state, + code_challenge: codeChallenge, + code_challenge_method: 'S256', + scope: 'llm:invoke api_key:create', // Mixed scopes + prompt: 'none', + }); + + const callbackUrl = new URL(redirectUrl); + const authCode = callbackUrl.searchParams.get('code'); + + expect(authCode).toBeTruthy(); + + const tokenResponse = await echoControlApi.exchangeCodeForToken({ + code: authCode!, + client_id: TEST_CLIENT_IDS.primary, + redirect_uri: 'http://localhost:3000/callback', + code_verifier: codeVerifier, + }); + + // Should return API key because api_key:create is present + expect(tokenResponse.access_token).toMatch(/^echo_/); + expect(tokenResponse.scope).toContain('llm:invoke'); + expect(tokenResponse.scope).toContain('api_key:create'); + + console.log('✅ Mixed scopes handled correctly'); + }); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9a8f1f70f..72846af95 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -152,6 +152,9 @@ importers: '@radix-ui/react-tabs': specifier: ^1.1.13 version: 1.1.13(@types/react-dom@19.1.7(@types/react@19.1.10))(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-tooltip': + specifier: ^1.2.8 + version: 1.2.8(@types/react-dom@19.1.7(@types/react@19.1.10))(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) '@react-email/render': specifier: ^1.2.3 version: 1.2.3(react-dom@19.1.1(react@19.1.1))(react@19.1.1) @@ -15788,6 +15791,26 @@ snapshots: '@types/react': 19.1.10 '@types/react-dom': 19.1.7(@types/react@19.1.10) + '@radix-ui/react-tooltip@1.2.8(@types/react-dom@19.1.7(@types/react@19.1.10))(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.10)(react@19.1.1) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.10)(react@19.1.1) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.1.7(@types/react@19.1.10))(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.10)(react@19.1.1) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.1.7(@types/react@19.1.10))(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.1.7(@types/react@19.1.10))(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.1.7(@types/react@19.1.10))(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.7(@types/react@19.1.10))(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@radix-ui/react-slot': 1.2.3(@types/react@19.1.10)(react@19.1.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.10)(react@19.1.1) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.1.7(@types/react@19.1.10))(@types/react@19.1.10)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + react: 19.1.1 + react-dom: 19.1.1(react@19.1.1) + optionalDependencies: + '@types/react': 19.1.10 + '@types/react-dom': 19.1.7(@types/react@19.1.10) + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.1.10)(react@19.1.0)': dependencies: react: 19.1.0 @@ -19121,14 +19144,14 @@ snapshots: chai: 5.2.0 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.3(msw@2.11.2(@types/node@20.19.16)(typescript@5.9.2))(vite@6.3.5(@types/node@20.19.16)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.20.5)(yaml@2.8.0))': + '@vitest/mocker@3.2.3(msw@2.11.2(@types/node@20.19.16)(typescript@5.9.2))(vite@6.3.5(@types/node@24.3.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.20.5)(yaml@2.8.0))': dependencies: '@vitest/spy': 3.2.3 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: msw: 2.11.2(@types/node@20.19.16)(typescript@5.9.2) - vite: 6.3.5(@types/node@20.19.16)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.20.5)(yaml@2.8.0) + vite: 6.3.5(@types/node@24.3.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.20.5)(yaml@2.8.0) '@vitest/mocker@3.2.3(msw@2.11.2(@types/node@24.3.1)(typescript@5.9.2))(vite@6.3.5(@types/node@24.3.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.20.5)(yaml@2.8.0))': dependencies: @@ -22003,7 +22026,7 @@ snapshots: '@typescript-eslint/parser': 8.34.1(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2) eslint: 9.35.0(jiti@2.5.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.34.1(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.35.0(jiti@2.5.1)))(eslint@9.35.0(jiti@2.5.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0)(eslint@9.35.0(jiti@2.5.1)) eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.34.1(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.35.0(jiti@2.5.1)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.35.0(jiti@2.5.1)) eslint-plugin-react: 7.37.5(eslint@9.35.0(jiti@2.5.1)) @@ -22023,7 +22046,7 @@ snapshots: '@typescript-eslint/parser': 8.34.1(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2) eslint: 9.35.0(jiti@2.5.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.34.1(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.35.0(jiti@2.5.1)))(eslint@9.35.0(jiti@2.5.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0)(eslint@9.35.0(jiti@2.5.1)) eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.34.1(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.35.0(jiti@2.5.1)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.35.0(jiti@2.5.1)) eslint-plugin-react: 7.37.5(eslint@9.35.0(jiti@2.5.1)) @@ -22047,7 +22070,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.34.1(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.35.0(jiti@2.5.1)))(eslint@9.35.0(jiti@2.5.1)): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0)(eslint@9.35.0(jiti@2.5.1)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.1 @@ -22062,14 +22085,14 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.34.1(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.34.1(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.35.0(jiti@2.5.1)))(eslint@9.35.0(jiti@2.5.1)))(eslint@9.35.0(jiti@2.5.1)): + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.34.1(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0)(eslint@9.35.0(jiti@2.5.1)))(eslint@9.35.0(jiti@2.5.1)): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.34.1(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2) eslint: 9.35.0(jiti@2.5.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.34.1(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.35.0(jiti@2.5.1)))(eslint@9.35.0(jiti@2.5.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0)(eslint@9.35.0(jiti@2.5.1)) transitivePeerDependencies: - supports-color @@ -22084,7 +22107,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.35.0(jiti@2.5.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.34.1(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.34.1(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint@9.35.0(jiti@2.5.1)))(eslint@9.35.0(jiti@2.5.1)))(eslint@9.35.0(jiti@2.5.1)) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.34.1(eslint@9.35.0(jiti@2.5.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0)(eslint@9.35.0(jiti@2.5.1)))(eslint@9.35.0(jiti@2.5.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -27852,7 +27875,7 @@ snapshots: dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.3 - '@vitest/mocker': 3.2.3(msw@2.11.2(@types/node@20.19.16)(typescript@5.9.2))(vite@6.3.5(@types/node@20.19.16)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.20.5)(yaml@2.8.0)) + '@vitest/mocker': 3.2.3(msw@2.11.2(@types/node@20.19.16)(typescript@5.9.2))(vite@6.3.5(@types/node@24.3.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.20.5)(yaml@2.8.0)) '@vitest/pretty-format': 3.2.3 '@vitest/runner': 3.2.3 '@vitest/snapshot': 3.2.3