From 6afb453fc07aae7637da74a3717d04ecce108a5e Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Mon, 26 May 2025 12:00:03 -0700 Subject: [PATCH] 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 --- apps/docs/content/docs/tools/dropdown.mdx | 87 ---- apps/docs/content/docs/tools/meta.json | 1 - .../api/auth/forget-password/route.test.ts | 149 +++++++ .../api/auth/oauth/connections/route.test.ts | 220 ++++++++++ .../api/auth/oauth/credentials/route.test.ts | 256 ++++++++++++ .../api/auth/oauth/disconnect/route.test.ts | 159 +++++++ apps/sim/app/api/codegen/route.test.ts | 390 ++++++++++++++++++ apps/sim/app/api/copilot/route.ts | 2 - .../oauth => tools}/confluence/page/route.ts | 0 .../oauth => tools}/confluence/pages/route.ts | 0 .../oauth => tools}/discord/channels/route.ts | 0 .../oauth => tools}/discord/servers/route.ts | 0 .../{auth/oauth => tools}/drive/file/route.ts | 2 +- .../oauth => tools}/drive/files/route.ts | 2 +- .../oauth => tools}/gmail/label/route.ts | 2 +- .../oauth => tools}/gmail/labels/route.ts | 2 +- .../{auth/oauth => tools}/jira/issue/route.ts | 2 +- .../oauth => tools}/jira/issues/route.ts | 2 +- .../oauth => tools}/jira/projects/route.ts | 2 +- .../microsoft-teams/channels/route.ts | 2 +- .../microsoft-teams/chats/route.ts | 2 +- .../microsoft-teams/teams/route.ts | 2 +- .../oauth => tools}/outlook/folders/route.ts | 2 +- .../components/confluence-file-selector.tsx | 4 +- .../components/discord-channel-selector.tsx | 4 +- .../components/google-drive-picker.tsx | 2 +- .../components/jira-issue-selector.tsx | 4 +- .../components/teams-message-selector.tsx | 14 +- .../folder-selector/folder-selector.tsx | 6 +- .../components/discord-server-selector.tsx | 4 +- .../components/jira-project-selector.tsx | 4 +- .../webhook/components/providers/gmail.tsx | 2 +- apps/sim/next.config.ts | 4 +- apps/sim/tools/confluence/retrieve.ts | 2 +- apps/sim/tools/confluence/update.ts | 2 +- bun.lock | 49 +-- package.json | 3 +- 37 files changed, 1238 insertions(+), 152 deletions(-) delete mode 100644 apps/docs/content/docs/tools/dropdown.mdx create mode 100644 apps/sim/app/api/auth/forget-password/route.test.ts create mode 100644 apps/sim/app/api/auth/oauth/connections/route.test.ts create mode 100644 apps/sim/app/api/auth/oauth/credentials/route.test.ts create mode 100644 apps/sim/app/api/auth/oauth/disconnect/route.test.ts create mode 100644 apps/sim/app/api/codegen/route.test.ts rename apps/sim/app/api/{auth/oauth => tools}/confluence/page/route.ts (100%) rename apps/sim/app/api/{auth/oauth => tools}/confluence/pages/route.ts (100%) rename apps/sim/app/api/{auth/oauth => tools}/discord/channels/route.ts (100%) rename apps/sim/app/api/{auth/oauth => tools}/discord/servers/route.ts (100%) rename apps/sim/app/api/{auth/oauth => tools}/drive/file/route.ts (98%) rename apps/sim/app/api/{auth/oauth => tools}/drive/files/route.ts (98%) rename apps/sim/app/api/{auth/oauth => tools}/gmail/label/route.ts (98%) rename apps/sim/app/api/{auth/oauth => tools}/gmail/labels/route.ts (98%) rename apps/sim/app/api/{auth/oauth => tools}/jira/issue/route.ts (98%) rename apps/sim/app/api/{auth/oauth => tools}/jira/issues/route.ts (99%) rename apps/sim/app/api/{auth/oauth => tools}/jira/projects/route.ts (99%) rename apps/sim/app/api/{auth/oauth => tools}/microsoft-teams/channels/route.ts (98%) rename apps/sim/app/api/{auth/oauth => tools}/microsoft-teams/chats/route.ts (98%) rename apps/sim/app/api/{auth/oauth => tools}/microsoft-teams/teams/route.ts (97%) rename apps/sim/app/api/{auth/oauth => tools}/outlook/folders/route.ts (98%) diff --git a/apps/docs/content/docs/tools/dropdown.mdx b/apps/docs/content/docs/tools/dropdown.mdx deleted file mode 100644 index ccf80b081..000000000 --- a/apps/docs/content/docs/tools/dropdown.mdx +++ /dev/null @@ -1,87 +0,0 @@ ---- -title: File -description: Read and parse multiple files ---- - -import { BlockInfoCard } from "@/components/ui/block-info-card" - - - - - `} -/> - -## 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` diff --git a/apps/docs/content/docs/tools/meta.json b/apps/docs/content/docs/tools/meta.json index 30754e6d9..35286ec92 100644 --- a/apps/docs/content/docs/tools/meta.json +++ b/apps/docs/content/docs/tools/meta.json @@ -7,7 +7,6 @@ "clay", "confluence", "discord", - "dropdown", "elevenlabs", "exa", "file", diff --git a/apps/sim/app/api/auth/forget-password/route.test.ts b/apps/sim/app/api/auth/forget-password/route.test.ts new file mode 100644 index 000000000..7bc5d5b6a --- /dev/null +++ b/apps/sim/app/api/auth/forget-password/route.test.ts @@ -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() + }) +}) diff --git a/apps/sim/app/api/auth/oauth/connections/route.test.ts b/apps/sim/app/api/auth/oauth/connections/route.test.ts new file mode 100644 index 000000000..0293ccead --- /dev/null +++ b/apps/sim/app/api/auth/oauth/connections/route.test.ts @@ -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') + }) +}) diff --git a/apps/sim/app/api/auth/oauth/credentials/route.test.ts b/apps/sim/app/api/auth/oauth/credentials/route.test.ts new file mode 100644 index 000000000..a3cefe5e2 --- /dev/null +++ b/apps/sim/app/api/auth/oauth/credentials/route.test.ts @@ -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() + }) +}) diff --git a/apps/sim/app/api/auth/oauth/disconnect/route.test.ts b/apps/sim/app/api/auth/oauth/disconnect/route.test.ts new file mode 100644 index 000000000..4cb4269d4 --- /dev/null +++ b/apps/sim/app/api/auth/oauth/disconnect/route.test.ts @@ -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() + }) +}) diff --git a/apps/sim/app/api/codegen/route.test.ts b/apps/sim/app/api/codegen/route.test.ts new file mode 100644 index 000000000..38442a457 --- /dev/null +++ b/apps/sim/app/api/codegen/route.test.ts @@ -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 = ;\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 = ;\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 = ;\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() + }) +}) diff --git a/apps/sim/app/api/copilot/route.ts b/apps/sim/app/api/copilot/route.ts index 122b56201..e9c2ad421 100644 --- a/apps/sim/app/api/copilot/route.ts +++ b/apps/sim/app/api/copilot/route.ts @@ -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', diff --git a/apps/sim/app/api/auth/oauth/confluence/page/route.ts b/apps/sim/app/api/tools/confluence/page/route.ts similarity index 100% rename from apps/sim/app/api/auth/oauth/confluence/page/route.ts rename to apps/sim/app/api/tools/confluence/page/route.ts diff --git a/apps/sim/app/api/auth/oauth/confluence/pages/route.ts b/apps/sim/app/api/tools/confluence/pages/route.ts similarity index 100% rename from apps/sim/app/api/auth/oauth/confluence/pages/route.ts rename to apps/sim/app/api/tools/confluence/pages/route.ts diff --git a/apps/sim/app/api/auth/oauth/discord/channels/route.ts b/apps/sim/app/api/tools/discord/channels/route.ts similarity index 100% rename from apps/sim/app/api/auth/oauth/discord/channels/route.ts rename to apps/sim/app/api/tools/discord/channels/route.ts diff --git a/apps/sim/app/api/auth/oauth/discord/servers/route.ts b/apps/sim/app/api/tools/discord/servers/route.ts similarity index 100% rename from apps/sim/app/api/auth/oauth/discord/servers/route.ts rename to apps/sim/app/api/tools/discord/servers/route.ts diff --git a/apps/sim/app/api/auth/oauth/drive/file/route.ts b/apps/sim/app/api/tools/drive/file/route.ts similarity index 98% rename from apps/sim/app/api/auth/oauth/drive/file/route.ts rename to apps/sim/app/api/tools/drive/file/route.ts index 92eb5ed03..4db83803b 100644 --- a/apps/sim/app/api/auth/oauth/drive/file/route.ts +++ b/apps/sim/app/api/tools/drive/file/route.ts @@ -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' diff --git a/apps/sim/app/api/auth/oauth/drive/files/route.ts b/apps/sim/app/api/tools/drive/files/route.ts similarity index 98% rename from apps/sim/app/api/auth/oauth/drive/files/route.ts rename to apps/sim/app/api/tools/drive/files/route.ts index ed79d7f8c..98f8c8c2c 100644 --- a/apps/sim/app/api/auth/oauth/drive/files/route.ts +++ b/apps/sim/app/api/tools/drive/files/route.ts @@ -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' diff --git a/apps/sim/app/api/auth/oauth/gmail/label/route.ts b/apps/sim/app/api/tools/gmail/label/route.ts similarity index 98% rename from apps/sim/app/api/auth/oauth/gmail/label/route.ts rename to apps/sim/app/api/tools/gmail/label/route.ts index d013a9807..a691f40a5 100644 --- a/apps/sim/app/api/auth/oauth/gmail/label/route.ts +++ b/apps/sim/app/api/tools/gmail/label/route.ts @@ -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' diff --git a/apps/sim/app/api/auth/oauth/gmail/labels/route.ts b/apps/sim/app/api/tools/gmail/labels/route.ts similarity index 98% rename from apps/sim/app/api/auth/oauth/gmail/labels/route.ts rename to apps/sim/app/api/tools/gmail/labels/route.ts index 2ad15e632..654b25668 100644 --- a/apps/sim/app/api/auth/oauth/gmail/labels/route.ts +++ b/apps/sim/app/api/tools/gmail/labels/route.ts @@ -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' diff --git a/apps/sim/app/api/auth/oauth/jira/issue/route.ts b/apps/sim/app/api/tools/jira/issue/route.ts similarity index 98% rename from apps/sim/app/api/auth/oauth/jira/issue/route.ts rename to apps/sim/app/api/tools/jira/issue/route.ts index a9148faa5..2f454f57d 100644 --- a/apps/sim/app/api/auth/oauth/jira/issue/route.ts +++ b/apps/sim/app/api/tools/jira/issue/route.ts @@ -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 { diff --git a/apps/sim/app/api/auth/oauth/jira/issues/route.ts b/apps/sim/app/api/tools/jira/issues/route.ts similarity index 99% rename from apps/sim/app/api/auth/oauth/jira/issues/route.ts rename to apps/sim/app/api/tools/jira/issues/route.ts index 175be48f8..70a448fc9 100644 --- a/apps/sim/app/api/auth/oauth/jira/issues/route.ts +++ b/apps/sim/app/api/tools/jira/issues/route.ts @@ -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 { diff --git a/apps/sim/app/api/auth/oauth/jira/projects/route.ts b/apps/sim/app/api/tools/jira/projects/route.ts similarity index 99% rename from apps/sim/app/api/auth/oauth/jira/projects/route.ts rename to apps/sim/app/api/tools/jira/projects/route.ts index 92d1216de..7f2641a5a 100644 --- a/apps/sim/app/api/auth/oauth/jira/projects/route.ts +++ b/apps/sim/app/api/tools/jira/projects/route.ts @@ -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 { diff --git a/apps/sim/app/api/auth/oauth/microsoft-teams/channels/route.ts b/apps/sim/app/api/tools/microsoft-teams/channels/route.ts similarity index 98% rename from apps/sim/app/api/auth/oauth/microsoft-teams/channels/route.ts rename to apps/sim/app/api/tools/microsoft-teams/channels/route.ts index f52fc252e..510570efb 100644 --- a/apps/sim/app/api/auth/oauth/microsoft-teams/channels/route.ts +++ b/apps/sim/app/api/tools/microsoft-teams/channels/route.ts @@ -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' diff --git a/apps/sim/app/api/auth/oauth/microsoft-teams/chats/route.ts b/apps/sim/app/api/tools/microsoft-teams/chats/route.ts similarity index 98% rename from apps/sim/app/api/auth/oauth/microsoft-teams/chats/route.ts rename to apps/sim/app/api/tools/microsoft-teams/chats/route.ts index 6305c3775..da7940e96 100644 --- a/apps/sim/app/api/auth/oauth/microsoft-teams/chats/route.ts +++ b/apps/sim/app/api/tools/microsoft-teams/chats/route.ts @@ -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' diff --git a/apps/sim/app/api/auth/oauth/microsoft-teams/teams/route.ts b/apps/sim/app/api/tools/microsoft-teams/teams/route.ts similarity index 97% rename from apps/sim/app/api/auth/oauth/microsoft-teams/teams/route.ts rename to apps/sim/app/api/tools/microsoft-teams/teams/route.ts index 0bea8ab6e..9c8a332e9 100644 --- a/apps/sim/app/api/auth/oauth/microsoft-teams/teams/route.ts +++ b/apps/sim/app/api/tools/microsoft-teams/teams/route.ts @@ -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' diff --git a/apps/sim/app/api/auth/oauth/outlook/folders/route.ts b/apps/sim/app/api/tools/outlook/folders/route.ts similarity index 98% rename from apps/sim/app/api/auth/oauth/outlook/folders/route.ts rename to apps/sim/app/api/tools/outlook/folders/route.ts index df0897ebb..a17979327 100644 --- a/apps/sim/app/api/auth/oauth/outlook/folders/route.ts +++ b/apps/sim/app/api/tools/outlook/folders/route.ts @@ -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' diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/file-selector/components/confluence-file-selector.tsx b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/file-selector/components/confluence-file-selector.tsx index 687632fae..a7f2e9e7b 100644 --- a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/file-selector/components/confluence-file-selector.tsx +++ b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/file-selector/components/confluence-file-selector.tsx @@ -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', diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/file-selector/components/discord-channel-selector.tsx b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/file-selector/components/discord-channel-selector.tsx index 6c0005f4d..97f3c97d3 100644 --- a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/file-selector/components/discord-channel-selector.tsx +++ b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/file-selector/components/discord-channel-selector.tsx @@ -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', diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/file-selector/components/google-drive-picker.tsx b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/file-selector/components/google-drive-picker.tsx index fd6f70362..2d0938bae 100644 --- a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/file-selector/components/google-drive-picker.tsx +++ b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/file-selector/components/google-drive-picker.tsx @@ -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() diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/file-selector/components/jira-issue-selector.tsx b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/file-selector/components/jira-issue-selector.tsx index 2d33c6c0a..2a2c29229 100644 --- a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/file-selector/components/jira-issue-selector.tsx +++ b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/file-selector/components/jira-issue-selector.tsx @@ -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', diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/file-selector/components/teams-message-selector.tsx b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/file-selector/components/teams-message-selector.tsx index fc457c49a..1cea95426 100644 --- a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/file-selector/components/teams-message-selector.tsx +++ b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/file-selector/components/teams-message-selector.tsx @@ -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({ diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/folder-selector/folder-selector.tsx b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/folder-selector/folder-selector.tsx index c86334540..59db6b111 100644 --- a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/folder-selector/folder-selector.tsx +++ b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/folder-selector/folder-selector.tsx @@ -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) diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/project-selector/components/discord-server-selector.tsx b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/project-selector/components/discord-server-selector.tsx index d2f651e30..92dc5b88d 100644 --- a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/project-selector/components/discord-server-selector.tsx +++ b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/project-selector/components/discord-server-selector.tsx @@ -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', diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/project-selector/components/jira-project-selector.tsx b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/project-selector/components/jira-project-selector.tsx index 19e83f1ef..5d562708a 100644 --- a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/project-selector/components/jira-project-selector.tsx +++ b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/project-selector/components/jira-project-selector.tsx @@ -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() diff --git a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/components/providers/gmail.tsx b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/components/providers/gmail.tsx index d3a6375c1..0760ad104 100644 --- a/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/components/providers/gmail.tsx +++ b/apps/sim/app/w/[id]/components/workflow-block/components/sub-block/components/webhook/components/providers/gmail.tsx @@ -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') } diff --git a/apps/sim/next.config.ts b/apps/sim/next.config.ts index 95a02a33f..98a3eb745 100644 --- a/apps/sim/next.config.ts +++ b/apps/sim/next.config.ts @@ -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', diff --git a/apps/sim/tools/confluence/retrieve.ts b/apps/sim/tools/confluence/retrieve.ts index 32c1e962e..5acc80f22 100644 --- a/apps/sim/tools/confluence/retrieve.ts +++ b/apps/sim/tools/confluence/retrieve.ts @@ -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) => { diff --git a/apps/sim/tools/confluence/update.ts b/apps/sim/tools/confluence/update.ts index 2c0d8220b..73b204758 100644 --- a/apps/sim/tools/confluence/update.ts +++ b/apps/sim/tools/confluence/update.ts @@ -54,7 +54,7 @@ export const confluenceUpdateTool: ToolConfig { - return '/api/auth/oauth/confluence/page' + return '/api/tools/confluence/page' }, method: 'PUT', headers: (params: ConfluenceUpdateParams) => { diff --git a/bun.lock b/bun.lock index 8f6356293..e9c3dddeb 100644 --- a/bun.lock +++ b/bun.lock @@ -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=="], diff --git a/package.json b/package.json index de944b306..2d2b87343 100644 --- a/package.json +++ b/package.json @@ -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",