improvement(oauth): refactor oauth apis to dedicated tools routes, fix docs (#423)

* migrate oauth apis to dedicated tools routes

* added tests

* fix docs

* fix confluence file selector
This commit is contained in:
Waleed Latif
2025-05-26 12:00:03 -07:00
committed by GitHub
parent 8e6057a39e
commit 6afb453fc0
37 changed files with 1238 additions and 152 deletions

View File

@@ -1,87 +0,0 @@
---
title: File
description: Read and parse multiple files
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="dropdown"
color="#40916C"
icon={true}
iconSvg={`<svg className="block-icon"
viewBox='0 0 23 28'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<path
d='M8 15.2H15.2M8 20H11.6M2 4.4V23.6C2 24.2365 2.25286 24.847 2.70294 25.2971C3.15303 25.7471 3.76348 26 4.4 26H18.8C19.4365 26 20.047 25.7471 20.4971 25.2971C20.9471 24.847 21.2 24.2365 21.2 23.6V9.6104C21.2 9.29067 21.136 8.97417 21.012 8.67949C20.8879 8.38481 20.7062 8.11789 20.4776 7.8944L15.1496 2.684C14.7012 2.24559 14.0991 2.00008 13.472 2H4.4C3.76348 2 3.15303 2.25286 2.70294 2.70294C2.25286 3.15303 2 3.76348 2 4.4Z'
stroke='currentColor'
strokeWidth='2.25'
strokeLinecap='round'
strokeLinejoin='round'
/>
<path
d='M14 2V6.8C14 7.43652 14.2529 8.04697 14.7029 8.49706C15.153 8.94714 15.7635 9.2 16.4 9.2H21.2'
stroke='currentColor'
strokeWidth='2.25'
strokeLinejoin='round'
/>
</svg>`}
/>
## Usage Instructions
Upload and extract contents from structured file formats including PDFs, CSV spreadsheets, and Word documents (DOCX). ${
shouldEnableURLInput
? 'You can either provide a URL to a file or upload files directly. '
: 'Upload files directly. '
}Specialized parsers extract text and metadata from each format. You can upload multiple files at once and access them individually or as a combined document.
## Tools
### `file_parser`
Parse one or more uploaded files or files from URLs (text, PDF, CSV, images, etc.)
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `filePath` | string | Yes | Path to the file\(s\). Can be a single path, URL, or an array of paths. |
| `fileType` | string | No | Type of file to parse \(auto-detected if not specified\) |
#### Output
This tool does not produce any outputs.
## Block Configuration
### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `inputMethod` | string | No | |
### Outputs
| Output | Type | Description |
| ------ | ---- | ----------- |
| `response` | object | Output from response |
| ↳ `files` | json | files of the response |
| ↳ `combinedContent` | string | combinedContent of the response |
## Notes
- Category: `tools`
- Type: `dropdown`

View File

@@ -7,7 +7,6 @@
"clay",
"confluence",
"discord",
"dropdown",
"elevenlabs",
"exa",
"file",

View File

@@ -0,0 +1,149 @@
/**
* Tests for forget password API route
*
* @vitest-environment node
*/
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { createMockRequest } from '@/app/api/__test-utils__/utils'
describe('Forget Password API Route', () => {
const mockAuth = {
api: {
forgetPassword: vi.fn(),
},
}
const mockLogger = {
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
debug: vi.fn(),
}
beforeEach(() => {
vi.resetModules()
vi.doMock('@/lib/auth', () => ({
auth: mockAuth,
}))
vi.doMock('@/lib/logs/console-logger', () => ({
createLogger: vi.fn().mockReturnValue(mockLogger),
}))
})
afterEach(() => {
vi.clearAllMocks()
})
it('should send password reset email successfully', async () => {
mockAuth.api.forgetPassword.mockResolvedValueOnce(undefined)
const req = createMockRequest('POST', {
email: 'test@example.com',
redirectTo: 'https://example.com/reset',
})
const { POST } = await import('./route')
const response = await POST(req)
const data = await response.json()
expect(response.status).toBe(200)
expect(data.success).toBe(true)
expect(mockAuth.api.forgetPassword).toHaveBeenCalledWith({
body: {
email: 'test@example.com',
redirectTo: 'https://example.com/reset',
},
method: 'POST',
})
})
it('should send password reset email without redirectTo', async () => {
mockAuth.api.forgetPassword.mockResolvedValueOnce(undefined)
const req = createMockRequest('POST', {
email: 'test@example.com',
})
const { POST } = await import('./route')
const response = await POST(req)
const data = await response.json()
expect(response.status).toBe(200)
expect(data.success).toBe(true)
expect(mockAuth.api.forgetPassword).toHaveBeenCalledWith({
body: {
email: 'test@example.com',
redirectTo: undefined,
},
method: 'POST',
})
})
it('should handle missing email', async () => {
const req = createMockRequest('POST', {})
const { POST } = await import('./route')
const response = await POST(req)
const data = await response.json()
expect(response.status).toBe(400)
expect(data.message).toBe('Email is required')
expect(mockAuth.api.forgetPassword).not.toHaveBeenCalled()
})
it('should handle empty email', async () => {
const req = createMockRequest('POST', {
email: '',
})
const { POST } = await import('./route')
const response = await POST(req)
const data = await response.json()
expect(response.status).toBe(400)
expect(data.message).toBe('Email is required')
expect(mockAuth.api.forgetPassword).not.toHaveBeenCalled()
})
it('should handle auth service error with message', async () => {
const errorMessage = 'User not found'
mockAuth.api.forgetPassword.mockRejectedValueOnce(new Error(errorMessage))
const req = createMockRequest('POST', {
email: 'nonexistent@example.com',
})
const { POST } = await import('./route')
const response = await POST(req)
const data = await response.json()
expect(response.status).toBe(500)
expect(data.message).toBe(errorMessage)
expect(mockLogger.error).toHaveBeenCalledWith('Error requesting password reset:', {
error: expect.any(Error),
})
})
it('should handle unknown error', async () => {
mockAuth.api.forgetPassword.mockRejectedValueOnce('Unknown error')
const req = createMockRequest('POST', {
email: 'test@example.com',
})
const { POST } = await import('./route')
const response = await POST(req)
const data = await response.json()
expect(response.status).toBe(500)
expect(data.message).toBe('Failed to send password reset email. Please try again later.')
expect(mockLogger.error).toHaveBeenCalled()
})
})

View File

@@ -0,0 +1,220 @@
/**
* Tests for OAuth connections API route
*
* @vitest-environment node
*/
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { createMockRequest } from '@/app/api/__test-utils__/utils'
describe('OAuth Connections API Route', () => {
const mockGetSession = vi.fn()
const mockDb = {
select: vi.fn().mockReturnThis(),
from: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
limit: vi.fn(),
}
const mockLogger = {
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
debug: vi.fn(),
}
const mockUUID = 'mock-uuid-12345678-90ab-cdef-1234-567890abcdef'
beforeEach(() => {
vi.resetModules()
vi.stubGlobal('crypto', {
randomUUID: vi.fn().mockReturnValue(mockUUID),
})
vi.doMock('@/lib/auth', () => ({
getSession: mockGetSession,
}))
vi.doMock('@/db', () => ({
db: mockDb,
}))
vi.doMock('@/db/schema', () => ({
account: { userId: 'userId', providerId: 'providerId' },
user: { email: 'email', id: 'id' },
}))
vi.doMock('drizzle-orm', () => ({
eq: vi.fn((field, value) => ({ field, value, type: 'eq' })),
}))
vi.doMock('jwt-decode', () => ({
jwtDecode: vi.fn(),
}))
vi.doMock('@/lib/logs/console-logger', () => ({
createLogger: vi.fn().mockReturnValue(mockLogger),
}))
})
afterEach(() => {
vi.clearAllMocks()
})
it('should return connections successfully', async () => {
mockGetSession.mockResolvedValueOnce({
user: { id: 'user-123' },
})
const mockAccounts = [
{
id: 'account-1',
providerId: 'google-email',
accountId: 'test@example.com',
scope: 'email profile',
updatedAt: new Date('2024-01-01'),
idToken: null,
},
{
id: 'account-2',
providerId: 'github',
accountId: 'testuser',
scope: 'repo',
updatedAt: new Date('2024-01-02'),
idToken: null,
},
]
const mockUserRecord = [{ email: 'user@example.com' }]
mockDb.select.mockReturnValueOnce(mockDb)
mockDb.from.mockReturnValueOnce(mockDb)
mockDb.where.mockResolvedValueOnce(mockAccounts)
mockDb.select.mockReturnValueOnce(mockDb)
mockDb.from.mockReturnValueOnce(mockDb)
mockDb.where.mockReturnValueOnce(mockDb)
mockDb.limit.mockResolvedValueOnce(mockUserRecord)
const req = createMockRequest('GET')
const { GET } = await import('./route')
const response = await GET(req)
const data = await response.json()
expect(response.status).toBe(200)
expect(data.connections).toHaveLength(2)
expect(data.connections[0]).toMatchObject({
provider: 'google-email',
baseProvider: 'google',
featureType: 'email',
isConnected: true,
})
expect(data.connections[1]).toMatchObject({
provider: 'github',
baseProvider: 'github',
featureType: 'default',
isConnected: true,
})
})
it('should handle unauthenticated user', async () => {
mockGetSession.mockResolvedValueOnce(null)
const req = createMockRequest('GET')
const { GET } = await import('./route')
const response = await GET(req)
const data = await response.json()
expect(response.status).toBe(401)
expect(data.error).toBe('User not authenticated')
expect(mockLogger.warn).toHaveBeenCalled()
})
it('should handle user with no connections', async () => {
mockGetSession.mockResolvedValueOnce({
user: { id: 'user-123' },
})
mockDb.select.mockReturnValueOnce(mockDb)
mockDb.from.mockReturnValueOnce(mockDb)
mockDb.where.mockResolvedValueOnce([])
mockDb.select.mockReturnValueOnce(mockDb)
mockDb.from.mockReturnValueOnce(mockDb)
mockDb.where.mockReturnValueOnce(mockDb)
mockDb.limit.mockResolvedValueOnce([])
const req = createMockRequest('GET')
const { GET } = await import('./route')
const response = await GET(req)
const data = await response.json()
expect(response.status).toBe(200)
expect(data.connections).toHaveLength(0)
})
it('should handle database error', async () => {
mockGetSession.mockResolvedValueOnce({
user: { id: 'user-123' },
})
mockDb.select.mockReturnValueOnce(mockDb)
mockDb.from.mockReturnValueOnce(mockDb)
mockDb.where.mockRejectedValueOnce(new Error('Database error'))
const req = createMockRequest('GET')
const { GET } = await import('./route')
const response = await GET(req)
const data = await response.json()
expect(response.status).toBe(500)
expect(data.error).toBe('Internal server error')
expect(mockLogger.error).toHaveBeenCalled()
})
it('should decode ID token for display name', async () => {
const { jwtDecode } = await import('jwt-decode')
const mockJwtDecode = jwtDecode as any
mockGetSession.mockResolvedValueOnce({
user: { id: 'user-123' },
})
const mockAccounts = [
{
id: 'account-1',
providerId: 'google',
accountId: 'google-user-id',
scope: 'email profile',
updatedAt: new Date('2024-01-01'),
idToken: 'mock-jwt-token',
},
]
mockJwtDecode.mockReturnValueOnce({
email: 'decoded@example.com',
name: 'Decoded User',
})
mockDb.select.mockReturnValueOnce(mockDb)
mockDb.from.mockReturnValueOnce(mockDb)
mockDb.where.mockResolvedValueOnce(mockAccounts)
mockDb.select.mockReturnValueOnce(mockDb)
mockDb.from.mockReturnValueOnce(mockDb)
mockDb.where.mockReturnValueOnce(mockDb)
mockDb.limit.mockResolvedValueOnce([])
const req = createMockRequest('GET')
const { GET } = await import('./route')
const response = await GET(req)
const data = await response.json()
expect(response.status).toBe(200)
expect(data.connections[0].accounts[0].name).toBe('decoded@example.com')
})
})

View File

@@ -0,0 +1,256 @@
/**
* Tests for OAuth credentials API route
*
* @vitest-environment node
*/
import { NextRequest } from 'next/server'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
describe('OAuth Credentials API Route', () => {
const mockGetSession = vi.fn()
const mockParseProvider = vi.fn()
const mockDb = {
select: vi.fn().mockReturnThis(),
from: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
limit: vi.fn(),
}
const mockLogger = {
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
debug: vi.fn(),
}
const mockUUID = 'mock-uuid-12345678-90ab-cdef-1234-567890abcdef'
function createMockRequestWithQuery(method = 'GET', queryParams = ''): NextRequest {
const url = `http://localhost:3000/api/auth/oauth/credentials${queryParams}`
return new NextRequest(new URL(url), { method })
}
beforeEach(() => {
vi.resetModules()
vi.stubGlobal('crypto', {
randomUUID: vi.fn().mockReturnValue(mockUUID),
})
vi.doMock('@/lib/auth', () => ({
getSession: mockGetSession,
}))
vi.doMock('@/lib/oauth', () => ({
parseProvider: mockParseProvider,
}))
vi.doMock('@/db', () => ({
db: mockDb,
}))
vi.doMock('@/db/schema', () => ({
account: { userId: 'userId', providerId: 'providerId' },
user: { email: 'email', id: 'id' },
}))
vi.doMock('drizzle-orm', () => ({
and: vi.fn((...conditions) => ({ conditions, type: 'and' })),
eq: vi.fn((field, value) => ({ field, value, type: 'eq' })),
}))
vi.doMock('jwt-decode', () => ({
jwtDecode: vi.fn(),
}))
vi.doMock('@/lib/logs/console-logger', () => ({
createLogger: vi.fn().mockReturnValue(mockLogger),
}))
})
afterEach(() => {
vi.clearAllMocks()
})
it('should return credentials successfully', async () => {
mockGetSession.mockResolvedValueOnce({
user: { id: 'user-123' },
})
mockParseProvider.mockReturnValueOnce({
baseProvider: 'google',
})
const mockAccounts = [
{
id: 'credential-1',
userId: 'user-123',
providerId: 'google-email',
accountId: 'test@example.com',
updatedAt: new Date('2024-01-01'),
idToken: null,
},
{
id: 'credential-2',
userId: 'user-123',
providerId: 'google-default',
accountId: 'user-id',
updatedAt: new Date('2024-01-02'),
idToken: null,
},
]
mockDb.select.mockReturnValueOnce(mockDb)
mockDb.from.mockReturnValueOnce(mockDb)
mockDb.where.mockResolvedValueOnce(mockAccounts)
mockDb.select.mockReturnValueOnce(mockDb)
mockDb.from.mockReturnValueOnce(mockDb)
mockDb.where.mockReturnValueOnce(mockDb)
mockDb.limit.mockResolvedValueOnce([{ email: 'user@example.com' }])
const req = createMockRequestWithQuery('GET', '?provider=google-email')
const { GET } = await import('./route')
const response = await GET(req)
const data = await response.json()
expect(response.status).toBe(200)
expect(data.credentials).toHaveLength(2)
expect(data.credentials[0]).toMatchObject({
id: 'credential-1',
provider: 'google-email',
isDefault: false,
})
expect(data.credentials[1]).toMatchObject({
id: 'credential-2',
provider: 'google-email',
isDefault: true,
})
})
it('should handle unauthenticated user', async () => {
mockGetSession.mockResolvedValueOnce(null)
const req = createMockRequestWithQuery('GET', '?provider=google')
const { GET } = await import('./route')
const response = await GET(req)
const data = await response.json()
expect(response.status).toBe(401)
expect(data.error).toBe('User not authenticated')
expect(mockLogger.warn).toHaveBeenCalled()
})
it('should handle missing provider parameter', async () => {
mockGetSession.mockResolvedValueOnce({
user: { id: 'user-123' },
})
const req = createMockRequestWithQuery('GET')
const { GET } = await import('./route')
const response = await GET(req)
const data = await response.json()
expect(response.status).toBe(400)
expect(data.error).toBe('Provider is required')
expect(mockLogger.warn).toHaveBeenCalled()
})
it('should handle no credentials found', async () => {
mockGetSession.mockResolvedValueOnce({
user: { id: 'user-123' },
})
mockParseProvider.mockReturnValueOnce({
baseProvider: 'github',
})
mockDb.select.mockReturnValueOnce(mockDb)
mockDb.from.mockReturnValueOnce(mockDb)
mockDb.where.mockResolvedValueOnce([])
const req = createMockRequestWithQuery('GET', '?provider=github')
const { GET } = await import('./route')
const response = await GET(req)
const data = await response.json()
expect(response.status).toBe(200)
expect(data.credentials).toHaveLength(0)
})
it('should decode ID token for display name', async () => {
const { jwtDecode } = await import('jwt-decode')
const mockJwtDecode = jwtDecode as any
mockGetSession.mockResolvedValueOnce({
user: { id: 'user-123' },
})
mockParseProvider.mockReturnValueOnce({
baseProvider: 'google',
})
const mockAccounts = [
{
id: 'credential-1',
userId: 'user-123',
providerId: 'google-default',
accountId: 'google-user-id',
updatedAt: new Date('2024-01-01'),
idToken: 'mock-jwt-token',
},
]
mockJwtDecode.mockReturnValueOnce({
email: 'decoded@example.com',
name: 'Decoded User',
})
mockDb.select.mockReturnValueOnce(mockDb)
mockDb.from.mockReturnValueOnce(mockDb)
mockDb.where.mockResolvedValueOnce(mockAccounts)
const req = createMockRequestWithQuery('GET', '?provider=google')
const { GET } = await import('./route')
const response = await GET(req)
const data = await response.json()
expect(response.status).toBe(200)
expect(data.credentials[0].name).toBe('decoded@example.com')
})
it('should handle database error', async () => {
mockGetSession.mockResolvedValueOnce({
user: { id: 'user-123' },
})
mockParseProvider.mockReturnValueOnce({
baseProvider: 'google',
})
mockDb.select.mockReturnValueOnce(mockDb)
mockDb.from.mockReturnValueOnce(mockDb)
mockDb.where.mockRejectedValueOnce(new Error('Database error'))
const req = createMockRequestWithQuery('GET', '?provider=google')
const { GET } = await import('./route')
const response = await GET(req)
const data = await response.json()
expect(response.status).toBe(500)
expect(data.error).toBe('Internal server error')
expect(mockLogger.error).toHaveBeenCalled()
})
})

View File

@@ -0,0 +1,159 @@
/**
* Tests for OAuth disconnect API route
*
* @vitest-environment node
*/
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { createMockRequest } from '@/app/api/__test-utils__/utils'
describe('OAuth Disconnect API Route', () => {
const mockGetSession = vi.fn()
const mockDb = {
delete: vi.fn().mockReturnThis(),
where: vi.fn(),
}
const mockLogger = {
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
debug: vi.fn(),
}
const mockUUID = 'mock-uuid-12345678-90ab-cdef-1234-567890abcdef'
beforeEach(() => {
vi.resetModules()
vi.stubGlobal('crypto', {
randomUUID: vi.fn().mockReturnValue(mockUUID),
})
vi.doMock('@/lib/auth', () => ({
getSession: mockGetSession,
}))
vi.doMock('@/db', () => ({
db: mockDb,
}))
vi.doMock('@/db/schema', () => ({
account: { userId: 'userId', providerId: 'providerId' },
}))
vi.doMock('drizzle-orm', () => ({
and: vi.fn((...conditions) => ({ conditions, type: 'and' })),
eq: vi.fn((field, value) => ({ field, value, type: 'eq' })),
like: vi.fn((field, value) => ({ field, value, type: 'like' })),
or: vi.fn((...conditions) => ({ conditions, type: 'or' })),
}))
vi.doMock('@/lib/logs/console-logger', () => ({
createLogger: vi.fn().mockReturnValue(mockLogger),
}))
})
afterEach(() => {
vi.clearAllMocks()
})
it('should disconnect provider successfully', async () => {
mockGetSession.mockResolvedValueOnce({
user: { id: 'user-123' },
})
mockDb.delete.mockReturnValueOnce(mockDb)
mockDb.where.mockResolvedValueOnce(undefined)
const req = createMockRequest('POST', {
provider: 'google',
})
const { POST } = await import('./route')
const response = await POST(req)
const data = await response.json()
expect(response.status).toBe(200)
expect(data.success).toBe(true)
expect(mockLogger.info).toHaveBeenCalled()
})
it('should disconnect specific provider ID successfully', async () => {
mockGetSession.mockResolvedValueOnce({
user: { id: 'user-123' },
})
mockDb.delete.mockReturnValueOnce(mockDb)
mockDb.where.mockResolvedValueOnce(undefined)
const req = createMockRequest('POST', {
provider: 'google',
providerId: 'google-email',
})
const { POST } = await import('./route')
const response = await POST(req)
const data = await response.json()
expect(response.status).toBe(200)
expect(data.success).toBe(true)
expect(mockLogger.info).toHaveBeenCalled()
})
it('should handle unauthenticated user', async () => {
mockGetSession.mockResolvedValueOnce(null)
const req = createMockRequest('POST', {
provider: 'google',
})
const { POST } = await import('./route')
const response = await POST(req)
const data = await response.json()
expect(response.status).toBe(401)
expect(data.error).toBe('User not authenticated')
expect(mockLogger.warn).toHaveBeenCalled()
})
it('should handle missing provider', async () => {
mockGetSession.mockResolvedValueOnce({
user: { id: 'user-123' },
})
const req = createMockRequest('POST', {})
const { POST } = await import('./route')
const response = await POST(req)
const data = await response.json()
expect(response.status).toBe(400)
expect(data.error).toBe('Provider is required')
expect(mockLogger.warn).toHaveBeenCalled()
})
it('should handle database error', async () => {
mockGetSession.mockResolvedValueOnce({
user: { id: 'user-123' },
})
mockDb.delete.mockReturnValueOnce(mockDb)
mockDb.where.mockRejectedValueOnce(new Error('Database error'))
const req = createMockRequest('POST', {
provider: 'google',
})
const { POST } = await import('./route')
const response = await POST(req)
const data = await response.json()
expect(response.status).toBe(500)
expect(data.error).toBe('Internal server error')
expect(mockLogger.error).toHaveBeenCalled()
})
})

View File

@@ -0,0 +1,390 @@
/**
* Tests for codegen API route
*
* @vitest-environment node
*/
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { createMockRequest } from '@/app/api/__test-utils__/utils'
describe('Codegen API Route', () => {
const mockOpenAI = {
chat: {
completions: {
create: vi.fn(),
},
},
}
const mockLogger = {
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
debug: vi.fn(),
}
const mockEnv = {
OPENAI_API_KEY: 'test-api-key',
}
const mockUUID = 'mock-uuid-12345678-90ab-cdef-1234-567890abcdef'
beforeEach(() => {
vi.resetModules()
mockEnv.OPENAI_API_KEY = 'test-api-key'
vi.stubGlobal('crypto', {
randomUUID: vi.fn().mockReturnValue(mockUUID),
})
const MockAPIError = class extends Error {
status: number
constructor(message: string, status?: number) {
super(message)
this.status = status || 500
}
}
vi.doMock('openai', () => ({
default: vi.fn().mockImplementation(() => mockOpenAI),
APIError: MockAPIError,
}))
vi.doMock('@/lib/env', () => ({
env: mockEnv,
}))
vi.doMock('@/lib/logs/console-logger', () => ({
createLogger: vi.fn().mockReturnValue(mockLogger),
}))
vi.doMock('next/cache', () => ({
unstable_noStore: vi.fn(),
}))
})
afterEach(() => {
vi.clearAllMocks()
})
it('should generate JSON schema successfully', async () => {
const mockResponse = {
choices: [
{
message: {
content: JSON.stringify({
name: 'test_function',
description: 'A test function',
strict: true,
schema: {
type: 'object',
properties: {
input: { type: 'string', description: 'Test input' },
},
additionalProperties: false,
required: ['input'],
},
}),
},
},
],
}
mockOpenAI.chat.completions.create.mockResolvedValueOnce(mockResponse)
const req = createMockRequest('POST', {
prompt: 'Create a function that takes a string input',
generationType: 'json-schema',
})
const { POST } = await import('./route')
const response = await POST(req)
const data = await response.json()
expect(response.status).toBe(200)
expect(data.success).toBe(true)
expect(data.generatedContent).toBeDefined()
expect(() => JSON.parse(data.generatedContent)).not.toThrow()
expect(mockOpenAI.chat.completions.create).toHaveBeenCalledWith({
model: 'gpt-4o',
messages: expect.arrayContaining([
expect.objectContaining({ role: 'system' }),
expect.objectContaining({
role: 'user',
content: 'Create a function that takes a string input',
}),
]),
temperature: 0.2,
max_tokens: 1500,
response_format: { type: 'json_object' },
})
})
it('should generate JavaScript function body successfully', async () => {
const mockResponse = {
choices: [
{
message: {
content: 'const input = <input>;\nreturn input.toUpperCase();',
},
},
],
}
mockOpenAI.chat.completions.create.mockResolvedValueOnce(mockResponse)
const req = createMockRequest('POST', {
prompt: 'Convert input to uppercase',
generationType: 'javascript-function-body',
})
const { POST } = await import('./route')
const response = await POST(req)
const data = await response.json()
expect(response.status).toBe(200)
expect(data.success).toBe(true)
expect(data.generatedContent).toBe('const input = <input>;\nreturn input.toUpperCase();')
expect(mockOpenAI.chat.completions.create).toHaveBeenCalledWith({
model: 'gpt-4o',
messages: expect.arrayContaining([
expect.objectContaining({ role: 'system' }),
expect.objectContaining({ role: 'user' }),
]),
temperature: 0.2,
max_tokens: 1500,
response_format: undefined,
})
})
it('should generate custom tool schema successfully', async () => {
const mockResponse = {
choices: [
{
message: {
content: JSON.stringify({
type: 'function',
function: {
name: 'testFunction',
description: 'A test function',
parameters: {
type: 'object',
properties: {
input: { type: 'string', description: 'Test input' },
},
required: ['input'],
additionalProperties: false,
},
},
}),
},
},
],
}
mockOpenAI.chat.completions.create.mockResolvedValueOnce(mockResponse)
const req = createMockRequest('POST', {
prompt: 'Create a custom tool for testing',
generationType: 'custom-tool-schema',
})
const { POST } = await import('./route')
const response = await POST(req)
const data = await response.json()
expect(response.status).toBe(200)
expect(data.success).toBe(true)
expect(data.generatedContent).toBeDefined()
})
it('should include context in the prompt', async () => {
const mockResponse = {
choices: [
{
message: {
content: 'const result = <input>;\nreturn result;',
},
},
],
}
mockOpenAI.chat.completions.create.mockResolvedValueOnce(mockResponse)
const req = createMockRequest('POST', {
prompt: 'Modify this function',
generationType: 'javascript-function-body',
context: 'existing function code here',
})
const { POST } = await import('./route')
const response = await POST(req)
expect(response.status).toBe(200)
expect(mockOpenAI.chat.completions.create).toHaveBeenCalledWith({
model: 'gpt-4o',
messages: expect.arrayContaining([
expect.objectContaining({ role: 'system' }),
expect.objectContaining({
role: 'user',
content:
'Prompt: Modify this function\\n\\nExisting Content/Context:\\nexisting function code here',
}),
]),
temperature: 0.2,
max_tokens: 1500,
response_format: undefined,
})
})
it('should include conversation history', async () => {
const mockResponse = {
choices: [
{
message: {
content: 'Updated function code',
},
},
],
}
mockOpenAI.chat.completions.create.mockResolvedValueOnce(mockResponse)
const req = createMockRequest('POST', {
prompt: 'Update the function',
generationType: 'javascript-function-body',
history: [
{ role: 'user', content: 'Create a function' },
{ role: 'assistant', content: 'function created' },
],
})
const { POST } = await import('./route')
const response = await POST(req)
expect(response.status).toBe(200)
expect(mockOpenAI.chat.completions.create).toHaveBeenCalledWith({
model: 'gpt-4o',
messages: expect.arrayContaining([
expect.objectContaining({ role: 'system' }),
expect.objectContaining({ role: 'user', content: 'Create a function' }),
expect.objectContaining({ role: 'assistant', content: 'function created' }),
expect.objectContaining({ role: 'user', content: 'Update the function' }),
]),
temperature: 0.2,
max_tokens: 1500,
response_format: undefined,
})
})
it('should handle missing OpenAI API key', async () => {
mockEnv.OPENAI_API_KEY = ''
const req = createMockRequest('POST', {
prompt: 'Test prompt',
generationType: 'json-schema',
})
const { POST } = await import('./route')
const response = await POST(req)
const data = await response.json()
expect(response.status).toBe(503)
expect(data.success).toBe(false)
expect(data.error).toBe('Code generation service is not configured.')
})
it('should handle missing required fields', async () => {
const req = createMockRequest('POST', {
prompt: '',
})
const { POST } = await import('./route')
const response = await POST(req)
const data = await response.json()
expect(response.status).toBe(400)
expect(data.success).toBe(false)
expect(data.error).toBe('Missing required fields: prompt and generationType.')
expect(mockLogger.warn).toHaveBeenCalled()
})
it('should handle invalid generation type', async () => {
const req = createMockRequest('POST', {
prompt: 'Test prompt',
generationType: 'invalid-type',
})
const { POST } = await import('./route')
const response = await POST(req)
const data = await response.json()
expect(response.status).toBe(400)
expect(data.success).toBe(false)
expect(data.error).toBe('Invalid generationType: invalid-type')
expect(mockLogger.warn).toHaveBeenCalled()
})
it('should handle empty OpenAI response', async () => {
const mockResponse = {
choices: [
{
message: {
content: null,
},
},
],
}
mockOpenAI.chat.completions.create.mockResolvedValueOnce(mockResponse)
const req = createMockRequest('POST', {
prompt: 'Test prompt',
generationType: 'javascript-function-body',
})
const { POST } = await import('./route')
const response = await POST(req)
const data = await response.json()
expect(response.status).toBe(500)
expect(data.success).toBe(false)
expect(data.error).toBe('Failed to generate content. OpenAI response was empty.')
expect(mockLogger.error).toHaveBeenCalled()
})
it('should handle invalid JSON schema generation', async () => {
const mockResponse = {
choices: [
{
message: {
content: 'invalid json content',
},
},
],
}
mockOpenAI.chat.completions.create.mockResolvedValueOnce(mockResponse)
const req = createMockRequest('POST', {
prompt: 'Create a schema',
generationType: 'json-schema',
})
const { POST } = await import('./route')
const response = await POST(req)
const data = await response.json()
expect(response.status).toBe(500)
expect(data.success).toBe(false)
expect(data.error).toBe('Generated JSON schema was invalid.')
expect(mockLogger.error).toHaveBeenCalled()
})
})

View File

@@ -6,7 +6,6 @@ import { createLogger } from '@/lib/logs/console-logger'
const logger = createLogger('CopilotAPI')
// Validation schemas
const MessageSchema = z.object({
role: z.enum(['user', 'assistant', 'system']),
content: z.string(),
@@ -20,7 +19,6 @@ const RequestSchema = z.object({
}),
})
// Define function schemas with strict typing
const workflowActions = {
addBlock: {
description: 'Add one new block to the workflow',

View File

@@ -2,9 +2,9 @@ import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console-logger'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
import { db } from '@/db'
import { account } from '@/db/schema'
import { refreshAccessTokenIfNeeded } from '../../utils'
export const dynamic = 'force-dynamic'

View File

@@ -2,9 +2,9 @@ import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console-logger'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
import { db } from '@/db'
import { account } from '@/db/schema'
import { refreshAccessTokenIfNeeded } from '../../utils'
export const dynamic = 'force-dynamic'

View File

@@ -2,9 +2,9 @@ import { and, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console-logger'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
import { db } from '@/db'
import { account } from '@/db/schema'
import { refreshAccessTokenIfNeeded } from '../../utils'
export const dynamic = 'force-dynamic'

View File

@@ -2,9 +2,9 @@ import { and, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console-logger'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
import { db } from '@/db'
import { account } from '@/db/schema'
import { refreshAccessTokenIfNeeded } from '../../utils'
export const dynamic = 'force-dynamic'

View File

@@ -4,7 +4,7 @@ import { getJiraCloudId } from '@/tools/jira/utils'
export const dynamic = 'force-dynamic'
const logger = new Logger('jira_issue')
const logger = new Logger('JiraIssueAPI')
export async function POST(request: Request) {
try {

View File

@@ -4,7 +4,7 @@ import { getJiraCloudId } from '@/tools/jira/utils'
export const dynamic = 'force-dynamic'
const logger = new Logger('jira_issues')
const logger = new Logger('JiraIssuesAPI')
export async function POST(request: Request) {
try {

View File

@@ -4,7 +4,7 @@ import { getJiraCloudId } from '@/tools/jira/utils'
export const dynamic = 'force-dynamic'
const logger = new Logger('jira_projects')
const logger = new Logger('JiraProjectsAPI')
export async function GET(request: Request) {
try {

View File

@@ -1,7 +1,7 @@
import { NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console-logger'
import { refreshAccessTokenIfNeeded } from '../../utils'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
export const dynamic = 'force-dynamic'

View File

@@ -1,7 +1,7 @@
import { NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console-logger'
import { refreshAccessTokenIfNeeded } from '../../utils'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
export const dynamic = 'force-dynamic'

View File

@@ -1,7 +1,7 @@
import { NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console-logger'
import { refreshAccessTokenIfNeeded } from '../../utils'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
export const dynamic = 'force-dynamic'

View File

@@ -1,7 +1,7 @@
import { NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console-logger'
import { refreshAccessTokenIfNeeded } from '../../utils'
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
export const dynamic = 'force-dynamic'

View File

@@ -183,7 +183,7 @@ export function ConfluenceFileSelector({
const accessToken = tokenData.accessToken
// Use the access token to fetch the page info
const response = await fetch('/api/auth/oauth/confluence/page', {
const response = await fetch('/api/tools/confluence/page', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -267,7 +267,7 @@ export function ConfluenceFileSelector({
}
// Simply fetch pages directly using the endpoint
const response = await fetch('/api/auth/oauth/confluence/pages', {
const response = await fetch('/api/tools/confluence/pages', {
method: 'POST',
headers: {
'Content-Type': 'application/json',

View File

@@ -63,7 +63,7 @@ export function DiscordChannelSelector({
setError(null)
try {
const response = await fetch('/api/auth/oauth/discord/channels', {
const response = await fetch('/api/tools/discord/channels', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -120,7 +120,7 @@ export function DiscordChannelSelector({
try {
// Only fetch the specific channel by ID instead of all channels
const response = await fetch('/api/auth/oauth/discord/channels', {
const response = await fetch('/api/tools/discord/channels', {
method: 'POST',
headers: {
'Content-Type': 'application/json',

View File

@@ -144,7 +144,7 @@ export function GoogleDrivePicker({
fileId: fileId,
})
const response = await fetch(`/api/auth/oauth/drive/file?${queryParams.toString()}`)
const response = await fetch(`/api/tools/drive/file?${queryParams.toString()}`)
if (response.ok) {
const data = await response.json()

View File

@@ -192,7 +192,7 @@ export function JiraIssueSelector({
}
// Use the access token to fetch the issue info
const response = await fetch('/api/auth/oauth/jira/issue', {
const response = await fetch('/api/tools/jira/issue', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -299,7 +299,7 @@ export function JiraIssueSelector({
...(cloudId && { cloudId }),
})
const response = await fetch(`/api/auth/oauth/jira/issues?${queryParams.toString()}`, {
const response = await fetch(`/api/tools/jira/issues?${queryParams.toString()}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',

View File

@@ -138,7 +138,7 @@ export function TeamsMessageSelector({
setError(null)
try {
const response = await fetch('/api/auth/oauth/microsoft-teams/teams', {
const response = await fetch('/api/tools/microsoft-teams/teams', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -198,7 +198,7 @@ export function TeamsMessageSelector({
setError(null)
try {
const response = await fetch('/api/auth/oauth/microsoft-teams/channels', {
const response = await fetch('/api/tools/microsoft-teams/channels', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -263,7 +263,7 @@ export function TeamsMessageSelector({
setError(null)
try {
const response = await fetch('/api/auth/oauth/microsoft-teams/chats', {
const response = await fetch('/api/tools/microsoft-teams/chats', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -501,7 +501,7 @@ export function TeamsMessageSelector({
setIsLoading(true)
try {
const response = await fetch('/api/auth/oauth/microsoft-teams/teams', {
const response = await fetch('/api/tools/microsoft-teams/teams', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ credential: selectedCredentialId, workflowId }),
@@ -539,7 +539,7 @@ export function TeamsMessageSelector({
setIsLoading(true)
try {
const response = await fetch('/api/auth/oauth/microsoft-teams/chats', {
const response = await fetch('/api/tools/microsoft-teams/chats', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ credential: selectedCredentialId, workflowId }),
@@ -578,7 +578,7 @@ export function TeamsMessageSelector({
setIsLoading(true)
try {
// First fetch teams to search through them
const teamsResponse = await fetch('/api/auth/oauth/microsoft-teams/teams', {
const teamsResponse = await fetch('/api/tools/microsoft-teams/teams', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ credential: selectedCredentialId, workflowId }),
@@ -591,7 +591,7 @@ export function TeamsMessageSelector({
const channelSearchPromises = teamsData.teams.map(
async (team: { id: string; displayName: string }) => {
try {
const channelsResponse = await fetch('/api/auth/oauth/microsoft-teams/channels', {
const channelsResponse = await fetch('/api/tools/microsoft-teams/channels', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({

View File

@@ -122,7 +122,7 @@ export function FolderSelector({
labelId: folderId,
})
const response = await fetch(`/api/auth/oauth/gmail/label?${queryParams.toString()}`)
const response = await fetch(`/api/tools/gmail/label?${queryParams.toString()}`)
if (response.ok) {
const data = await response.json()
@@ -166,10 +166,10 @@ export function FolderSelector({
// Determine the API endpoint based on provider
let apiEndpoint: string
if (provider === 'outlook') {
apiEndpoint = `/api/auth/oauth/outlook/folders?${queryParams.toString()}`
apiEndpoint = `/api/tools/outlook/folders?${queryParams.toString()}`
} else {
// Default to Gmail
apiEndpoint = `/api/auth/oauth/gmail/labels?${queryParams.toString()}`
apiEndpoint = `/api/tools/gmail/labels?${queryParams.toString()}`
}
const response = await fetch(apiEndpoint)

View File

@@ -59,7 +59,7 @@ export function DiscordServerSelector({
setError(null)
try {
const response = await fetch('/api/auth/oauth/discord/servers', {
const response = await fetch('/api/tools/discord/servers', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -114,7 +114,7 @@ export function DiscordServerSelector({
try {
// Only fetch the specific server by ID instead of all servers
const response = await fetch('/api/auth/oauth/discord/servers', {
const response = await fetch('/api/tools/discord/servers', {
method: 'POST',
headers: {
'Content-Type': 'application/json',

View File

@@ -196,7 +196,7 @@ export function JiraProjectSelector({
...(cloudId && { cloudId }),
})
const response = await fetch(`/api/auth/oauth/jira/project?${queryParams.toString()}`)
const response = await fetch(`/api/tools/jira/project?${queryParams.toString()}`)
if (!response.ok) {
const errorData = await response.json()
@@ -280,7 +280,7 @@ export function JiraProjectSelector({
})
// Use the GET endpoint for project search
const response = await fetch(`/api/auth/oauth/jira/projects?${queryParams.toString()}`)
const response = await fetch(`/api/tools/jira/projects?${queryParams.toString()}`)
if (!response.ok) {
const errorData = await response.json()

View File

@@ -123,7 +123,7 @@ export function GmailConfig({
const credentialId = credentialsData.credentials[0].id
const response = await fetch(`/api/auth/oauth/gmail/labels?credentialId=${credentialId}`)
const response = await fetch(`/api/tools/gmail/labels?credentialId=${credentialId}`)
if (!response.ok) {
throw new Error('Failed to fetch Gmail labels')
}

View File

@@ -102,7 +102,7 @@ const nextConfig: NextConfig = {
},
{
// Apply Cross-Origin Isolation headers to all routes except those that use the Google Drive Picker
source: '/((?!w/.*|api/auth/oauth/drive).*)',
source: '/((?!w/.*|api/tools/drive).*)',
headers: [
{
key: 'Cross-Origin-Embedder-Policy',
@@ -116,7 +116,7 @@ const nextConfig: NextConfig = {
},
{
// For routes that use the Google Drive Picker, only apply COOP but not COEP
source: '/(w/.*|api/auth/oauth/drive)',
source: '/(w/.*|api/tools/drive)',
headers: [
{
key: 'Cross-Origin-Opener-Policy',

View File

@@ -43,7 +43,7 @@ export const confluenceRetrieveTool: ToolConfig<
request: {
url: (params: ConfluenceRetrieveParams) => {
// Instead of calling Confluence API directly, use your API route
return '/api/auth/oauth/confluence/page'
return '/api/tools/confluence/page'
},
method: 'POST',
headers: (params: ConfluenceRetrieveParams) => {

View File

@@ -54,7 +54,7 @@ export const confluenceUpdateTool: ToolConfig<ConfluenceUpdateParams, Confluence
request: {
url: (params: ConfluenceUpdateParams) => {
return '/api/auth/oauth/confluence/page'
return '/api/tools/confluence/page'
},
method: 'PUT',
headers: (params: ConfluenceUpdateParams) => {

View File

@@ -30,7 +30,7 @@
"fumadocs-core": "^15.0.16",
"fumadocs-mdx": "^11.5.6",
"fumadocs-ui": "^15.0.16",
"lucide-react": "^0.479.0",
"lucide-react": "^0.511.0",
"next": "^15.2.3",
"next-themes": "^0.4.6",
"react": "19.1.0",
@@ -139,7 +139,7 @@
"@testing-library/react": "^16.2.0",
"@testing-library/user-event": "^14.6.1",
"@types/lodash": "^4.17.16",
"@types/node": "^20",
"@types/node": "^22",
"@types/prismjs": "^1.26.5",
"@types/react": "^19",
"@types/react-dom": "^19",
@@ -183,7 +183,6 @@
"overrides": {
"react": "19.1.0",
"react-dom": "19.1.0",
"tailwindcss": "3.4.1",
},
"packages": {
"@adobe/css-tools": ["@adobe/css-tools@4.4.2", "", {}, "sha512-baYZExFpsdkBNuvGKTKWCwKH57HRZLVtycZS05WTQNVOiXVSeAki3nU35zlRbToeMW8aHlJfyS+1C4BOv27q0A=="],
@@ -1488,7 +1487,7 @@
"check-error": ["check-error@2.1.1", "", {}, "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw=="],
"chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
"chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="],
@@ -2098,7 +2097,7 @@
"lru-cache": ["lru-cache@11.1.0", "", {}, "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A=="],
"lucide-react": ["lucide-react@0.479.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-aBhNnveRhorBOK7uA4gDjgaf+YlHMdMhQ/3cupk6exM10hWlEU+2QtWYOfhXhjAsmdb6LeKR+NZnow4UxRRiTQ=="],
"lucide-react": ["lucide-react@0.511.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-VK5a2ydJ7xm8GvBeKLS9mu1pVK6ucef9780JVUjw6bAjJL/QXnd4Y0p7SPeOUMC27YhzNCZvm5d/QX0Tp3rc0w=="],
"lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="],
@@ -2412,7 +2411,7 @@
"postcss-nested": ["postcss-nested@6.2.0", "", { "dependencies": { "postcss-selector-parser": "^6.1.1" }, "peerDependencies": { "postcss": "^8.2.14" } }, "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ=="],
"postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="],
"postcss-selector-parser": ["postcss-selector-parser@7.1.0", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA=="],
"postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="],
@@ -2502,7 +2501,7 @@
"readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
"readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
"real-require": ["real-require@0.2.0", "", {}, "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg=="],
@@ -2716,7 +2715,7 @@
"tailwind-merge": ["tailwind-merge@3.3.0", "", {}, "sha512-fyW/pEfcQSiigd5SNn0nApUOxx0zB/dm6UDU/rEwc2c3sX2smWUNbapHv+QRqLGVp9GWX3THIa7MUGPo+YkDzQ=="],
"tailwindcss": ["tailwindcss@3.4.1", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", "chokidar": "^3.5.3", "didyoumean": "^1.2.2", "dlv": "^1.1.3", "fast-glob": "^3.3.0", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", "jiti": "^1.19.1", "lilconfig": "^2.1.0", "micromatch": "^4.0.5", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", "picocolors": "^1.0.0", "postcss": "^8.4.23", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", "postcss-load-config": "^4.0.1", "postcss-nested": "^6.0.1", "postcss-selector-parser": "^6.0.11", "resolve": "^1.22.2", "sucrase": "^3.32.0" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" } }, "sha512-qAYmXRfk3ENzuPBakNK0SRrUDipP8NQnEY6772uDhflcQz5EhRdD7JNZxyrFHVQNCwULPBn6FNPp9brpO7ctcA=="],
"tailwindcss": ["tailwindcss@4.1.7", "", {}, "sha512-kr1o/ErIdNhTz8uzAYL7TpaUuzKIE6QPQ4qmSdxnoX/lo+5wmUHQA6h3L5yIqEImSRnAAURDirLu/BgiXGPAhg=="],
"tailwindcss-animate": ["tailwindcss-animate@1.0.7", "", { "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders" } }, "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA=="],
@@ -3208,8 +3207,6 @@
"bl/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
"chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"cli-truncate/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
"cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
@@ -3232,10 +3229,6 @@
"form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
"fumadocs-mdx/chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
"fumadocs-ui/postcss-selector-parser": ["postcss-selector-parser@7.1.0", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA=="],
"gaxios/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="],
"glob/minimatch": ["minimatch@10.0.1", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ=="],
@@ -3310,6 +3303,8 @@
"playwright/fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="],
"postcss-nested/postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="],
"pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="],
"protobufjs/long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="],
@@ -3318,18 +3313,18 @@
"react-email/chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="],
"react-email/chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
"react-email/commander": ["commander@13.1.0", "", {}, "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw=="],
"resend/@react-email/render": ["@react-email/render@1.0.6", "", { "dependencies": { "html-to-text": "9.0.5", "prettier": "3.5.3", "react-promise-suspense": "0.3.4" }, "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-zNueW5Wn/4jNC1c5LFgXzbUdv5Lhms+FWjOvWAhal7gx5YVf0q6dPJ0dnR70+ifo59gcMLwCZEaTS9EEuUhKvQ=="],
"restore-cursor/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
"sim/@types/node": ["@types/node@20.17.47", "", { "dependencies": { "undici-types": "~6.19.2" } }, "sha512-3dLX0Upo1v7RvUimvxLeXqwrfyKxUINk0EAM83swP2mlSUcwV73sZy8XhNz8bcZ3VbsfQyC/y6jRdL5tgCNpDQ=="],
"sim/lucide-react": ["lucide-react@0.479.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-aBhNnveRhorBOK7uA4gDjgaf+YlHMdMhQ/3cupk6exM10hWlEU+2QtWYOfhXhjAsmdb6LeKR+NZnow4UxRRiTQ=="],
"sim/tailwind-merge": ["tailwind-merge@2.6.0", "", {}, "sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA=="],
"sim/tailwindcss": ["tailwindcss@3.4.1", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", "chokidar": "^3.5.3", "didyoumean": "^1.2.2", "dlv": "^1.1.3", "fast-glob": "^3.3.0", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", "jiti": "^1.19.1", "lilconfig": "^2.1.0", "micromatch": "^4.0.5", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", "picocolors": "^1.0.0", "postcss": "^8.4.23", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", "postcss-load-config": "^4.0.1", "postcss-nested": "^6.0.1", "postcss-selector-parser": "^6.0.11", "resolve": "^1.22.2", "sucrase": "^3.32.0" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" } }, "sha512-qAYmXRfk3ENzuPBakNK0SRrUDipP8NQnEY6772uDhflcQz5EhRdD7JNZxyrFHVQNCwULPBn6FNPp9brpO7ctcA=="],
"simstudio/@types/node": ["@types/node@20.17.47", "", { "dependencies": { "undici-types": "~6.19.2" } }, "sha512-3dLX0Upo1v7RvUimvxLeXqwrfyKxUINk0EAM83swP2mlSUcwV73sZy8XhNz8bcZ3VbsfQyC/y6jRdL5tgCNpDQ=="],
"slice-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="],
@@ -3354,8 +3349,6 @@
"sucrase/glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="],
"tailwindcss/lilconfig": ["lilconfig@2.1.0", "", {}, "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ=="],
"terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
"test-exclude/glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="],
@@ -3364,6 +3357,8 @@
"unicode-trie/pako": ["pako@0.2.9", "", {}, "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA=="],
"unplugin/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
"vite/picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="],
"webpack/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
@@ -3532,8 +3527,6 @@
"form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
"fumadocs-mdx/chokidar/readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
"groq-sdk/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
"inquirer/ora/is-interactive": ["is-interactive@1.0.0", "", {}, "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w=="],
@@ -3600,9 +3593,11 @@
"ora/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
"react-email/chokidar/readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
"sim/tailwindcss/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
"sim/@types/node/undici-types": ["undici-types@6.19.8", "", {}, "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="],
"sim/tailwindcss/lilconfig": ["lilconfig@2.1.0", "", {}, "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ=="],
"sim/tailwindcss/postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="],
"simstudio/@types/node/undici-types": ["undici-types@6.19.8", "", {}, "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="],
@@ -3614,6 +3609,10 @@
"test-exclude/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
"unplugin/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"unplugin/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
"webpack/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
"@aws-crypto/sha1-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="],
@@ -3654,6 +3653,10 @@
"ora/cli-cursor/restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="],
"sim/tailwindcss/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"sim/tailwindcss/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
"sucrase/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
"test-exclude/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],

View File

@@ -23,8 +23,7 @@
},
"overrides": {
"react": "19.1.0",
"react-dom": "19.1.0",
"tailwindcss": "3.4.1"
"react-dom": "19.1.0"
},
"dependencies": {
"@t3-oss/env-nextjs": "0.13.4",