From 065fc5b87baa0a40079eb4e20910c371c766d20d Mon Sep 17 00:00:00 2001 From: Waleed Date: Fri, 12 Sep 2025 11:46:47 -0700 Subject: [PATCH] feat(api-keys): add workspace level api keys to share with other workspace members, add encryption for api keys (#1323) * update infra and remove railway * feat(api-keys): add workspace-level api keys * encrypt api keys * Revert "update infra and remove railway" This reverts commit b23258a5a128b47956d6f7f4622e1e736dc2332f. * reran migrations * tested workspace keys * consolidated code * more consolidation * cleanup * consolidate, remove unused code * add dummy key for ci * continue with regular path for self-hosted folks that don't have key set * fix tests * fix test * remove tests * removed ci additions --- apps/sim/.env.example | 3 +- apps/sim/app/api/jobs/[jobId]/route.ts | 23 +- apps/sim/app/api/users/me/api-keys/route.ts | 65 +- apps/sim/app/api/v1/auth.ts | 35 +- apps/sim/app/api/v1/middleware.ts | 4 +- .../app/api/workflows/[id]/deploy/route.ts | 135 +- apps/sim/app/api/workflows/[id]/route.ts | 31 +- apps/sim/app/api/workflows/middleware.ts | 41 +- .../workspaces/[id]/api-keys/[keyId]/route.ts | 141 + .../app/api/workspaces/[id]/api-keys/route.ts | 212 + .../components/deploy-form/deploy-form.tsx | 491 +- .../components/api-key/api-key.tsx | 41 +- .../example-command/example-command.tsx | 57 +- .../components/deploy-modal/deploy-modal.tsx | 9 +- .../components/api-keys/api-keys.tsx | 465 +- .../components/copilot/copilot.tsx | 1 - .../db/migrations/0089_amused_pete_wisdom.sql | 25 + .../sim/db/migrations/meta/0089_snapshot.json | 6754 +++++++++++++++++ apps/sim/db/migrations/meta/_journal.json | 7 + apps/sim/db/schema.ts | 39 +- .../handlers/loop/loop-handler.test.ts | 2 +- apps/sim/lib/api-key/auth.ts | 215 + apps/sim/lib/api-key/service.ts | 252 + apps/sim/lib/auth/hybrid.ts | 16 +- apps/sim/lib/copilot/auth.ts | 16 +- apps/sim/lib/email/validation.test.ts | 1 + apps/sim/lib/env.ts | 2 +- apps/sim/lib/schedules/utils.test.ts | 2 +- apps/sim/lib/uploads/blob/blob-client.test.ts | 2 +- apps/sim/lib/utils.test.ts | 20 - apps/sim/lib/utils.ts | 9 - apps/sim/lib/workflows/utils.ts | 40 +- apps/sim/serializer/index.test.ts | 2 +- .../serializer/tests/dual-validation.test.ts | 2 +- .../stores/workflows/workflow/utils.test.ts | 2 +- apps/sim/test-self-hosting.ts | 22 + bun.lock | 6 + package.json | 2 + 38 files changed, 8693 insertions(+), 499 deletions(-) create mode 100644 apps/sim/app/api/workspaces/[id]/api-keys/[keyId]/route.ts create mode 100644 apps/sim/app/api/workspaces/[id]/api-keys/route.ts create mode 100644 apps/sim/db/migrations/0089_amused_pete_wisdom.sql create mode 100644 apps/sim/db/migrations/meta/0089_snapshot.json create mode 100644 apps/sim/lib/api-key/auth.ts create mode 100644 apps/sim/lib/api-key/service.ts create mode 100644 apps/sim/test-self-hosting.ts diff --git a/apps/sim/.env.example b/apps/sim/.env.example index 0e5523715..0f3a44bb9 100644 --- a/apps/sim/.env.example +++ b/apps/sim/.env.example @@ -12,7 +12,8 @@ BETTER_AUTH_URL=http://localhost:3000 NEXT_PUBLIC_APP_URL=http://localhost:3000 # Security (Required) -ENCRYPTION_KEY=your_encryption_key # Use `openssl rand -hex 32` to generate +ENCRYPTION_KEY=your_encryption_key # Use `openssl rand -hex 32` to generate, used to encrypt environment variables +INTERNAL_API_SECRET=your_internal_api_secret # Use `openssl rand -hex 32` to generate, used to encrypt internal api routes # Email Provider (Optional) # RESEND_API_KEY= # Uncomment and add your key from https://resend.com to send actual emails diff --git a/apps/sim/app/api/jobs/[jobId]/route.ts b/apps/sim/app/api/jobs/[jobId]/route.ts index 8d377802d..bf5fd3ed9 100644 --- a/apps/sim/app/api/jobs/[jobId]/route.ts +++ b/apps/sim/app/api/jobs/[jobId]/route.ts @@ -1,12 +1,10 @@ import { runs } from '@trigger.dev/sdk' -import { eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' +import { authenticateApiKeyFromHeader, updateApiKeyLastUsed } from '@/lib/api-key/service' import { getSession } from '@/lib/auth' import { createLogger } from '@/lib/logs/console/logger' import { generateRequestId } from '@/lib/utils' import { createErrorResponse } from '@/app/api/workflows/utils' -import { db } from '@/db' -import { apiKey as apiKeyTable } from '@/db/schema' const logger = createLogger('TaskStatusAPI') @@ -27,14 +25,17 @@ export async function GET( if (!authenticatedUserId) { const apiKeyHeader = request.headers.get('x-api-key') if (apiKeyHeader) { - const [apiKeyRecord] = await db - .select({ userId: apiKeyTable.userId }) - .from(apiKeyTable) - .where(eq(apiKeyTable.key, apiKeyHeader)) - .limit(1) - - if (apiKeyRecord) { - authenticatedUserId = apiKeyRecord.userId + const authResult = await authenticateApiKeyFromHeader(apiKeyHeader) + if (authResult.success && authResult.userId) { + authenticatedUserId = authResult.userId + if (authResult.keyId) { + await updateApiKeyLastUsed(authResult.keyId).catch((error) => { + logger.warn(`[${requestId}] Failed to update API key last used timestamp:`, { + keyId: authResult.keyId, + error, + }) + }) + } } } } diff --git a/apps/sim/app/api/users/me/api-keys/route.ts b/apps/sim/app/api/users/me/api-keys/route.ts index 764301e93..4f24a4302 100644 --- a/apps/sim/app/api/users/me/api-keys/route.ts +++ b/apps/sim/app/api/users/me/api-keys/route.ts @@ -1,9 +1,9 @@ -import { eq } from 'drizzle-orm' +import { and, eq } from 'drizzle-orm' import { nanoid } from 'nanoid' import { type NextRequest, NextResponse } from 'next/server' +import { createApiKey, getApiKeyDisplayFormat } from '@/lib/api-key/auth' import { getSession } from '@/lib/auth' import { createLogger } from '@/lib/logs/console/logger' -import { generateApiKey } from '@/lib/utils' import { db } from '@/db' import { apiKey } from '@/db/schema' @@ -19,7 +19,6 @@ export async function GET(request: NextRequest) { const userId = session.user.id - // Fetch all API keys for this user const keys = await db .select({ id: apiKey.id, @@ -30,13 +29,19 @@ export async function GET(request: NextRequest) { expiresAt: apiKey.expiresAt, }) .from(apiKey) - .where(eq(apiKey.userId, userId)) + .where(and(eq(apiKey.userId, userId), eq(apiKey.type, 'personal'))) .orderBy(apiKey.createdAt) - const maskedKeys = keys.map((key) => ({ - ...key, - key: key.key, - })) + const maskedKeys = await Promise.all( + keys.map(async (key) => { + const displayFormat = await getApiKeyDisplayFormat(key.key) + return { + ...key, + key: key.key, + displayKey: displayFormat, + } + }) + ) return NextResponse.json({ keys: maskedKeys }) } catch (error) { @@ -56,33 +61,61 @@ export async function POST(request: NextRequest) { const userId = session.user.id const body = await request.json() - // Validate the request - const { name } = body - if (!name || typeof name !== 'string') { + const { name: rawName } = body + if (!rawName || typeof rawName !== 'string') { return NextResponse.json({ error: 'Invalid request. Name is required.' }, { status: 400 }) } - const keyValue = generateApiKey() + const name = rawName.trim() + if (!name) { + return NextResponse.json({ error: 'Name cannot be empty.' }, { status: 400 }) + } + + const existingKey = await db + .select() + .from(apiKey) + .where(and(eq(apiKey.userId, userId), eq(apiKey.name, name), eq(apiKey.type, 'personal'))) + .limit(1) + + if (existingKey.length > 0) { + return NextResponse.json( + { + error: `A personal API key named "${name}" already exists. Please choose a different name.`, + }, + { status: 409 } + ) + } + + const { key: plainKey, encryptedKey } = await createApiKey(true) + + if (!encryptedKey) { + throw new Error('Failed to encrypt API key for storage') + } - // Insert the new API key const [newKey] = await db .insert(apiKey) .values({ id: nanoid(), userId, + workspaceId: null, name, - key: keyValue, + key: encryptedKey, + type: 'personal', createdAt: new Date(), updatedAt: new Date(), }) .returning({ id: apiKey.id, name: apiKey.name, - key: apiKey.key, createdAt: apiKey.createdAt, }) - return NextResponse.json({ key: newKey }) + return NextResponse.json({ + key: { + ...newKey, + key: plainKey, + }, + }) } catch (error) { logger.error('Failed to create API key', { error }) return NextResponse.json({ error: 'Failed to create API key' }, { status: 500 }) diff --git a/apps/sim/app/api/v1/auth.ts b/apps/sim/app/api/v1/auth.ts index 78e9b1c1c..b9e5b59c2 100644 --- a/apps/sim/app/api/v1/auth.ts +++ b/apps/sim/app/api/v1/auth.ts @@ -1,18 +1,18 @@ -import { eq } from 'drizzle-orm' import type { NextRequest } from 'next/server' +import { authenticateApiKeyFromHeader, updateApiKeyLastUsed } from '@/lib/api-key/service' import { createLogger } from '@/lib/logs/console/logger' -import { db } from '@/db' -import { apiKey as apiKeyTable } from '@/db/schema' const logger = createLogger('V1Auth') export interface AuthResult { authenticated: boolean userId?: string + workspaceId?: string + keyType?: 'personal' | 'workspace' error?: string } -export async function authenticateApiKey(request: NextRequest): Promise { +export async function authenticateV1Request(request: NextRequest): Promise { const apiKey = request.headers.get('x-api-key') if (!apiKey) { @@ -23,36 +23,23 @@ export async function authenticateApiKey(request: NextRequest): Promise { try { - const auth = await authenticateApiKey(request) + const auth = await authenticateV1Request(request) if (!auth.authenticated) { return { allowed: false, diff --git a/apps/sim/app/api/workflows/[id]/deploy/route.ts b/apps/sim/app/api/workflows/[id]/deploy/route.ts index 6dfeb331f..caaedee42 100644 --- a/apps/sim/app/api/workflows/[id]/deploy/route.ts +++ b/apps/sim/app/api/workflows/[id]/deploy/route.ts @@ -1,8 +1,9 @@ import { and, desc, eq } from 'drizzle-orm' import type { NextRequest } from 'next/server' import { v4 as uuidv4 } from 'uuid' +import { generateApiKey } from '@/lib/api-key/service' import { createLogger } from '@/lib/logs/console/logger' -import { generateApiKey, generateRequestId } from '@/lib/utils' +import { generateRequestId } from '@/lib/utils' import { validateWorkflowAccess } from '@/app/api/workflows/middleware' import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/utils' import { db } from '@/db' @@ -33,7 +34,7 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ deployedAt: workflow.deployedAt, userId: workflow.userId, deployedState: workflow.deployedState, - pinnedApiKey: workflow.pinnedApiKey, + pinnedApiKeyId: workflow.pinnedApiKeyId, }) .from(workflow) .where(eq(workflow.id, id)) @@ -57,18 +58,28 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ }) } - let userKey: string | null = null + let keyInfo: { name: string; type: 'personal' | 'workspace' } | null = null - if (workflowData.pinnedApiKey) { - userKey = workflowData.pinnedApiKey + if (workflowData.pinnedApiKeyId) { + const pinnedKey = await db + .select({ key: apiKey.key, name: apiKey.name, type: apiKey.type }) + .from(apiKey) + .where(eq(apiKey.id, workflowData.pinnedApiKeyId)) + .limit(1) + + if (pinnedKey.length > 0) { + keyInfo = { name: pinnedKey[0].name, type: pinnedKey[0].type as 'personal' | 'workspace' } + } } else { // Fetch the user's API key, preferring the most recently used const userApiKey = await db .select({ key: apiKey.key, + name: apiKey.name, + type: apiKey.type, }) .from(apiKey) - .where(eq(apiKey.userId, workflowData.userId)) + .where(and(eq(apiKey.userId, workflowData.userId), eq(apiKey.type, 'personal'))) .orderBy(desc(apiKey.lastUsed), desc(apiKey.createdAt)) .limit(1) @@ -76,22 +87,24 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ if (userApiKey.length === 0) { try { const newApiKeyVal = generateApiKey() + const keyName = 'Default API Key' await db.insert(apiKey).values({ id: uuidv4(), userId: workflowData.userId, - name: 'Default API Key', + workspaceId: null, + name: keyName, key: newApiKeyVal, + type: 'personal', createdAt: new Date(), updatedAt: new Date(), }) - userKey = newApiKeyVal + keyInfo = { name: keyName, type: 'personal' } logger.info(`[${requestId}] Generated new API key for user: ${workflowData.userId}`) } catch (keyError) { - // If key generation fails, log the error but continue with the request logger.error(`[${requestId}] Failed to generate API key:`, keyError) } } else { - userKey = userApiKey[0].key + keyInfo = { name: userApiKey[0].name, type: userApiKey[0].type as 'personal' | 'workspace' } } } @@ -120,8 +133,11 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ } logger.info(`[${requestId}] Successfully retrieved deployment info: ${id}`) + + const responseApiKeyInfo = keyInfo ? `${keyInfo.name} (${keyInfo.type})` : 'No API key found' + return createSuccessResponse({ - apiKey: userKey, + apiKey: responseApiKeyInfo, isDeployed: workflowData.isDeployed, deployedAt: workflowData.deployedAt, needsRedeployment, @@ -149,7 +165,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{ const workflowData = await db .select({ userId: workflow.userId, - pinnedApiKey: workflow.pinnedApiKey, + pinnedApiKeyId: workflow.pinnedApiKeyId, }) .from(workflow) .where(eq(workflow.id, id)) @@ -285,12 +301,10 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{ key: apiKey.key, }) .from(apiKey) - .where(eq(apiKey.userId, userId)) + .where(and(eq(apiKey.userId, userId), eq(apiKey.type, 'personal'))) .orderBy(desc(apiKey.lastUsed), desc(apiKey.createdAt)) .limit(1) - let userKey = null - // If no API key exists, create one if (userApiKey.length === 0) { try { @@ -298,30 +312,85 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{ await db.insert(apiKey).values({ id: uuidv4(), userId, + workspaceId: null, // Personal keys must have NULL workspaceId name: 'Default API Key', key: newApiKey, + type: 'personal', // Explicitly set type createdAt: new Date(), updatedAt: new Date(), }) - userKey = newApiKey logger.info(`[${requestId}] Generated new API key for user: ${userId}`) } catch (keyError) { // If key generation fails, log the error but continue with the request logger.error(`[${requestId}] Failed to generate API key:`, keyError) } - } else { - userKey = userApiKey[0].key } - // If client provided a specific API key and it belongs to the user, prefer it + let keyInfo: { name: string; type: 'personal' | 'workspace' } | null = null + let matchedKey: { + id: string + key: string + name: string + type: 'personal' | 'workspace' + } | null = null + if (providedApiKey) { - const [owned] = await db - .select({ key: apiKey.key }) + let isValidKey = false + + const [personalKey] = await db + .select({ id: apiKey.id, key: apiKey.key, name: apiKey.name, expiresAt: apiKey.expiresAt }) .from(apiKey) - .where(and(eq(apiKey.userId, userId), eq(apiKey.key, providedApiKey))) + .where( + and(eq(apiKey.id, providedApiKey), eq(apiKey.userId, userId), eq(apiKey.type, 'personal')) + ) .limit(1) - if (owned) { - userKey = providedApiKey + + if (personalKey) { + if (!personalKey.expiresAt || personalKey.expiresAt >= new Date()) { + matchedKey = { ...personalKey, type: 'personal' } + isValidKey = true + keyInfo = { name: personalKey.name, type: 'personal' } + } + } + + if (!isValidKey) { + const [workflowData] = await db + .select({ workspaceId: workflow.workspaceId }) + .from(workflow) + .where(eq(workflow.id, id)) + .limit(1) + + if (workflowData?.workspaceId) { + const [workspaceKey] = await db + .select({ + id: apiKey.id, + key: apiKey.key, + name: apiKey.name, + expiresAt: apiKey.expiresAt, + }) + .from(apiKey) + .where( + and( + eq(apiKey.id, providedApiKey), + eq(apiKey.workspaceId, workflowData.workspaceId), + eq(apiKey.type, 'workspace') + ) + ) + .limit(1) + + if (workspaceKey) { + if (!workspaceKey.expiresAt || workspaceKey.expiresAt >= new Date()) { + matchedKey = { ...workspaceKey, type: 'workspace' } + isValidKey = true + keyInfo = { name: workspaceKey.name, type: 'workspace' } + } + } + } + } + + if (!isValidKey) { + logger.warn(`[${requestId}] Invalid API key ID provided for workflow deployment: ${id}`) + return createErrorResponse('Invalid API key provided', 400) } } @@ -332,26 +401,33 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{ deployedState: currentState, } // Only pin when the client explicitly provided a key in this request - if (providedApiKey) { - updateData.pinnedApiKey = userKey + if (providedApiKey && keyInfo && matchedKey) { + updateData.pinnedApiKeyId = matchedKey.id } await db.update(workflow).set(updateData).where(eq(workflow.id, id)) // Update lastUsed for the key we returned - if (userKey) { + if (matchedKey) { try { await db .update(apiKey) .set({ lastUsed: new Date(), updatedAt: new Date() }) - .where(eq(apiKey.key, userKey)) + .where(eq(apiKey.id, matchedKey.id)) } catch (e) { logger.warn(`[${requestId}] Failed to update lastUsed for api key`) } } logger.info(`[${requestId}] Workflow deployed successfully: ${id}`) - return createSuccessResponse({ apiKey: userKey, isDeployed: true, deployedAt }) + + const responseApiKeyInfo = keyInfo ? `${keyInfo.name} (${keyInfo.type})` : 'Default key' + + return createSuccessResponse({ + apiKey: responseApiKeyInfo, + isDeployed: true, + deployedAt, + }) } catch (error: any) { logger.error(`[${requestId}] Error deploying workflow: ${id}`, { error: error.message, @@ -387,6 +463,7 @@ export async function DELETE( isDeployed: false, deployedAt: null, deployedState: null, + pinnedApiKeyId: null, }) .where(eq(workflow.id, id)) diff --git a/apps/sim/app/api/workflows/[id]/route.ts b/apps/sim/app/api/workflows/[id]/route.ts index 815d66cdc..5a4a24a3a 100644 --- a/apps/sim/app/api/workflows/[id]/route.ts +++ b/apps/sim/app/api/workflows/[id]/route.ts @@ -1,6 +1,7 @@ import { eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' +import { authenticateApiKeyFromHeader, updateApiKeyLastUsed } from '@/lib/api-key/service' import { getSession } from '@/lib/auth' import { verifyInternalToken } from '@/lib/auth/internal' import { env } from '@/lib/env' @@ -9,7 +10,7 @@ import { getUserEntityPermissions, hasAdminPermission } from '@/lib/permissions/ import { generateRequestId } from '@/lib/utils' import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/db-helpers' import { db } from '@/db' -import { apiKey as apiKeyTable, templates, workflow } from '@/db/schema' +import { templates, workflow } from '@/db/schema' const logger = createLogger('WorkflowByIdAPI') @@ -31,7 +32,6 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ const { id: workflowId } = await params try { - // Check for internal JWT token for server-side calls const authHeader = request.headers.get('authorization') let isInternalCall = false @@ -43,26 +43,25 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ let userId: string | null = null if (isInternalCall) { - // For internal calls, we'll skip user-specific access checks logger.info(`[${requestId}] Internal API call for workflow ${workflowId}`) } else { - // Try session auth first (for web UI) const session = await getSession() let authenticatedUserId: string | null = session?.user?.id || null - // If no session, check for API key auth if (!authenticatedUserId) { const apiKeyHeader = request.headers.get('x-api-key') if (apiKeyHeader) { - // Verify API key - const [apiKeyRecord] = await db - .select({ userId: apiKeyTable.userId }) - .from(apiKeyTable) - .where(eq(apiKeyTable.key, apiKeyHeader)) - .limit(1) - - if (apiKeyRecord) { - authenticatedUserId = apiKeyRecord.userId + const authResult = await authenticateApiKeyFromHeader(apiKeyHeader) + if (authResult.success && authResult.userId) { + authenticatedUserId = authResult.userId + if (authResult.keyId) { + await updateApiKeyLastUsed(authResult.keyId).catch((error) => { + logger.warn(`[${requestId}] Failed to update API key last used timestamp:`, { + keyId: authResult.keyId, + error, + }) + }) + } } } } @@ -117,7 +116,6 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ } } - // Try to load from normalized tables first logger.debug(`[${requestId}] Attempting to load workflow ${workflowId} from normalized tables`) const normalizedData = await loadWorkflowFromNormalizedTables(workflowId) @@ -130,7 +128,6 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ loops: normalizedData.loops, }) - // Construct response object with workflow data and state from normalized tables const finalWorkflowData = { ...workflowData, state: { @@ -175,7 +172,6 @@ export async function DELETE( const { id: workflowId } = await params try { - // Get the session const session = await getSession() if (!session?.user?.id) { logger.warn(`[${requestId}] Unauthorized deletion attempt for workflow ${workflowId}`) @@ -184,7 +180,6 @@ export async function DELETE( const userId = session.user.id - // Fetch the workflow to check ownership/access const workflowData = await db .select() .from(workflow) diff --git a/apps/sim/app/api/workflows/middleware.ts b/apps/sim/app/api/workflows/middleware.ts index 855fbb6e0..5cab568a2 100644 --- a/apps/sim/app/api/workflows/middleware.ts +++ b/apps/sim/app/api/workflows/middleware.ts @@ -1,9 +1,8 @@ -import { and, eq } from 'drizzle-orm' import type { NextRequest } from 'next/server' +import { authenticateApiKey } from '@/lib/api-key/auth' +import { authenticateApiKeyFromHeader, updateApiKeyLastUsed } from '@/lib/api-key/service' import { createLogger } from '@/lib/logs/console/logger' import { getWorkflowById } from '@/lib/workflows/utils' -import { db } from '@/db' -import { apiKey } from '@/db/schema' const logger = createLogger('WorkflowMiddleware') @@ -57,8 +56,9 @@ export async function validateWorkflowAccess( } // If a pinned key exists, only accept that specific key - if (workflow.pinnedApiKey) { - if (workflow.pinnedApiKey !== apiKeyHeader) { + if (workflow.pinnedApiKey?.key) { + const isValidPinnedKey = await authenticateApiKey(apiKeyHeader, workflow.pinnedApiKey.key) + if (!isValidPinnedKey) { return { error: { message: 'Unauthorized: Invalid API key', @@ -67,14 +67,29 @@ export async function validateWorkflowAccess( } } } else { - // Otherwise, verify the key belongs to the workflow owner - const [owned] = await db - .select({ key: apiKey.key }) - .from(apiKey) - .where(and(eq(apiKey.userId, workflow.userId), eq(apiKey.key, apiKeyHeader))) - .limit(1) + // Try personal keys first + const personalResult = await authenticateApiKeyFromHeader(apiKeyHeader, { + userId: workflow.userId as string, + keyTypes: ['personal'], + }) - if (!owned) { + let validResult = null + if (personalResult.success) { + validResult = personalResult + } else if (workflow.workspaceId) { + // Try workspace keys + const workspaceResult = await authenticateApiKeyFromHeader(apiKeyHeader, { + workspaceId: workflow.workspaceId as string, + keyTypes: ['workspace'], + }) + + if (workspaceResult.success) { + validResult = workspaceResult + } + } + + // If no valid key found, reject + if (!validResult) { return { error: { message: 'Unauthorized: Invalid API key', @@ -82,6 +97,8 @@ export async function validateWorkflowAccess( }, } } + + await updateApiKeyLastUsed(validResult.keyId!) } } return { workflow } diff --git a/apps/sim/app/api/workspaces/[id]/api-keys/[keyId]/route.ts b/apps/sim/app/api/workspaces/[id]/api-keys/[keyId]/route.ts new file mode 100644 index 000000000..96cd1ef2b --- /dev/null +++ b/apps/sim/app/api/workspaces/[id]/api-keys/[keyId]/route.ts @@ -0,0 +1,141 @@ +import { and, eq, not } from 'drizzle-orm' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { getSession } from '@/lib/auth' +import { createLogger } from '@/lib/logs/console/logger' +import { getUserEntityPermissions } from '@/lib/permissions/utils' +import { generateRequestId } from '@/lib/utils' +import { db } from '@/db' +import { apiKey } from '@/db/schema' + +const logger = createLogger('WorkspaceApiKeyAPI') + +const UpdateKeySchema = z.object({ + name: z.string().min(1, 'Name is required'), +}) + +export async function PUT( + request: NextRequest, + { params }: { params: Promise<{ id: string; keyId: string }> } +) { + const requestId = generateRequestId() + const { id: workspaceId, keyId } = await params + + try { + const session = await getSession() + if (!session?.user?.id) { + logger.warn(`[${requestId}] Unauthorized workspace API key update attempt`) + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const userId = session.user.id + + const permission = await getUserEntityPermissions(userId, 'workspace', workspaceId) + if (!permission || (permission !== 'admin' && permission !== 'write')) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) + } + + const body = await request.json() + const { name } = UpdateKeySchema.parse(body) + + const existingKey = await db + .select() + .from(apiKey) + .where( + and(eq(apiKey.workspaceId, workspaceId), eq(apiKey.id, keyId), eq(apiKey.type, 'workspace')) + ) + .limit(1) + + if (existingKey.length === 0) { + return NextResponse.json({ error: 'API key not found' }, { status: 404 }) + } + + const conflictingKey = await db + .select() + .from(apiKey) + .where( + and( + eq(apiKey.workspaceId, workspaceId), + eq(apiKey.name, name), + eq(apiKey.type, 'workspace'), + not(eq(apiKey.id, keyId)) + ) + ) + .limit(1) + + if (conflictingKey.length > 0) { + return NextResponse.json( + { error: 'A workspace API key with this name already exists' }, + { status: 400 } + ) + } + + const [updatedKey] = await db + .update(apiKey) + .set({ + name, + updatedAt: new Date(), + }) + .where( + and(eq(apiKey.workspaceId, workspaceId), eq(apiKey.id, keyId), eq(apiKey.type, 'workspace')) + ) + .returning({ + id: apiKey.id, + name: apiKey.name, + createdAt: apiKey.createdAt, + updatedAt: apiKey.updatedAt, + }) + + logger.info(`[${requestId}] Updated workspace API key: ${keyId} in workspace ${workspaceId}`) + return NextResponse.json({ key: updatedKey }) + } catch (error: unknown) { + logger.error(`[${requestId}] Workspace API key PUT error`, error) + return NextResponse.json( + { error: error instanceof Error ? error.message : 'Failed to update workspace API key' }, + { status: 500 } + ) + } +} + +export async function DELETE( + request: NextRequest, + { params }: { params: Promise<{ id: string; keyId: string }> } +) { + const requestId = generateRequestId() + const { id: workspaceId, keyId } = await params + + try { + const session = await getSession() + if (!session?.user?.id) { + logger.warn(`[${requestId}] Unauthorized workspace API key deletion attempt`) + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const userId = session.user.id + + const permission = await getUserEntityPermissions(userId, 'workspace', workspaceId) + if (!permission || (permission !== 'admin' && permission !== 'write')) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) + } + + const deletedRows = await db + .delete(apiKey) + .where( + and(eq(apiKey.workspaceId, workspaceId), eq(apiKey.id, keyId), eq(apiKey.type, 'workspace')) + ) + .returning({ id: apiKey.id }) + + if (deletedRows.length === 0) { + return NextResponse.json({ error: 'API key not found' }, { status: 404 }) + } + + logger.info(`[${requestId}] Deleted workspace API key: ${keyId} from workspace ${workspaceId}`) + return NextResponse.json({ success: true }) + } catch (error: unknown) { + logger.error(`[${requestId}] Workspace API key DELETE error`, error) + return NextResponse.json( + { error: error instanceof Error ? error.message : 'Failed to delete workspace API key' }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/workspaces/[id]/api-keys/route.ts b/apps/sim/app/api/workspaces/[id]/api-keys/route.ts new file mode 100644 index 000000000..c33ec087a --- /dev/null +++ b/apps/sim/app/api/workspaces/[id]/api-keys/route.ts @@ -0,0 +1,212 @@ +import { and, eq, inArray } from 'drizzle-orm' +import { nanoid } from 'nanoid' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { createApiKey, getApiKeyDisplayFormat } from '@/lib/api-key/auth' +import { getSession } from '@/lib/auth' +import { createLogger } from '@/lib/logs/console/logger' +import { getUserEntityPermissions } from '@/lib/permissions/utils' +import { generateRequestId } from '@/lib/utils' +import { db } from '@/db' +import { apiKey, workspace } from '@/db/schema' + +const logger = createLogger('WorkspaceApiKeysAPI') + +const CreateKeySchema = z.object({ + name: z.string().trim().min(1, 'Name is required'), +}) + +const DeleteKeysSchema = z.object({ + keys: z.array(z.string()).min(1), +}) + +export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { + const requestId = generateRequestId() + const workspaceId = (await params).id + + try { + const session = await getSession() + if (!session?.user?.id) { + logger.warn(`[${requestId}] Unauthorized workspace API keys access attempt`) + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const userId = session.user.id + + const ws = await db.select().from(workspace).where(eq(workspace.id, workspaceId)).limit(1) + if (!ws.length) { + return NextResponse.json({ error: 'Workspace not found' }, { status: 404 }) + } + + const permission = await getUserEntityPermissions(userId, 'workspace', workspaceId) + if (!permission) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const workspaceKeys = await db + .select({ + id: apiKey.id, + name: apiKey.name, + key: apiKey.key, + createdAt: apiKey.createdAt, + lastUsed: apiKey.lastUsed, + expiresAt: apiKey.expiresAt, + createdBy: apiKey.createdBy, + }) + .from(apiKey) + .where(and(eq(apiKey.workspaceId, workspaceId), eq(apiKey.type, 'workspace'))) + .orderBy(apiKey.createdAt) + + const formattedWorkspaceKeys = await Promise.all( + workspaceKeys.map(async (key) => { + const displayFormat = await getApiKeyDisplayFormat(key.key) + return { + ...key, + key: key.key, + displayKey: displayFormat, + } + }) + ) + + return NextResponse.json({ + keys: formattedWorkspaceKeys, + }) + } catch (error: unknown) { + logger.error(`[${requestId}] Workspace API keys GET error`, error) + return NextResponse.json( + { error: error instanceof Error ? error.message : 'Failed to load API keys' }, + { status: 500 } + ) + } +} + +export async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { + const requestId = generateRequestId() + const workspaceId = (await params).id + + try { + const session = await getSession() + if (!session?.user?.id) { + logger.warn(`[${requestId}] Unauthorized workspace API key creation attempt`) + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const userId = session.user.id + + const permission = await getUserEntityPermissions(userId, 'workspace', workspaceId) + if (!permission || (permission !== 'admin' && permission !== 'write')) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) + } + + const body = await request.json() + const { name } = CreateKeySchema.parse(body) + + const existingKey = await db + .select() + .from(apiKey) + .where( + and( + eq(apiKey.workspaceId, workspaceId), + eq(apiKey.name, name), + eq(apiKey.type, 'workspace') + ) + ) + .limit(1) + + if (existingKey.length > 0) { + return NextResponse.json( + { + error: `A workspace API key named "${name}" already exists. Please choose a different name.`, + }, + { status: 409 } + ) + } + + const { key: plainKey, encryptedKey } = await createApiKey(true) + + if (!encryptedKey) { + throw new Error('Failed to encrypt API key for storage') + } + + const [newKey] = await db + .insert(apiKey) + .values({ + id: nanoid(), + workspaceId, + userId: userId, + createdBy: userId, + name, + key: encryptedKey, + type: 'workspace', + createdAt: new Date(), + updatedAt: new Date(), + }) + .returning({ + id: apiKey.id, + name: apiKey.name, + createdAt: apiKey.createdAt, + }) + + logger.info(`[${requestId}] Created workspace API key: ${name} in workspace ${workspaceId}`) + + return NextResponse.json({ + key: { + ...newKey, + key: plainKey, + }, + }) + } catch (error: unknown) { + logger.error(`[${requestId}] Workspace API key POST error`, error) + return NextResponse.json( + { error: error instanceof Error ? error.message : 'Failed to create workspace API key' }, + { status: 500 } + ) + } +} + +export async function DELETE( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + const requestId = generateRequestId() + const workspaceId = (await params).id + + try { + const session = await getSession() + if (!session?.user?.id) { + logger.warn(`[${requestId}] Unauthorized workspace API key deletion attempt`) + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const userId = session.user.id + + const permission = await getUserEntityPermissions(userId, 'workspace', workspaceId) + if (!permission || (permission !== 'admin' && permission !== 'write')) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) + } + + const body = await request.json() + const { keys } = DeleteKeysSchema.parse(body) + + const deletedCount = await db + .delete(apiKey) + .where( + and( + eq(apiKey.workspaceId, workspaceId), + eq(apiKey.type, 'workspace'), + inArray(apiKey.id, keys) + ) + ) + + logger.info( + `[${requestId}] Deleted ${deletedCount} workspace API keys from workspace ${workspaceId}` + ) + return NextResponse.json({ success: true, deletedCount }) + } catch (error: unknown) { + logger.error(`[${requestId}] Workspace API key DELETE error`, error) + return NextResponse.json( + { error: error instanceof Error ? error.message : 'Failed to delete workspace API keys' }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/components/deploy-form/deploy-form.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/components/deploy-form/deploy-form.tsx index f60672092..34c2c70ce 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/components/deploy-form/deploy-form.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/components/deploy-form/deploy-form.tsx @@ -2,17 +2,20 @@ import { useEffect, useState } from 'react' import { zodResolver } from '@hookform/resolvers/zod' -import { Check, Copy, Loader2, Plus, X } from 'lucide-react' +import { Check, Copy, Loader2, Plus } from 'lucide-react' +import { useParams } from 'next/navigation' import { useForm } from 'react-hook-form' import { z } from 'zod' -import { Button } from '@/components/ui/button' import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, -} from '@/components/ui/dialog' + AlertDialog, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from '@/components/ui/alert-dialog' +import { Button } from '@/components/ui/button' import { Form, FormControl, @@ -22,15 +25,17 @@ import { FormMessage, } from '@/components/ui/form' import { Input } from '@/components/ui/input' -import { Label } from '@/components/ui/label' import { Select, SelectContent, + SelectGroup, SelectItem, + SelectLabel, SelectTrigger, SelectValue, } from '@/components/ui/select' import { createLogger } from '@/lib/logs/console/logger' +import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider' const logger = createLogger('DeployForm') @@ -38,9 +43,17 @@ interface ApiKey { id: string name: string key: string + displayKey?: string lastUsed?: string createdAt: string expiresAt?: string + createdBy?: string +} + +interface ApiKeysData { + workspace: ApiKey[] + personal: ApiKey[] + conflicts: string[] } // Form schema for API key selection or creation @@ -52,12 +65,9 @@ const deployFormSchema = z.object({ type DeployFormValues = z.infer interface DeployFormProps { - apiKeys: ApiKey[] + apiKeys: ApiKey[] // Legacy prop for backward compatibility keysLoaded: boolean - endpointUrl: string - workflowId: string onSubmit: (data: DeployFormValues) => void - getInputFormatExample: () => string onApiKeyCreated?: () => void // Optional id to bind an external submit button via the `form` attribute formId?: string @@ -66,45 +76,137 @@ interface DeployFormProps { export function DeployForm({ apiKeys, keysLoaded, - endpointUrl, - workflowId, onSubmit, - getInputFormatExample, onApiKeyCreated, formId, }: DeployFormProps) { + const params = useParams() + const workspaceId = (params?.workspaceId as string) || '' + const userPermissions = useUserPermissionsContext() + const canCreateWorkspaceKeys = userPermissions.canEdit || userPermissions.canAdmin + // State + const [apiKeysData, setApiKeysData] = useState(null) const [isCreatingKey, setIsCreatingKey] = useState(false) const [newKeyName, setNewKeyName] = useState('') + const [keyType, setKeyType] = useState<'personal' | 'workspace'>('personal') const [newKey, setNewKey] = useState(null) const [showNewKeyDialog, setShowNewKeyDialog] = useState(false) const [copySuccess, setCopySuccess] = useState(false) - const [isCreating, setIsCreating] = useState(false) + const [isSubmittingCreate, setIsSubmittingCreate] = useState(false) + const [keysLoaded2, setKeysLoaded2] = useState(false) + const [createError, setCreateError] = useState(null) + const [justCreatedKeyId, setJustCreatedKeyId] = useState(null) + + // Get all available API keys (workspace + personal) + const allApiKeys = apiKeysData ? [...apiKeysData.workspace, ...apiKeysData.personal] : apiKeys // Initialize form with react-hook-form const form = useForm({ resolver: zodResolver(deployFormSchema), defaultValues: { - apiKey: apiKeys.length > 0 ? apiKeys[0].key : '', + apiKey: allApiKeys.length > 0 ? allApiKeys[0].id : '', newKeyName: '', }, }) + // Fetch workspace and personal API keys + const fetchApiKeys = async () => { + if (!workspaceId) return + + try { + setKeysLoaded2(false) + const [workspaceResponse, personalResponse] = await Promise.all([ + fetch(`/api/workspaces/${workspaceId}/api-keys`), + fetch('/api/users/me/api-keys'), + ]) + + let workspaceKeys: ApiKey[] = [] + let personalKeys: ApiKey[] = [] + + if (workspaceResponse.ok) { + const workspaceData = await workspaceResponse.json() + workspaceKeys = workspaceData.keys || [] + } else { + logger.error('Error fetching workspace API keys:', { status: workspaceResponse.status }) + } + + if (personalResponse.ok) { + const personalData = await personalResponse.json() + personalKeys = personalData.keys || [] + } else { + logger.error('Error fetching personal API keys:', { status: personalResponse.status }) + } + + // Client-side conflict detection + const workspaceKeyNames = new Set(workspaceKeys.map((k) => k.name)) + const conflicts = personalKeys + .filter((key) => workspaceKeyNames.has(key.name)) + .map((key) => key.name) + + setApiKeysData({ + workspace: workspaceKeys, + personal: personalKeys, + conflicts, + }) + setKeysLoaded2(true) + } catch (error) { + logger.error('Error fetching API keys:', { error }) + setKeysLoaded2(true) + } + } + // Update on dependency changes beyond the initial load useEffect(() => { - if (keysLoaded && apiKeys.length > 0) { - // Ensure that form has a value after loading - form.setValue('apiKey', form.getValues().apiKey || apiKeys[0].key) + if (workspaceId) { + fetchApiKeys() } - }, [keysLoaded, apiKeys, form]) + }, [workspaceId]) + + useEffect(() => { + if ((keysLoaded || keysLoaded2) && allApiKeys.length > 0) { + const currentValue = form.getValues().apiKey + + // If we just created a key, prioritize selecting it + if (justCreatedKeyId && allApiKeys.find((key) => key.id === justCreatedKeyId)) { + form.setValue('apiKey', justCreatedKeyId) + setJustCreatedKeyId(null) // Clear after setting + } + // Otherwise, ensure form has a value if it doesn't already + else if (!currentValue || !allApiKeys.find((key) => key.id === currentValue)) { + form.setValue('apiKey', allApiKeys[0].id) + } + } + }, [keysLoaded, keysLoaded2, allApiKeys, form, justCreatedKeyId]) // Generate a new API key const handleCreateKey = async () => { if (!newKeyName.trim()) return - setIsCreating(true) + // Client-side duplicate check for immediate feedback + const trimmedName = newKeyName.trim() + const isDuplicate = + keyType === 'workspace' + ? (apiKeysData?.workspace || []).some((k) => k.name === trimmedName) + : (apiKeysData?.personal || apiKeys || []).some((k) => k.name === trimmedName) + if (isDuplicate) { + setCreateError( + keyType === 'workspace' + ? `A workspace API key named "${trimmedName}" already exists. Please choose a different name.` + : `A personal API key named "${trimmedName}" already exists. Please choose a different name.` + ) + return + } + + setIsSubmittingCreate(true) + setCreateError(null) try { - const response = await fetch('/api/users/me/api-keys', { + const url = + keyType === 'workspace' + ? `/api/workspaces/${workspaceId}/api-keys` + : '/api/users/me/api-keys' + + const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -114,30 +216,53 @@ export function DeployForm({ }), }) - if (!response.ok) { - throw new Error('Failed to create new API key') - } + if (response.ok) { + const data = await response.json() + // Show the new key dialog with the API key (only shown once) + setNewKey(data.key) + setShowNewKeyDialog(true) + // Reset form and close the create dialog ONLY on success + setNewKeyName('') + setKeyType('personal') + setCreateError(null) + setIsCreatingKey(false) - const data = await response.json() - // Show the new key dialog with the API key (only shown once) - setNewKey(data.key) - setShowNewKeyDialog(true) - // Reset form - setNewKeyName('') - // Close the create dialog - setIsCreatingKey(false) + // Store the newly created key ID for auto-selection + setJustCreatedKeyId(data.key.id) - // Update the form with the new key - form.setValue('apiKey', data.key.key) + // Refresh the keys list - the useEffect will handle auto-selection + await fetchApiKeys() - // Trigger a refresh of the keys list in the parent component - if (onApiKeyCreated) { - onApiKeyCreated() + // Trigger a refresh of the keys list in the parent component + if (onApiKeyCreated) { + onApiKeyCreated() + } + } else { + let errorData + try { + errorData = await response.json() + } catch (parseError) { + errorData = { error: 'Server error' } + } + // Check for duplicate name error and prefer server message + const serverMessage = typeof errorData?.error === 'string' ? errorData.error : null + if (response.status === 409 || serverMessage?.toLowerCase().includes('already exists')) { + setCreateError( + serverMessage || + (keyType === 'workspace' + ? `A workspace API key named "${trimmedName}" already exists. Please choose a different name.` + : `A personal API key named "${trimmedName}" already exists. Please choose a different name.`) + ) + } else { + setCreateError(errorData.error || 'Failed to create API key. Please try again.') + } + logger.error('Failed to create API key:', errorData) } } catch (error) { + setCreateError('Failed to create API key. Please check your connection and try again.') logger.error('Error creating API key:', { error }) } finally { - setIsCreating(false) + setIsSubmittingCreate(false) } } @@ -171,7 +296,10 @@ export function DeployForm({ variant='ghost' size='sm' className='h-7 gap-1 px-2 text-muted-foreground text-xs' - onClick={() => setIsCreatingKey(true)} + onClick={() => { + setIsCreatingKey(true) + setCreateError(null) + }} > Create new @@ -191,22 +319,68 @@ export function DeployForm({ - {apiKeys.map((apiKey) => ( - -
-
- {apiKey.name} - - {apiKey.key.slice(-5)} - -
+ {apiKeysData && apiKeysData.workspace.length > 0 && ( + + + Workspace + + {apiKeysData.workspace.map((apiKey) => ( + +
+
+ {apiKey.name} + + {apiKey.displayKey || apiKey.key} + +
+
+
+ ))} +
+ )} + + {((apiKeysData && apiKeysData.personal.length > 0) || + (!apiKeysData && apiKeys.length > 0)) && ( + + + Personal + + {(apiKeysData ? apiKeysData.personal : apiKeys).map((apiKey) => ( + +
+
+ {apiKey.name} + + {apiKey.displayKey || apiKey.key} + +
+
+
+ ))} +
+ )} + + {!apiKeysData && apiKeys.length === 0 && ( +
+ No API keys available +
+ )} + + {apiKeysData && + apiKeysData.workspace.length === 0 && + apiKeysData.personal.length === 0 && ( +
+ No API keys available
- - ))} + )} @@ -215,130 +389,145 @@ export function DeployForm({ /> {/* Create API Key Dialog */} - - - -
- Create new API key - -
-
+ + + + Create new API key + + {keyType === 'workspace' + ? "This key will have access to all workflows in this workspace. Make sure to copy it after creation as you won't be able to see it again." + : "This key will have access to your personal workflows. Make sure to copy it after creation as you won't be able to see it again."} + + -
+
+ {canCreateWorkspaceKeys && ( +
+

API Key Type

+
+ + +
+
+ )}
- +

+ Enter a name for your API key to help you identify it later. +

setNewKeyName(e.target.value)} - className='focus-visible:ring-primary' + onChange={(e) => { + setNewKeyName(e.target.value) + if (createError) setCreateError(null) // Clear error when user types + }} + placeholder='e.g., Development, Production' + className='h-9 rounded-[8px]' + autoFocus /> + {createError &&
{createError}
}
-
- - -
- -
+ + + {/* New API Key Dialog */} - { setShowNewKeyDialog(open) - if (!open) setNewKey(null) + if (!open) { + setNewKey(null) + setCopySuccess(false) + } }} > - - -
- - Your API key has been created - + + + Your API key has been created + + This is the only time you will see your API key.{' '} + Copy it now and store it securely. + + + + {newKey && ( +
+
+ + {newKey.key} + +
- - This is the only time you will see your API key. Copy it now and store it securely. - - - - {newKey && ( -
-
- -
- - -
-

- For security, we don't store the complete key. You won't be able to - view it again. -

-
-
)} - -
- -
- -
+ + ) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/components/deployment-info/components/api-key/api-key.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/components/deployment-info/components/api-key/api-key.tsx index 2bdbbfc0d..0651146c4 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/components/deployment-info/components/api-key/api-key.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/components/deployment-info/components/api-key/api-key.tsx @@ -1,7 +1,5 @@ 'use client' -import { useState } from 'react' -import { CopyButton } from '@/components/ui/copy-button' import { Label } from '@/components/ui/label' interface ApiKeyProps { @@ -10,15 +8,22 @@ interface ApiKeyProps { } export function ApiKey({ apiKey, showLabel = true }: ApiKeyProps) { - const [showKey, setShowKey] = useState(false) + // Extract key name and type from the API response format "Name (type)" + const getKeyInfo = (keyInfo: string) => { + if (!keyInfo || keyInfo.includes('No API key found')) { + return { name: keyInfo, type: null } + } - // Function to mask API key with asterisks but keep first and last 4 chars visible - const maskApiKey = (key: string) => { - if (!key || key.includes('No API key found')) return key - if (key.length <= 8) return key - return `${key.substring(0, 4)}${'*'.repeat(key.length - 8)}${key.substring(key.length - 4)}` + const match = keyInfo.match(/^(.*?)\s+\(([^)]+)\)$/) + if (match) { + return { name: match[1].trim(), type: match[2] } + } + + return { name: keyInfo, type: null } } + const { name, type } = getKeyInfo(apiKey) + return (
{showLabel && ( @@ -26,15 +31,17 @@ export function ApiKey({ apiKey, showLabel = true }: ApiKeyProps) {
)} -
-
 setShowKey(!showKey)}
-          title={showKey ? 'Click to hide API Key' : 'Click to reveal API Key'}
-        >
-          {showKey ? apiKey : maskApiKey(apiKey)}
-        
- +
+
+
{name}
+ {type && ( +
+ + {type} + +
+ )} +
) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/components/deployment-info/components/example-command/example-command.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/components/deployment-info/components/example-command/example-command.tsx index a7c5014cc..e233fada3 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/components/deployment-info/components/example-command/example-command.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/components/deployment-info/components/example-command/example-command.tsx @@ -40,7 +40,7 @@ export function ExampleCommand({ if (!command.includes('curl')) return command // Replace the actual API key with a placeholder in the command - const sanitizedCommand = command.replace(apiKey, 'SIM_API_KEY') + const sanitizedCommand = command.replace(apiKey, '$SIM_API_KEY') // Format the command with line breaks for better readability return sanitizedCommand @@ -49,50 +49,19 @@ export function ExampleCommand({ .replace(' http', '\n http') } - // Get the actual command with real API key for copying + // Get the command with placeholder for copying (single line, no line breaks) const getActualCommand = () => { - const baseEndpoint = endpoint - const inputExample = getInputFormatExample - ? getInputFormatExample() - : ' -d \'{"input": "your data here"}\'' - - switch (mode) { - case 'sync': - // Use the original command but ensure it has the real API key - return command - - case 'async': - switch (exampleType) { - case 'execute': - return `curl -X POST \\ - -H "X-API-Key: ${apiKey}" \\ - -H "Content-Type: application/json" \\ - -H "X-Execution-Mode: async"${inputExample} \\ - ${baseEndpoint}` - - case 'status': { - const baseUrl = baseEndpoint.split('/api/workflows/')[0] - return `curl -H "X-API-Key: ${apiKey}" \\ - ${baseUrl}/api/jobs/JOB_ID_FROM_EXECUTION` - } - - case 'rate-limits': { - const baseUrlForRateLimit = baseEndpoint.split('/api/workflows/')[0] - return `curl -H "X-API-Key: ${apiKey}" \\ - ${baseUrlForRateLimit}/api/users/me/usage-limits` - } - - default: - return command - } - - default: - return command - } + const displayCommand = getDisplayCommand() + // Remove line breaks and extra whitespace for copying + return displayCommand + .replace(/\\\n\s*/g, ' ') // Remove backslash + newline + whitespace + .replace(/\n\s*/g, ' ') // Remove any remaining newlines + whitespace + .replace(/\s+/g, ' ') // Normalize multiple spaces to single space + .trim() } const getDisplayCommand = () => { - const baseEndpoint = endpoint.replace(apiKey, 'SIM_API_KEY') + const baseEndpoint = endpoint.replace(apiKey, '$SIM_API_KEY') const inputExample = getInputFormatExample ? getInputFormatExample() : ' -d \'{"input": "your data here"}\'' @@ -105,20 +74,20 @@ export function ExampleCommand({ switch (exampleType) { case 'execute': return `curl -X POST \\ - -H "X-API-Key: SIM_API_KEY" \\ + -H "X-API-Key: $SIM_API_KEY" \\ -H "Content-Type: application/json" \\ -H "X-Execution-Mode: async"${inputExample} \\ ${baseEndpoint}` case 'status': { const baseUrl = baseEndpoint.split('/api/workflows/')[0] - return `curl -H "X-API-Key: SIM_API_KEY" \\ + return `curl -H "X-API-Key: $SIM_API_KEY" \\ ${baseUrl}/api/jobs/JOB_ID_FROM_EXECUTION` } case 'rate-limits': { const baseUrlForRateLimit = baseEndpoint.split('/api/workflows/')[0] - return `curl -H "X-API-Key: SIM_API_KEY" \\ + return `curl -H "X-API-Key: $SIM_API_KEY" \\ ${baseUrlForRateLimit}/api/users/me/usage-limits` } diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/deploy-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/deploy-modal.tsx index 86f5fdd3f..d589b78ce 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/deploy-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/deploy-modal/deploy-modal.tsx @@ -274,7 +274,7 @@ export function DeployModal({ setDeploymentInfo(newDeploymentInfo) await refetchDeployedState() - } catch (error: any) { + } catch (error: unknown) { logger.error('Error deploying workflow:', { error }) } finally { setIsSubmitting(false) @@ -297,7 +297,7 @@ export function DeployModal({ setDeploymentStatus(workflowId, false) setChatExists(false) onOpenChange(false) - } catch (error: any) { + } catch (error: unknown) { logger.error('Error undeploying workflow:', { error }) } finally { setIsUndeploying(false) @@ -341,7 +341,7 @@ export function DeployModal({ // Ensure modal status updates immediately setDeploymentInfo((prev) => (prev ? { ...prev, needsRedeployment: false } : prev)) - } catch (error: any) { + } catch (error: unknown) { logger.error('Error redeploying workflow:', { error }) } finally { setIsSubmitting(false) @@ -471,10 +471,7 @@ export function DeployModal({ diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/api-keys/api-keys.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/api-keys/api-keys.tsx index 23e7bcc28..60b344180 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/api-keys/api-keys.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/api-keys/api-keys.tsx @@ -1,7 +1,8 @@ 'use client' -import { useEffect, useState } from 'react' +import { useEffect, useMemo, useRef, useState } from 'react' import { Check, Copy, Plus, Search } from 'lucide-react' +import { useParams } from 'next/navigation' import { AlertDialog, AlertDialogAction, @@ -18,29 +19,54 @@ import { Label } from '@/components/ui/label' import { Skeleton } from '@/components/ui/skeleton' import { useSession } from '@/lib/auth-client' import { createLogger } from '@/lib/logs/console/logger' +import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider' const logger = createLogger('ApiKeys') interface ApiKeysProps { onOpenChange?: (open: boolean) => void + registerCloseHandler?: (handler: (open: boolean) => void) => void } interface ApiKey { id: string name: string key: string + displayKey?: string lastUsed?: string createdAt: string expiresAt?: string + createdBy?: string } -export function ApiKeys({ onOpenChange }: ApiKeysProps) { +interface ApiKeyDisplayProps { + apiKey: ApiKey +} + +function ApiKeyDisplay({ apiKey }: ApiKeyDisplayProps) { + const displayValue = apiKey.displayKey || apiKey.key + return ( +
+ {displayValue} +
+ ) +} + +export function ApiKeys({ onOpenChange, registerCloseHandler }: ApiKeysProps) { const { data: session } = useSession() const userId = session?.user?.id + const params = useParams() + const workspaceId = (params?.workspaceId as string) || '' + const userPermissions = useUserPermissionsContext() + const canManageWorkspaceKeys = userPermissions.canEdit || userPermissions.canAdmin - const [apiKeys, setApiKeys] = useState([]) + // State for both workspace and personal keys + const [workspaceKeys, setWorkspaceKeys] = useState([]) + const [personalKeys, setPersonalKeys] = useState([]) + const [conflicts, setConflicts] = useState([]) const [isLoading, setIsLoading] = useState(true) - const [isCreating, setIsCreating] = useState(false) + const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false) + const [isSubmittingCreate, setIsSubmittingCreate] = useState(false) const [newKeyName, setNewKeyName] = useState('') const [newKey, setNewKey] = useState(null) const [showNewKeyDialog, setShowNewKeyDialog] = useState(false) @@ -49,23 +75,71 @@ export function ApiKeys({ onOpenChange }: ApiKeysProps) { const [copySuccess, setCopySuccess] = useState(false) const [searchTerm, setSearchTerm] = useState('') const [deleteConfirmationName, setDeleteConfirmationName] = useState('') + const [keyType, setKeyType] = useState<'personal' | 'workspace'>('personal') + const [shouldScrollToBottom, setShouldScrollToBottom] = useState(false) + const [createError, setCreateError] = useState(null) - // Filter API keys based on search term - const filteredApiKeys = apiKeys.filter((key) => - key.name.toLowerCase().includes(searchTerm.toLowerCase()) - ) + const scrollContainerRef = useRef(null) + + const filteredWorkspaceKeys = useMemo(() => { + if (!searchTerm.trim()) { + return workspaceKeys.map((key, index) => ({ key, originalIndex: index })) + } + return workspaceKeys + .map((key, index) => ({ key, originalIndex: index })) + .filter(({ key }) => key.name.toLowerCase().includes(searchTerm.toLowerCase())) + }, [workspaceKeys, searchTerm]) + + const filteredPersonalKeys = useMemo(() => { + if (!searchTerm.trim()) { + return personalKeys.map((key, index) => ({ key, originalIndex: index })) + } + return personalKeys + .map((key, index) => ({ key, originalIndex: index })) + .filter(({ key }) => key.name.toLowerCase().includes(searchTerm.toLowerCase())) + }, [personalKeys, searchTerm]) + + const personalHeaderMarginClass = useMemo(() => { + if (!searchTerm.trim()) return 'mt-8' + return filteredWorkspaceKeys.length > 0 ? 'mt-8' : 'mt-0' + }, [searchTerm, filteredWorkspaceKeys]) - // Fetch API keys const fetchApiKeys = async () => { - if (!userId) return + if (!userId || !workspaceId) return setIsLoading(true) try { - const response = await fetch('/api/users/me/api-keys') - if (response.ok) { - const data = await response.json() - setApiKeys(data.keys || []) + const [workspaceResponse, personalResponse] = await Promise.all([ + fetch(`/api/workspaces/${workspaceId}/api-keys`), + fetch('/api/users/me/api-keys'), + ]) + + let workspaceKeys: ApiKey[] = [] + let personalKeys: ApiKey[] = [] + + if (workspaceResponse.ok) { + const workspaceData = await workspaceResponse.json() + workspaceKeys = workspaceData.keys || [] + } else { + logger.error('Failed to fetch workspace API keys:', { status: workspaceResponse.status }) } + + if (personalResponse.ok) { + const personalData = await personalResponse.json() + personalKeys = personalData.keys || [] + } else { + logger.error('Failed to fetch personal API keys:', { status: personalResponse.status }) + } + + // Client-side conflict detection + const workspaceKeyNames = new Set(workspaceKeys.map((k) => k.name)) + const conflicts = personalKeys + .filter((key) => workspaceKeyNames.has(key.name)) + .map((key) => key.name) + + setWorkspaceKeys(workspaceKeys) + setPersonalKeys(personalKeys) + setConflicts(conflicts) } catch (error) { logger.error('Error fetching API keys:', { error }) } finally { @@ -73,13 +147,32 @@ export function ApiKeys({ onOpenChange }: ApiKeysProps) { } } - // Generate a new API key const handleCreateKey = async () => { if (!userId || !newKeyName.trim()) return - setIsCreating(true) + const trimmedName = newKeyName.trim() + const isDuplicate = + keyType === 'workspace' + ? workspaceKeys.some((k) => k.name === trimmedName) + : personalKeys.some((k) => k.name === trimmedName) + if (isDuplicate) { + setCreateError( + keyType === 'workspace' + ? `A workspace API key named "${trimmedName}" already exists. Please choose a different name.` + : `A personal API key named "${trimmedName}" already exists. Please choose a different name.` + ) + return + } + + setIsSubmittingCreate(true) + setCreateError(null) try { - const response = await fetch('/api/users/me/api-keys', { + const url = + keyType === 'workspace' + ? `/api/workspaces/${workspaceId}/api-keys` + : '/api/users/me/api-keys' + + const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -91,57 +184,105 @@ export function ApiKeys({ onOpenChange }: ApiKeysProps) { if (response.ok) { const data = await response.json() - // Show the new key dialog with the API key (only shown once) setNewKey(data.key) setShowNewKeyDialog(true) - // Refresh the keys list fetchApiKeys() - // Close the create dialog - setIsCreating(false) + setNewKeyName('') + setKeyType('personal') + setCreateError(null) + setIsSubmittingCreate(false) + setIsCreateDialogOpen(false) + } else { + let errorData + try { + errorData = await response.json() + } catch (parseError) { + logger.error('Error parsing API response:', parseError) + errorData = { error: 'Server error' } + } + + logger.error('API key creation failed:', { status: response.status, errorData }) + + const serverMessage = typeof errorData?.error === 'string' ? errorData.error : null + if (response.status === 409 || serverMessage?.toLowerCase().includes('already exists')) { + const errorMessage = + serverMessage || + (keyType === 'workspace' + ? `A workspace API key named "${trimmedName}" already exists. Please choose a different name.` + : `A personal API key named "${trimmedName}" already exists. Please choose a different name.`) + logger.error('Setting error message:', errorMessage) + setCreateError(errorMessage) + } else { + setCreateError(errorData.error || 'Failed to create API key. Please try again.') + } } } catch (error) { + setCreateError('Failed to create API key. Please check your connection and try again.') logger.error('Error creating API key:', { error }) } finally { - setIsCreating(false) + setIsSubmittingCreate(false) } } - // Delete an API key const handleDeleteKey = async () => { if (!userId || !deleteKey) return try { - const response = await fetch(`/api/users/me/api-keys/${deleteKey.id}`, { + const isWorkspaceKey = workspaceKeys.some((k) => k.id === deleteKey.id) + const url = isWorkspaceKey + ? `/api/workspaces/${workspaceId}/api-keys/${deleteKey.id}` + : `/api/users/me/api-keys/${deleteKey.id}` + + const response = await fetch(url, { method: 'DELETE', }) if (response.ok) { - // Refresh the keys list fetchApiKeys() - // Close the dialog setShowDeleteDialog(false) setDeleteKey(null) + setDeleteConfirmationName('') + } else { + const errorData = await response.json() + logger.error('Failed to delete API key:', errorData) } } catch (error) { logger.error('Error deleting API key:', { error }) } } - // Copy API key to clipboard const copyToClipboard = (key: string) => { navigator.clipboard.writeText(key) setCopySuccess(true) setTimeout(() => setCopySuccess(false), 2000) } - // Load API keys on mount + const handleModalClose = (open: boolean) => { + onOpenChange?.(open) + } + useEffect(() => { - if (userId) { + if (userId && workspaceId) { fetchApiKeys() } - }, [userId]) + }, [userId, workspaceId]) + + useEffect(() => { + if (registerCloseHandler) { + registerCloseHandler(handleModalClose) + } + }, [registerCloseHandler]) + + useEffect(() => { + if (shouldScrollToBottom && scrollContainerRef.current) { + scrollContainerRef.current.scrollTo({ + top: scrollContainerRef.current.scrollHeight, + behavior: 'smooth', + }) + setShouldScrollToBottom(false) + } + }, [shouldScrollToBottom]) - // Format date const formatDate = (dateString?: string) => { if (!dateString) return 'Never' return new Date(dateString).toLocaleDateString('en-US', { @@ -172,7 +313,10 @@ export function ApiKeys({ onOpenChange }: ApiKeysProps) {
{/* Scrollable Content */} -
+
{isLoading ? (
@@ -180,50 +324,136 @@ export function ApiKeys({ onOpenChange }: ApiKeysProps) {
- ) : apiKeys.length === 0 ? ( + ) : personalKeys.length === 0 && workspaceKeys.length === 0 ? (
Click "Create Key" below to get started
) : ( -
- {filteredApiKeys.map((key) => ( -
- -
-
-
- - •••••{key.key.slice(-6)} - + <> + {/* Workspace section */} + {!searchTerm.trim() ? ( +
+
Workspace
+ {workspaceKeys.length === 0 ? ( +
No workspace API keys yet.
+ ) : ( + workspaceKeys.map((key) => ( +
+ +
+
+ +

+ Last used: {formatDate(key.lastUsed)} +

+
+
+ +
+
+
+ )) + )} +
+ ) : filteredWorkspaceKeys.length > 0 ? ( +
+
Workspace
+ {filteredWorkspaceKeys.map(({ key }) => ( +
+ +
+
+ +

+ Last used: {formatDate(key.lastUsed)} +

+
+
-

- Last used: {formatDate(key.lastUsed)} -

+ ))} +
+ ) : null} - + {/* Personal section */} +
+ Personal +
+ {filteredPersonalKeys.map(({ key }) => { + const isConflict = conflicts.includes(key.name) + return ( +
+ +
+
+ +

+ Last used: {formatDate(key.lastUsed)} +

+
+
+ +
+
+ {isConflict && ( +
+ Workspace API key with the same name overrides this. Rename your personal + key to use it. +
+ )}
-
- ))} - {/* Show message when search has no results but there are keys */} - {searchTerm.trim() && filteredApiKeys.length === 0 && apiKeys.length > 0 && ( -
- No API keys found matching "{searchTerm}" -
- )} -
+ ) + })} + {/* Show message when search has no results across both sections */} + {searchTerm.trim() && + filteredPersonalKeys.length === 0 && + filteredWorkspaceKeys.length === 0 && + (personalKeys.length > 0 || workspaceKeys.length > 0) && ( +
+ No API keys found matching "{searchTerm}" +
+ )} + )}
@@ -239,60 +469,106 @@ export function ApiKeys({ onOpenChange }: ApiKeysProps) { ) : ( <> -
Keep your API keys secure
)}
{/* Create API Key Dialog */} - + Create new API key - This key will have access to your account and workflows. Make sure to copy it after - creation as you won't be able to see it again. + {keyType === 'workspace' + ? "This key will have access to all workflows in this workspace. Make sure to copy it after creation as you won't be able to see it again." + : "This key will have access to your personal workflows. Make sure to copy it after creation as you won't be able to see it again."} -
-

- Enter a name for your API key to help you identify it later. -

- setNewKeyName(e.target.value)} - placeholder='e.g., Development, Production' - className='h-9 rounded-[8px]' - autoFocus - /> +
+ {canManageWorkspaceKeys && ( +
+

API Key Type

+
+ + +
+
+ )} +
+

+ Enter a name for your API key to help you identify it later. +

+ { + setNewKeyName(e.target.value) + if (createError) setCreateError(null) // Clear error when user types + }} + placeholder='e.g., Development, Production' + className='h-9 rounded-[8px]' + autoFocus + /> + {createError &&
{createError}
} +
setNewKeyName('')} + onClick={() => { + setNewKeyName('') + setKeyType('personal') + }} > Cancel - { - handleCreateKey() - setNewKeyName('') - }} - className='h-9 w-full rounded-[8px] bg-primary text-muted-foreground transition-all duration-200 hover:bg-primary/90' - disabled={!newKeyName.trim()} + @@ -392,7 +668,6 @@ export function ApiKeys({ onOpenChange }: ApiKeysProps) { ) } -// Loading skeleton for API keys function ApiKeySkeleton() { return (
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/copilot/copilot.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/copilot/copilot.tsx index 32315f6a4..918a380d0 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/copilot/copilot.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/copilot/copilot.tsx @@ -267,7 +267,6 @@ export function Copilot() { Generate Key -
Keep your API keys secure
)}
diff --git a/apps/sim/db/migrations/0089_amused_pete_wisdom.sql b/apps/sim/db/migrations/0089_amused_pete_wisdom.sql new file mode 100644 index 000000000..0f92cadf1 --- /dev/null +++ b/apps/sim/db/migrations/0089_amused_pete_wisdom.sql @@ -0,0 +1,25 @@ +-- Step 1: Add new columns to api_key table +ALTER TABLE "api_key" ADD COLUMN "workspace_id" text;--> statement-breakpoint +ALTER TABLE "api_key" ADD COLUMN "created_by" text;--> statement-breakpoint +ALTER TABLE "api_key" ADD COLUMN "type" text DEFAULT 'personal' NOT NULL;--> statement-breakpoint + +-- Step 2: Add pinned_api_key_id column to workflow table +ALTER TABLE "workflow" ADD COLUMN "pinned_api_key_id" text;--> statement-breakpoint + +-- Step 3: Migrate pinned API key references from text key to foreign key ID +UPDATE "workflow" +SET "pinned_api_key_id" = ak."id" +FROM "api_key" ak +WHERE "workflow"."pinned_api_key" IS NOT NULL + AND ak."key" = "workflow"."pinned_api_key";--> statement-breakpoint + +-- Step 4: Add foreign key constraints +ALTER TABLE "api_key" ADD CONSTRAINT "api_key_workspace_id_workspace_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspace"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "api_key" ADD CONSTRAINT "api_key_created_by_user_id_fk" FOREIGN KEY ("created_by") REFERENCES "public"."user"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "workflow" ADD CONSTRAINT "workflow_pinned_api_key_id_api_key_id_fk" FOREIGN KEY ("pinned_api_key_id") REFERENCES "public"."api_key"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint + +-- Step 5: Add check constraint to ensure data integrity +ALTER TABLE "api_key" ADD CONSTRAINT "workspace_type_check" CHECK ((type = 'workspace' AND workspace_id IS NOT NULL) OR (type = 'personal' AND workspace_id IS NULL));--> statement-breakpoint + +-- Step 6: Drop old columns +ALTER TABLE "workflow" DROP COLUMN "pinned_api_key"; \ No newline at end of file diff --git a/apps/sim/db/migrations/meta/0089_snapshot.json b/apps/sim/db/migrations/meta/0089_snapshot.json new file mode 100644 index 000000000..2b0f67f09 --- /dev/null +++ b/apps/sim/db/migrations/meta/0089_snapshot.json @@ -0,0 +1,6754 @@ +{ + "id": "687bfe13-ff4a-4c5a-a3ef-b2674b31485d", + "prevId": "8f0deaa8-71e7-4202-89e1-a0e077be47d5", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_user_id_idx": { + "name": "account_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_key": { + "name": "api_key", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'personal'" + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "api_key_user_id_user_id_fk": { + "name": "api_key_user_id_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_key_workspace_id_workspace_id_fk": { + "name": "api_key_workspace_id_workspace_id_fk", + "tableFrom": "api_key", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_key_created_by_user_id_fk": { + "name": "api_key_created_by_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "api_key_key_unique": { + "name": "api_key_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "policies": {}, + "checkConstraints": { + "workspace_type_check": { + "name": "workspace_type_check", + "value": "(type = 'workspace' AND workspace_id IS NOT NULL) OR (type = 'personal' AND workspace_id IS NULL)" + } + }, + "isRLSEnabled": false + }, + "public.chat": { + "name": "chat", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subdomain": { + "name": "subdomain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "customizations": { + "name": "customizations", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_emails": { + "name": "allowed_emails", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "output_configs": { + "name": "output_configs", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "subdomain_idx": { + "name": "subdomain_idx", + "columns": [ + { + "expression": "subdomain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_workflow_id_workflow_id_fk": { + "name": "chat_workflow_id_workflow_id_fk", + "tableFrom": "chat", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_user_id_user_id_fk": { + "name": "chat_user_id_user_id_fk", + "tableFrom": "chat", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_chats": { + "name": "copilot_chats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "messages": { + "name": "messages", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'claude-3-7-sonnet-latest'" + }, + "conversation_id": { + "name": "conversation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "preview_yaml": { + "name": "preview_yaml", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_chats_user_id_idx": { + "name": "copilot_chats_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_workflow_id_idx": { + "name": "copilot_chats_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_workflow_idx": { + "name": "copilot_chats_user_workflow_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_created_at_idx": { + "name": "copilot_chats_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_updated_at_idx": { + "name": "copilot_chats_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_chats_user_id_user_id_fk": { + "name": "copilot_chats_user_id_user_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_chats_workflow_id_workflow_id_fk": { + "name": "copilot_chats_workflow_id_workflow_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_feedback": { + "name": "copilot_feedback", + "schema": "", + "columns": { + "feedback_id": { + "name": "feedback_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_query": { + "name": "user_query", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_response": { + "name": "agent_response", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_positive": { + "name": "is_positive", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "feedback": { + "name": "feedback", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_yaml": { + "name": "workflow_yaml", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_feedback_user_id_idx": { + "name": "copilot_feedback_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_chat_id_idx": { + "name": "copilot_feedback_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_user_chat_idx": { + "name": "copilot_feedback_user_chat_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_is_positive_idx": { + "name": "copilot_feedback_is_positive_idx", + "columns": [ + { + "expression": "is_positive", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_created_at_idx": { + "name": "copilot_feedback_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_feedback_user_id_user_id_fk": { + "name": "copilot_feedback_user_id_user_id_fk", + "tableFrom": "copilot_feedback", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_feedback_chat_id_copilot_chats_id_fk": { + "name": "copilot_feedback_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_feedback", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_tools": { + "name": "custom_tools", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema": { + "name": "schema", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "custom_tools_user_id_user_id_fk": { + "name": "custom_tools_user_id_user_id_fk", + "tableFrom": "custom_tools", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.docs_embeddings": { + "name": "docs_embeddings", + "schema": "", + "columns": { + "chunk_id": { + "name": "chunk_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chunk_text": { + "name": "chunk_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_document": { + "name": "source_document", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_link": { + "name": "source_link", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_text": { + "name": "header_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_level": { + "name": "header_level", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": true + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "chunk_text_tsv": { + "name": "chunk_text_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"docs_embeddings\".\"chunk_text\")", + "type": "stored" + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "docs_emb_source_document_idx": { + "name": "docs_emb_source_document_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_header_level_idx": { + "name": "docs_emb_header_level_idx", + "columns": [ + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_source_header_idx": { + "name": "docs_emb_source_header_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_model_idx": { + "name": "docs_emb_model_idx", + "columns": [ + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_created_at_idx": { + "name": "docs_emb_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_embedding_vector_hnsw_idx": { + "name": "docs_embedding_vector_hnsw_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "docs_emb_metadata_gin_idx": { + "name": "docs_emb_metadata_gin_idx", + "columns": [ + { + "expression": "metadata", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "docs_emb_chunk_text_fts_idx": { + "name": "docs_emb_chunk_text_fts_idx", + "columns": [ + { + "expression": "chunk_text_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "docs_embedding_not_null_check": { + "name": "docs_embedding_not_null_check", + "value": "\"embedding\" IS NOT NULL" + }, + "docs_header_level_check": { + "name": "docs_header_level_check", + "value": "\"header_level\" >= 1 AND \"header_level\" <= 6" + } + }, + "isRLSEnabled": false + }, + "public.document": { + "name": "document", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_url": { + "name": "file_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_size": { + "name": "file_size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_count": { + "name": "chunk_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "character_count": { + "name": "character_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "processing_status": { + "name": "processing_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_completed_at": { + "name": "processing_completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_error": { + "name": "processing_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "doc_kb_id_idx": { + "name": "doc_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_filename_idx": { + "name": "doc_filename_idx", + "columns": [ + { + "expression": "filename", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_kb_uploaded_at_idx": { + "name": "doc_kb_uploaded_at_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "uploaded_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_processing_status_idx": { + "name": "doc_processing_status_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "processing_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag1_idx": { + "name": "doc_tag1_idx", + "columns": [ + { + "expression": "tag1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag2_idx": { + "name": "doc_tag2_idx", + "columns": [ + { + "expression": "tag2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag3_idx": { + "name": "doc_tag3_idx", + "columns": [ + { + "expression": "tag3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag4_idx": { + "name": "doc_tag4_idx", + "columns": [ + { + "expression": "tag4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag5_idx": { + "name": "doc_tag5_idx", + "columns": [ + { + "expression": "tag5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag6_idx": { + "name": "doc_tag6_idx", + "columns": [ + { + "expression": "tag6", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag7_idx": { + "name": "doc_tag7_idx", + "columns": [ + { + "expression": "tag7", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "document_knowledge_base_id_knowledge_base_id_fk": { + "name": "document_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "document", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.embedding": { + "name": "embedding", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_index": { + "name": "chunk_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "chunk_hash": { + "name": "chunk_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_length": { + "name": "content_length", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "start_offset": { + "name": "start_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "end_offset": { + "name": "end_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "content_tsv": { + "name": "content_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"embedding\".\"content\")", + "type": "stored" + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "emb_kb_id_idx": { + "name": "emb_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_id_idx": { + "name": "emb_doc_id_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_chunk_idx": { + "name": "emb_doc_chunk_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chunk_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_model_idx": { + "name": "emb_kb_model_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_enabled_idx": { + "name": "emb_kb_enabled_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_enabled_idx": { + "name": "emb_doc_enabled_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "embedding_vector_hnsw_idx": { + "name": "embedding_vector_hnsw_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "emb_tag1_idx": { + "name": "emb_tag1_idx", + "columns": [ + { + "expression": "tag1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag2_idx": { + "name": "emb_tag2_idx", + "columns": [ + { + "expression": "tag2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag3_idx": { + "name": "emb_tag3_idx", + "columns": [ + { + "expression": "tag3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag4_idx": { + "name": "emb_tag4_idx", + "columns": [ + { + "expression": "tag4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag5_idx": { + "name": "emb_tag5_idx", + "columns": [ + { + "expression": "tag5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag6_idx": { + "name": "emb_tag6_idx", + "columns": [ + { + "expression": "tag6", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag7_idx": { + "name": "emb_tag7_idx", + "columns": [ + { + "expression": "tag7", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_content_fts_idx": { + "name": "emb_content_fts_idx", + "columns": [ + { + "expression": "content_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "embedding_knowledge_base_id_knowledge_base_id_fk": { + "name": "embedding_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "embedding", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "embedding_document_id_document_id_fk": { + "name": "embedding_document_id_document_id_fk", + "tableFrom": "embedding", + "tableTo": "document", + "columnsFrom": ["document_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "embedding_not_null_check": { + "name": "embedding_not_null_check", + "value": "\"embedding\" IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.environment": { + "name": "environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "environment_user_id_user_id_fk": { + "name": "environment_user_id_user_id_fk", + "tableFrom": "environment", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "environment_user_id_unique": { + "name": "environment_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "invitation_email_idx": { + "name": "invitation_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_organization_id_idx": { + "name": "invitation_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": ["inviter_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base": { + "name": "knowledge_base", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "embedding_dimension": { + "name": "embedding_dimension", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1536 + }, + "chunking_config": { + "name": "chunking_config", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{\"maxSize\": 1024, \"minSize\": 1, \"overlap\": 200}'" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kb_user_id_idx": { + "name": "kb_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_id_idx": { + "name": "kb_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_user_workspace_idx": { + "name": "kb_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_deleted_at_idx": { + "name": "kb_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_user_id_user_id_fk": { + "name": "knowledge_base_user_id_user_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_base_workspace_id_workspace_id_fk": { + "name": "knowledge_base_workspace_id_workspace_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base_tag_definitions": { + "name": "knowledge_base_tag_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag_slot": { + "name": "tag_slot", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "field_type": { + "name": "field_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kb_tag_definitions_kb_slot_idx": { + "name": "kb_tag_definitions_kb_slot_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tag_slot", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_display_name_idx": { + "name": "kb_tag_definitions_kb_display_name_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "display_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_id_idx": { + "name": "kb_tag_definitions_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk": { + "name": "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "knowledge_base_tag_definitions", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.marketplace": { + "name": "marketplace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "author_id": { + "name": "author_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_name": { + "name": "author_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "views": { + "name": "views", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "marketplace_workflow_id_workflow_id_fk": { + "name": "marketplace_workflow_id_workflow_id_fk", + "tableFrom": "marketplace", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "marketplace_author_id_user_id_fk": { + "name": "marketplace_author_id_user_id_fk", + "tableFrom": "marketplace", + "tableTo": "user", + "columnsFrom": ["author_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_servers": { + "name": "mcp_servers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "transport": { + "name": "transport", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "headers": { + "name": "headers", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "timeout": { + "name": "timeout", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 30000 + }, + "retries": { + "name": "retries", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3 + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "last_connected": { + "name": "last_connected", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "connection_status": { + "name": "connection_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'disconnected'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tool_count": { + "name": "tool_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_tools_refresh": { + "name": "last_tools_refresh", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_requests": { + "name": "total_requests", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mcp_servers_workspace_enabled_idx": { + "name": "mcp_servers_workspace_enabled_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_servers_workspace_deleted_idx": { + "name": "mcp_servers_workspace_deleted_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_servers_workspace_id_workspace_id_fk": { + "name": "mcp_servers_workspace_id_workspace_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_servers_created_by_user_id_fk": { + "name": "mcp_servers_created_by_user_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.member": { + "name": "member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "member_user_id_idx": { + "name": "member_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "member_organization_id_idx": { + "name": "member_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.memory": { + "name": "memory", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "memory_key_idx": { + "name": "memory_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workflow_idx": { + "name": "memory_workflow_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workflow_key_idx": { + "name": "memory_workflow_key_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "memory_workflow_id_workflow_id_fk": { + "name": "memory_workflow_id_workflow_id_fk", + "tableFrom": "memory", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "org_usage_limit": { + "name": "org_usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permissions": { + "name": "permissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission_type": { + "name": "permission_type", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permissions_user_id_idx": { + "name": "permissions_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_entity_idx": { + "name": "permissions_entity_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_type_idx": { + "name": "permissions_user_entity_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_permission_idx": { + "name": "permissions_user_entity_permission_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_idx": { + "name": "permissions_user_entity_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_unique_constraint": { + "name": "permissions_unique_constraint", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permissions_user_id_user_id_fk": { + "name": "permissions_user_id_user_id_fk", + "tableFrom": "permissions", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "session_user_id_idx": { + "name": "session_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "session_token_idx": { + "name": "session_token_idx", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_active_organization_id_organization_id_fk": { + "name": "session_active_organization_id_organization_id_fk", + "tableFrom": "session", + "tableTo": "organization", + "columnsFrom": ["active_organization_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'system'" + }, + "auto_connect": { + "name": "auto_connect", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "auto_fill_env_vars": { + "name": "auto_fill_env_vars", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "auto_pan": { + "name": "auto_pan", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "console_expanded_by_default": { + "name": "console_expanded_by_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "telemetry_enabled": { + "name": "telemetry_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "email_preferences": { + "name": "email_preferences", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "billing_usage_notifications_enabled": { + "name": "billing_usage_notifications_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "settings_user_id_user_id_fk": { + "name": "settings_user_id_user_id_fk", + "tableFrom": "settings", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "settings_user_id_unique": { + "name": "settings_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.subscription": { + "name": "subscription", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "period_start": { + "name": "period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "period_end": { + "name": "period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancel_at_period_end": { + "name": "cancel_at_period_end", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "seats": { + "name": "seats", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "trial_start": { + "name": "trial_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trial_end": { + "name": "trial_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "subscription_reference_status_idx": { + "name": "subscription_reference_status_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "check_enterprise_metadata": { + "name": "check_enterprise_metadata", + "value": "plan != 'enterprise' OR metadata IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.template_stars": { + "name": "template_stars", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "template_id": { + "name": "template_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "starred_at": { + "name": "starred_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "template_stars_user_id_idx": { + "name": "template_stars_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_template_id_idx": { + "name": "template_stars_template_id_idx", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_user_template_idx": { + "name": "template_stars_user_template_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_template_user_idx": { + "name": "template_stars_template_user_idx", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_starred_at_idx": { + "name": "template_stars_starred_at_idx", + "columns": [ + { + "expression": "starred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_template_starred_at_idx": { + "name": "template_stars_template_starred_at_idx", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "starred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_user_template_unique": { + "name": "template_stars_user_template_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "template_stars_user_id_user_id_fk": { + "name": "template_stars_user_id_user_id_fk", + "tableFrom": "template_stars", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "template_stars_template_id_templates_id_fk": { + "name": "template_stars_template_id_templates_id_fk", + "tableFrom": "template_stars", + "tableTo": "templates", + "columnsFrom": ["template_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.templates": { + "name": "templates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "author": { + "name": "author", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "views": { + "name": "views", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "stars": { + "name": "stars", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#3972F6'" + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'FileText'" + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "templates_workflow_id_idx": { + "name": "templates_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_user_id_idx": { + "name": "templates_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_category_idx": { + "name": "templates_category_idx", + "columns": [ + { + "expression": "category", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_views_idx": { + "name": "templates_views_idx", + "columns": [ + { + "expression": "views", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_stars_idx": { + "name": "templates_stars_idx", + "columns": [ + { + "expression": "stars", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_category_views_idx": { + "name": "templates_category_views_idx", + "columns": [ + { + "expression": "category", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "views", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_category_stars_idx": { + "name": "templates_category_stars_idx", + "columns": [ + { + "expression": "category", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stars", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_user_category_idx": { + "name": "templates_user_category_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "category", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_created_at_idx": { + "name": "templates_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_updated_at_idx": { + "name": "templates_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "templates_workflow_id_workflow_id_fk": { + "name": "templates_workflow_id_workflow_id_fk", + "tableFrom": "templates", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "templates_user_id_user_id_fk": { + "name": "templates_user_id_user_id_fk", + "tableFrom": "templates", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_rate_limits": { + "name": "user_rate_limits", + "schema": "", + "columns": { + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "sync_api_requests": { + "name": "sync_api_requests", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "async_api_requests": { + "name": "async_api_requests", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "api_endpoint_requests": { + "name": "api_endpoint_requests", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "window_start": { + "name": "window_start", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_request_at": { + "name": "last_request_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "is_rate_limited": { + "name": "is_rate_limited", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "rate_limit_reset_at": { + "name": "rate_limit_reset_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_stats": { + "name": "user_stats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "total_manual_executions": { + "name": "total_manual_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_api_calls": { + "name": "total_api_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_webhook_triggers": { + "name": "total_webhook_triggers", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_scheduled_executions": { + "name": "total_scheduled_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_chat_executions": { + "name": "total_chat_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_tokens_used": { + "name": "total_tokens_used", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_cost": { + "name": "total_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_usage_limit": { + "name": "current_usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'10'" + }, + "usage_limit_updated_at": { + "name": "usage_limit_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "current_period_cost": { + "name": "current_period_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "last_period_cost": { + "name": "last_period_cost", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "total_copilot_cost": { + "name": "total_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "total_copilot_tokens": { + "name": "total_copilot_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_copilot_calls": { + "name": "total_copilot_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_active": { + "name": "last_active", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "billing_blocked": { + "name": "billing_blocked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_stats_user_id_user_id_fk": { + "name": "user_stats_user_id_user_id_fk", + "tableFrom": "user_stats", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_stats_user_id_unique": { + "name": "user_stats_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.waitlist": { + "name": "waitlist", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "waitlist_email_unique": { + "name": "waitlist_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook": { + "name": "webhook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_config": { + "name": "provider_config", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "path_idx": { + "name": "path_idx", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_workflow_id_workflow_id_fk": { + "name": "webhook_workflow_id_workflow_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webhook_block_id_workflow_blocks_id_fk": { + "name": "webhook_block_id_workflow_blocks_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow_blocks", + "columnsFrom": ["block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow": { + "name": "workflow", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#3972F6'" + }, + "last_synced": { + "name": "last_synced", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "is_deployed": { + "name": "is_deployed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deployed_state": { + "name": "deployed_state", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "deployed_at": { + "name": "deployed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "pinned_api_key_id": { + "name": "pinned_api_key_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "collaborators": { + "name": "collaborators", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "run_count": { + "name": "run_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "is_published": { + "name": "is_published", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "marketplace_data": { + "name": "marketplace_data", + "type": "json", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_user_id_idx": { + "name": "workflow_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_id_idx": { + "name": "workflow_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_user_workspace_idx": { + "name": "workflow_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_user_id_user_id_fk": { + "name": "workflow_user_id_user_id_fk", + "tableFrom": "workflow", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_workspace_id_workspace_id_fk": { + "name": "workflow_workspace_id_workspace_id_fk", + "tableFrom": "workflow", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_folder_id_workflow_folder_id_fk": { + "name": "workflow_folder_id_workflow_folder_id_fk", + "tableFrom": "workflow", + "tableTo": "workflow_folder", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workflow_pinned_api_key_id_api_key_id_fk": { + "name": "workflow_pinned_api_key_id_api_key_id_fk", + "tableFrom": "workflow", + "tableTo": "api_key", + "columnsFrom": ["pinned_api_key_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_blocks": { + "name": "workflow_blocks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position_x": { + "name": "position_x", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "position_y": { + "name": "position_y", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "horizontal_handles": { + "name": "horizontal_handles", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "is_wide": { + "name": "is_wide", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "advanced_mode": { + "name": "advanced_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "trigger_mode": { + "name": "trigger_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "height": { + "name": "height", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "sub_blocks": { + "name": "sub_blocks", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "outputs": { + "name": "outputs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "extent": { + "name": "extent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_blocks_workflow_id_idx": { + "name": "workflow_blocks_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_blocks_parent_id_idx": { + "name": "workflow_blocks_parent_id_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_blocks_workflow_parent_idx": { + "name": "workflow_blocks_workflow_parent_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_blocks_workflow_type_idx": { + "name": "workflow_blocks_workflow_type_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_blocks_workflow_id_workflow_id_fk": { + "name": "workflow_blocks_workflow_id_workflow_id_fk", + "tableFrom": "workflow_blocks", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_checkpoints": { + "name": "workflow_checkpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_state": { + "name": "workflow_state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_checkpoints_user_id_idx": { + "name": "workflow_checkpoints_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_workflow_id_idx": { + "name": "workflow_checkpoints_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_chat_id_idx": { + "name": "workflow_checkpoints_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_message_id_idx": { + "name": "workflow_checkpoints_message_id_idx", + "columns": [ + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_user_workflow_idx": { + "name": "workflow_checkpoints_user_workflow_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_workflow_chat_idx": { + "name": "workflow_checkpoints_workflow_chat_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_created_at_idx": { + "name": "workflow_checkpoints_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_chat_created_at_idx": { + "name": "workflow_checkpoints_chat_created_at_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_checkpoints_user_id_user_id_fk": { + "name": "workflow_checkpoints_user_id_user_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_checkpoints_workflow_id_workflow_id_fk": { + "name": "workflow_checkpoints_workflow_id_workflow_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_checkpoints_chat_id_copilot_chats_id_fk": { + "name": "workflow_checkpoints_chat_id_copilot_chats_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_deployment_version": { + "name": "workflow_deployment_version", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_deployment_version_workflow_id_idx": { + "name": "workflow_deployment_version_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_version_workflow_version_unique": { + "name": "workflow_deployment_version_workflow_version_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_version_workflow_active_idx": { + "name": "workflow_deployment_version_workflow_active_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_version_created_at_idx": { + "name": "workflow_deployment_version_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_deployment_version_workflow_id_workflow_id_fk": { + "name": "workflow_deployment_version_workflow_id_workflow_id_fk", + "tableFrom": "workflow_deployment_version", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_edges": { + "name": "workflow_edges", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_block_id": { + "name": "source_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_block_id": { + "name": "target_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_handle": { + "name": "source_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_handle": { + "name": "target_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_edges_workflow_id_idx": { + "name": "workflow_edges_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_source_block_idx": { + "name": "workflow_edges_source_block_idx", + "columns": [ + { + "expression": "source_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_target_block_idx": { + "name": "workflow_edges_target_block_idx", + "columns": [ + { + "expression": "target_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_workflow_source_idx": { + "name": "workflow_edges_workflow_source_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_workflow_target_idx": { + "name": "workflow_edges_workflow_target_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_edges_workflow_id_workflow_id_fk": { + "name": "workflow_edges_workflow_id_workflow_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_edges_source_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_source_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": ["source_block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_edges_target_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_target_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": ["target_block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_logs": { + "name": "workflow_execution_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_snapshot_id": { + "name": "state_snapshot_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_duration_ms": { + "name": "total_duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "execution_data": { + "name": "execution_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "cost": { + "name": "cost", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "files": { + "name": "files", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_execution_logs_workflow_id_idx": { + "name": "workflow_execution_logs_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_execution_id_idx": { + "name": "workflow_execution_logs_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_trigger_idx": { + "name": "workflow_execution_logs_trigger_idx", + "columns": [ + { + "expression": "trigger", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_level_idx": { + "name": "workflow_execution_logs_level_idx", + "columns": [ + { + "expression": "level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_started_at_idx": { + "name": "workflow_execution_logs_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_execution_id_unique": { + "name": "workflow_execution_logs_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workflow_started_at_idx": { + "name": "workflow_execution_logs_workflow_started_at_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_execution_logs_workflow_id_workflow_id_fk": { + "name": "workflow_execution_logs_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk": { + "name": "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow_execution_snapshots", + "columnsFrom": ["state_snapshot_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_snapshots": { + "name": "workflow_execution_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_hash": { + "name": "state_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_data": { + "name": "state_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_snapshots_workflow_id_idx": { + "name": "workflow_snapshots_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_hash_idx": { + "name": "workflow_snapshots_hash_idx", + "columns": [ + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_workflow_hash_idx": { + "name": "workflow_snapshots_workflow_hash_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_created_at_idx": { + "name": "workflow_snapshots_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_execution_snapshots_workflow_id_workflow_id_fk": { + "name": "workflow_execution_snapshots_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_snapshots", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_folder": { + "name": "workflow_folder", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'#6B7280'" + }, + "is_expanded": { + "name": "is_expanded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_folder_user_idx": { + "name": "workflow_folder_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_workspace_parent_idx": { + "name": "workflow_folder_workspace_parent_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_parent_sort_idx": { + "name": "workflow_folder_parent_sort_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_folder_user_id_user_id_fk": { + "name": "workflow_folder_user_id_user_id_fk", + "tableFrom": "workflow_folder", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_folder_workspace_id_workspace_id_fk": { + "name": "workflow_folder_workspace_id_workspace_id_fk", + "tableFrom": "workflow_folder", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_log_webhook": { + "name": "workflow_log_webhook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "include_final_output": { + "name": "include_final_output", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "include_trace_spans": { + "name": "include_trace_spans", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "include_rate_limits": { + "name": "include_rate_limits", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "include_usage_data": { + "name": "include_usage_data", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "level_filter": { + "name": "level_filter", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY['info', 'error']::text[]" + }, + "trigger_filter": { + "name": "trigger_filter", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY['api', 'webhook', 'schedule', 'manual', 'chat']::text[]" + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_log_webhook_workflow_id_idx": { + "name": "workflow_log_webhook_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_log_webhook_active_idx": { + "name": "workflow_log_webhook_active_idx", + "columns": [ + { + "expression": "active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_log_webhook_workflow_id_workflow_id_fk": { + "name": "workflow_log_webhook_workflow_id_workflow_id_fk", + "tableFrom": "workflow_log_webhook", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_log_webhook_delivery": { + "name": "workflow_log_webhook_delivery", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "subscription_id": { + "name": "subscription_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "webhook_delivery_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_attempt_at": { + "name": "last_attempt_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "response_status": { + "name": "response_status", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "response_body": { + "name": "response_body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_log_webhook_delivery_subscription_id_idx": { + "name": "workflow_log_webhook_delivery_subscription_id_idx", + "columns": [ + { + "expression": "subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_log_webhook_delivery_execution_id_idx": { + "name": "workflow_log_webhook_delivery_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_log_webhook_delivery_status_idx": { + "name": "workflow_log_webhook_delivery_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_log_webhook_delivery_next_attempt_idx": { + "name": "workflow_log_webhook_delivery_next_attempt_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_log_webhook_delivery_subscription_id_workflow_log_webhook_id_fk": { + "name": "workflow_log_webhook_delivery_subscription_id_workflow_log_webhook_id_fk", + "tableFrom": "workflow_log_webhook_delivery", + "tableTo": "workflow_log_webhook", + "columnsFrom": ["subscription_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_log_webhook_delivery_workflow_id_workflow_id_fk": { + "name": "workflow_log_webhook_delivery_workflow_id_workflow_id_fk", + "tableFrom": "workflow_log_webhook_delivery", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_schedule": { + "name": "workflow_schedule", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_ran_at": { + "name": "last_ran_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'UTC'" + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_failed_at": { + "name": "last_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_schedule_workflow_block_unique": { + "name": "workflow_schedule_workflow_block_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_schedule_workflow_id_workflow_id_fk": { + "name": "workflow_schedule_workflow_id_workflow_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_block_id_workflow_blocks_id_fk": { + "name": "workflow_schedule_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow_blocks", + "columnsFrom": ["block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_subflows": { + "name": "workflow_subflows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_subflows_workflow_id_idx": { + "name": "workflow_subflows_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_subflows_workflow_type_idx": { + "name": "workflow_subflows_workflow_type_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_subflows_workflow_id_workflow_id_fk": { + "name": "workflow_subflows_workflow_id_workflow_id_fk", + "tableFrom": "workflow_subflows", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace": { + "name": "workspace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workspace_owner_id_user_id_fk": { + "name": "workspace_owner_id_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": ["owner_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_environment": { + "name": "workspace_environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_environment_workspace_unique": { + "name": "workspace_environment_workspace_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_environment_workspace_id_workspace_id_fk": { + "name": "workspace_environment_workspace_id_workspace_id_fk", + "tableFrom": "workspace_environment", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_invitation": { + "name": "workspace_invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "status": { + "name": "status", + "type": "workspace_invitation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permissions": { + "name": "permissions", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'admin'" + }, + "org_invitation_id": { + "name": "org_invitation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workspace_invitation_workspace_id_workspace_id_fk": { + "name": "workspace_invitation_workspace_id_workspace_id_fk", + "tableFrom": "workspace_invitation", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_invitation_inviter_id_user_id_fk": { + "name": "workspace_invitation_inviter_id_user_id_fk", + "tableFrom": "workspace_invitation", + "tableTo": "user", + "columnsFrom": ["inviter_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_invitation_token_unique": { + "name": "workspace_invitation_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.permission_type": { + "name": "permission_type", + "schema": "public", + "values": ["admin", "write", "read"] + }, + "public.webhook_delivery_status": { + "name": "webhook_delivery_status", + "schema": "public", + "values": ["pending", "in_progress", "success", "failed"] + }, + "public.workspace_invitation_status": { + "name": "workspace_invitation_status", + "schema": "public", + "values": ["pending", "accepted", "rejected", "cancelled"] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/apps/sim/db/migrations/meta/_journal.json b/apps/sim/db/migrations/meta/_journal.json index f5b57c2b5..19a75a7db 100644 --- a/apps/sim/db/migrations/meta/_journal.json +++ b/apps/sim/db/migrations/meta/_journal.json @@ -617,6 +617,13 @@ "when": 1757619657920, "tag": "0088_serious_firestar", "breakpoints": true + }, + { + "idx": 89, + "version": "7", + "when": 1757628623657, + "tag": "0089_amused_pete_wisdom", + "breakpoints": true } ] } diff --git a/apps/sim/db/schema.ts b/apps/sim/db/schema.ts index 367e54795..5d66f05df 100644 --- a/apps/sim/db/schema.ts +++ b/apps/sim/db/schema.ts @@ -146,7 +146,7 @@ export const workflow = pgTable( isDeployed: boolean('is_deployed').notNull().default(false), deployedState: json('deployed_state'), deployedAt: timestamp('deployed_at'), - pinnedApiKey: text('pinned_api_key'), + pinnedApiKeyId: text('pinned_api_key_id').references(() => apiKey.id, { onDelete: 'set null' }), collaborators: json('collaborators').notNull().default('[]'), runCount: integer('run_count').notNull().default(0), lastRunAt: timestamp('last_run_at'), @@ -503,18 +503,31 @@ export const workflowLogWebhookDelivery = pgTable( }) ) -export const apiKey = pgTable('api_key', { - id: text('id').primaryKey(), - userId: text('user_id') - .notNull() - .references(() => user.id, { onDelete: 'cascade' }), - name: text('name').notNull(), - key: text('key').notNull().unique(), - lastUsed: timestamp('last_used'), - createdAt: timestamp('created_at').notNull().defaultNow(), - updatedAt: timestamp('updated_at').notNull().defaultNow(), - expiresAt: timestamp('expires_at'), -}) +export const apiKey = pgTable( + 'api_key', + { + id: text('id').primaryKey(), + userId: text('user_id') + .notNull() + .references(() => user.id, { onDelete: 'cascade' }), + workspaceId: text('workspace_id').references(() => workspace.id, { onDelete: 'cascade' }), // Only set for workspace keys + createdBy: text('created_by').references(() => user.id, { onDelete: 'set null' }), // Who created the workspace key + name: text('name').notNull(), + key: text('key').notNull().unique(), + type: text('type').notNull().default('personal'), // 'personal' or 'workspace' + lastUsed: timestamp('last_used'), + createdAt: timestamp('created_at').notNull().defaultNow(), + updatedAt: timestamp('updated_at').notNull().defaultNow(), + expiresAt: timestamp('expires_at'), + }, + (table) => ({ + // Ensure workspace keys have a workspace_id and personal keys don't + workspaceTypeCheck: check( + 'workspace_type_check', + sql`(type = 'workspace' AND workspace_id IS NOT NULL) OR (type = 'personal' AND workspace_id IS NULL)` + ), + }) +) export const marketplace = pgTable('marketplace', { id: text('id').primaryKey(), diff --git a/apps/sim/executor/handlers/loop/loop-handler.test.ts b/apps/sim/executor/handlers/loop/loop-handler.test.ts index 1f3ebec7a..c86da68b8 100644 --- a/apps/sim/executor/handlers/loop/loop-handler.test.ts +++ b/apps/sim/executor/handlers/loop/loop-handler.test.ts @@ -1,4 +1,4 @@ -import { vi } from 'vitest' +import { beforeEach, describe, expect, it, vi } from 'vitest' import { BlockType } from '@/executor/consts' import { LoopBlockHandler } from '@/executor/handlers/loop/loop-handler' import type { ExecutionContext } from '@/executor/types' diff --git a/apps/sim/lib/api-key/auth.ts b/apps/sim/lib/api-key/auth.ts new file mode 100644 index 000000000..fd1118c78 --- /dev/null +++ b/apps/sim/lib/api-key/auth.ts @@ -0,0 +1,215 @@ +import { + decryptApiKey, + encryptApiKey, + generateApiKey, + generateEncryptedApiKey, + isEncryptedApiKeyFormat, + isLegacyApiKeyFormat, +} from '@/lib/api-key/service' +import { env } from '@/lib/env' +import { createLogger } from '@/lib/logs/console/logger' + +const logger = createLogger('ApiKeyAuth') + +/** + * API key authentication utilities supporting both legacy plain text keys + * and modern encrypted keys for gradual migration without breaking existing keys + */ + +/** + * Checks if a stored key is in the new encrypted format + * @param storedKey - The key stored in the database + * @returns true if the key is encrypted, false if it's plain text + */ +export function isEncryptedKey(storedKey: string): boolean { + // Check if it follows the encrypted format: iv:encrypted:authTag + return storedKey.includes(':') && storedKey.split(':').length === 3 +} + +/** + * Authenticates an API key against a stored key, supporting both legacy and new encrypted formats + * @param inputKey - The API key provided by the client + * @param storedKey - The key stored in the database (may be plain text or encrypted) + * @returns Promise - true if the key is valid + */ +export async function authenticateApiKey(inputKey: string, storedKey: string): Promise { + try { + // If input key has new encrypted prefix (sk-sim-), only check against encrypted storage + if (isEncryptedApiKeyFormat(inputKey)) { + if (isEncryptedKey(storedKey)) { + try { + const { decrypted } = await decryptApiKey(storedKey) + return inputKey === decrypted + } catch (decryptError) { + logger.error('Failed to decrypt stored API key:', { error: decryptError }) + return false + } + } + // New format keys should never match against plain text storage + return false + } + + // If input key has legacy prefix (sim_), check both encrypted and plain text + if (isLegacyApiKeyFormat(inputKey)) { + if (isEncryptedKey(storedKey)) { + try { + const { decrypted } = await decryptApiKey(storedKey) + return inputKey === decrypted + } catch (decryptError) { + logger.error('Failed to decrypt stored API key:', { error: decryptError }) + // Fall through to plain text comparison if decryption fails + } + } + // Legacy format can match against plain text storage + return inputKey === storedKey + } + + // If no recognized prefix, fall back to original behavior + if (isEncryptedKey(storedKey)) { + try { + const { decrypted } = await decryptApiKey(storedKey) + return inputKey === decrypted + } catch (decryptError) { + logger.error('Failed to decrypt stored API key:', { error: decryptError }) + } + } + + return inputKey === storedKey + } catch (error) { + logger.error('API key authentication error:', { error }) + return false + } +} + +/** + * Encrypts an API key for secure storage + * @param apiKey - The plain text API key to encrypt + * @returns Promise - The encrypted key + */ +export async function encryptApiKeyForStorage(apiKey: string): Promise { + try { + const { encrypted } = await encryptApiKey(apiKey) + return encrypted + } catch (error) { + logger.error('API key encryption error:', { error }) + throw new Error('Failed to encrypt API key') + } +} + +/** + * Creates a new API key + * @param useStorage - Whether to encrypt the key before storage (default: true) + * @returns Promise<{key: string, encryptedKey?: string}> - The plain key and optionally encrypted version + */ +export async function createApiKey(useStorage = true): Promise<{ + key: string + encryptedKey?: string +}> { + try { + const hasEncryptionKey = env.API_ENCRYPTION_KEY !== undefined + + const plainKey = hasEncryptionKey ? generateEncryptedApiKey() : generateApiKey() + + if (useStorage) { + const encryptedKey = await encryptApiKeyForStorage(plainKey) + return { key: plainKey, encryptedKey } + } + + return { key: plainKey } + } catch (error) { + logger.error('API key creation error:', { error }) + throw new Error('Failed to create API key') + } +} + +/** + * Decrypts an API key from storage for display purposes + * @param encryptedKey - The encrypted API key from the database + * @returns Promise - The decrypted API key + */ +export async function decryptApiKeyFromStorage(encryptedKey: string): Promise { + try { + const { decrypted } = await decryptApiKey(encryptedKey) + return decrypted + } catch (error) { + logger.error('API key decryption error:', { error }) + throw new Error('Failed to decrypt API key') + } +} + +/** + * Gets the last 4 characters of an API key for display purposes + * @param apiKey - The API key (plain text) + * @returns string - The last 4 characters + */ +export function getApiKeyLast4(apiKey: string): string { + return apiKey.slice(-4) +} + +/** + * Gets the display format for an API key showing prefix and last 4 characters + * @param encryptedKey - The encrypted API key from the database + * @returns Promise - The display format like "sk-sim-...r6AA" + */ +export async function getApiKeyDisplayFormat(encryptedKey: string): Promise { + try { + if (isEncryptedKey(encryptedKey)) { + const decryptedKey = await decryptApiKeyFromStorage(encryptedKey) + return formatApiKeyForDisplay(decryptedKey) + } + // For plain text keys (legacy), format directly + return formatApiKeyForDisplay(encryptedKey) + } catch (error) { + logger.error('Failed to format API key for display:', { error }) + return '****' + } +} + +/** + * Formats an API key for display showing prefix and last 4 characters + * @param apiKey - The API key (plain text) + * @returns string - The display format like "sk-sim-...r6AA" or "sim_...r6AA" + */ +export function formatApiKeyForDisplay(apiKey: string): string { + if (isEncryptedApiKeyFormat(apiKey)) { + // For sk-sim- format: "sk-sim-...r6AA" + const last4 = getApiKeyLast4(apiKey) + return `sk-sim-...${last4}` + } + if (isLegacyApiKeyFormat(apiKey)) { + // For sim_ format: "sim_...r6AA" + const last4 = getApiKeyLast4(apiKey) + return `sim_...${last4}` + } + // Unknown format, just show last 4 + const last4 = getApiKeyLast4(apiKey) + return `...${last4}` +} + +/** + * Gets the last 4 characters of an encrypted API key by decrypting it first + * @param encryptedKey - The encrypted API key from the database + * @returns Promise - The last 4 characters + */ +export async function getEncryptedApiKeyLast4(encryptedKey: string): Promise { + try { + if (isEncryptedKey(encryptedKey)) { + const decryptedKey = await decryptApiKeyFromStorage(encryptedKey) + return getApiKeyLast4(decryptedKey) + } + // For plain text keys (legacy), return last 4 directly + return getApiKeyLast4(encryptedKey) + } catch (error) { + logger.error('Failed to get last 4 characters of API key:', { error }) + return '****' + } +} + +/** + * Validates API key format (basic validation) + * @param apiKey - The API key to validate + * @returns boolean - true if the format appears valid + */ +export function isValidApiKeyFormat(apiKey: string): boolean { + return typeof apiKey === 'string' && apiKey.length > 10 && apiKey.length < 200 +} diff --git a/apps/sim/lib/api-key/service.ts b/apps/sim/lib/api-key/service.ts new file mode 100644 index 000000000..bf62000a9 --- /dev/null +++ b/apps/sim/lib/api-key/service.ts @@ -0,0 +1,252 @@ +import { createCipheriv, createDecipheriv, randomBytes } from 'crypto' +import { and, eq } from 'drizzle-orm' +import { nanoid } from 'nanoid' +import { authenticateApiKey } from '@/lib/api-key/auth' +import { env } from '@/lib/env' +import { createLogger } from '@/lib/logs/console/logger' +import { db } from '@/db' +import { apiKey as apiKeyTable, workspace } from '@/db/schema' + +const logger = createLogger('ApiKeyService') + +export interface ApiKeyAuthOptions { + userId?: string + workspaceId?: string + keyTypes?: ('personal' | 'workspace')[] +} + +export interface ApiKeyAuthResult { + success: boolean + userId?: string + keyId?: string + keyType?: 'personal' | 'workspace' + workspaceId?: string + error?: string +} + +/** + * Authenticate an API key from header with flexible filtering options + */ +export async function authenticateApiKeyFromHeader( + apiKeyHeader: string, + options: ApiKeyAuthOptions = {} +): Promise { + if (!apiKeyHeader) { + return { success: false, error: 'API key required' } + } + + try { + // Build query based on options + let query = db + .select({ + id: apiKeyTable.id, + userId: apiKeyTable.userId, + workspaceId: apiKeyTable.workspaceId, + type: apiKeyTable.type, + key: apiKeyTable.key, + expiresAt: apiKeyTable.expiresAt, + }) + .from(apiKeyTable) + + // Add workspace join if needed for workspace keys + if (options.workspaceId || options.keyTypes?.includes('workspace')) { + query = query.leftJoin(workspace, eq(apiKeyTable.workspaceId, workspace.id)) as any + } + + // Apply filters + const conditions = [] + + if (options.userId) { + conditions.push(eq(apiKeyTable.userId, options.userId)) + } + + if (options.workspaceId) { + conditions.push(eq(apiKeyTable.workspaceId, options.workspaceId)) + } + + if (options.keyTypes?.length) { + if (options.keyTypes.length === 1) { + conditions.push(eq(apiKeyTable.type, options.keyTypes[0])) + } else { + // For multiple types, we'll filter in memory since drizzle's inArray is complex here + } + } + + if (conditions.length > 0) { + query = query.where(and(...conditions)) as any + } + + const keyRecords = await query + + // Filter by keyTypes in memory if multiple types specified + const filteredRecords = + options.keyTypes?.length && options.keyTypes.length > 1 + ? keyRecords.filter((record) => options.keyTypes!.includes(record.type as any)) + : keyRecords + + // Authenticate each key + for (const storedKey of filteredRecords) { + // Skip expired keys + if (storedKey.expiresAt && storedKey.expiresAt < new Date()) { + continue + } + + try { + const isValid = await authenticateApiKey(apiKeyHeader, storedKey.key) + if (isValid) { + return { + success: true, + userId: storedKey.userId, + keyId: storedKey.id, + keyType: storedKey.type as 'personal' | 'workspace', + workspaceId: storedKey.workspaceId || undefined, + } + } + } catch (error) { + logger.error('Error authenticating API key:', error) + } + } + + return { success: false, error: 'Invalid API key' } + } catch (error) { + logger.error('API key authentication error:', error) + return { success: false, error: 'Authentication failed' } + } +} + +/** + * Update the last used timestamp for an API key + */ +export async function updateApiKeyLastUsed(keyId: string): Promise { + try { + await db.update(apiKeyTable).set({ lastUsed: new Date() }).where(eq(apiKeyTable.id, keyId)) + } catch (error) { + logger.error('Error updating API key last used:', error) + } +} + +/** + * Get the API encryption key from the environment + * @returns The API encryption key + */ +function getApiEncryptionKey(): Buffer | null { + const key = env.API_ENCRYPTION_KEY + if (!key) { + logger.warn( + 'API_ENCRYPTION_KEY not set - API keys will be stored in plain text. Consider setting this for better security.' + ) + return null + } + if (key.length !== 64) { + throw new Error('API_ENCRYPTION_KEY must be a 64-character hex string (32 bytes)') + } + return Buffer.from(key, 'hex') +} + +/** + * Encrypts an API key using the dedicated API encryption key + * @param apiKey - The API key to encrypt + * @returns A promise that resolves to an object containing the encrypted API key and IV + */ +export async function encryptApiKey(apiKey: string): Promise<{ encrypted: string; iv: string }> { + const key = getApiEncryptionKey() + + // If no API encryption key is set, return the key as-is for backward compatibility + if (!key) { + return { encrypted: apiKey, iv: '' } + } + + const iv = randomBytes(16) + const cipher = createCipheriv('aes-256-gcm', key, iv) + let encrypted = cipher.update(apiKey, 'utf8', 'hex') + encrypted += cipher.final('hex') + + const authTag = cipher.getAuthTag() + + // Format: iv:encrypted:authTag + return { + encrypted: `${iv.toString('hex')}:${encrypted}:${authTag.toString('hex')}`, + iv: iv.toString('hex'), + } +} + +/** + * Decrypts an API key using the dedicated API encryption key + * @param encryptedValue - The encrypted value in format "iv:encrypted:authTag" or plain text + * @returns A promise that resolves to an object containing the decrypted API key + */ +export async function decryptApiKey(encryptedValue: string): Promise<{ decrypted: string }> { + // Check if this is actually encrypted (contains colons) + if (!encryptedValue.includes(':') || encryptedValue.split(':').length !== 3) { + // This is a plain text key, return as-is + return { decrypted: encryptedValue } + } + + const key = getApiEncryptionKey() + + // If no API encryption key is set, assume it's plain text + if (!key) { + return { decrypted: encryptedValue } + } + + const parts = encryptedValue.split(':') + const ivHex = parts[0] + const authTagHex = parts[parts.length - 1] + const encrypted = parts.slice(1, -1).join(':') + + if (!ivHex || !encrypted || !authTagHex) { + throw new Error('Invalid encrypted API key format. Expected "iv:encrypted:authTag"') + } + + const iv = Buffer.from(ivHex, 'hex') + const authTag = Buffer.from(authTagHex, 'hex') + + try { + const decipher = createDecipheriv('aes-256-gcm', key, iv) + decipher.setAuthTag(authTag) + + let decrypted = decipher.update(encrypted, 'hex', 'utf8') + decrypted += decipher.final('utf8') + + return { decrypted } + } catch (error: unknown) { + logger.error('API key decryption error:', { + error: error instanceof Error ? error.message : 'Unknown error', + }) + throw error + } +} + +/** + * Generates a standardized API key with the 'sim_' prefix (legacy format) + * @returns A new API key string + */ +export function generateApiKey(): string { + return `sim_${nanoid(32)}` +} + +/** + * Generates a new encrypted API key with the 'sk-sim-' prefix + * @returns A new encrypted API key string + */ +export function generateEncryptedApiKey(): string { + return `sk-sim-${nanoid(32)}` +} + +/** + * Determines if an API key uses the new encrypted format based on prefix + * @param apiKey - The API key to check + * @returns true if the key uses the new encrypted format (sk-sim- prefix) + */ +export function isEncryptedApiKeyFormat(apiKey: string): boolean { + return apiKey.startsWith('sk-sim-') +} + +/** + * Determines if an API key uses the legacy format based on prefix + * @param apiKey - The API key to check + * @returns true if the key uses the legacy format (sim_ prefix) + */ +export function isLegacyApiKeyFormat(apiKey: string): boolean { + return apiKey.startsWith('sim_') && !apiKey.startsWith('sk-sim-') +} diff --git a/apps/sim/lib/auth/hybrid.ts b/apps/sim/lib/auth/hybrid.ts index 5145025da..cca01d9a4 100644 --- a/apps/sim/lib/auth/hybrid.ts +++ b/apps/sim/lib/auth/hybrid.ts @@ -1,10 +1,11 @@ import { eq } from 'drizzle-orm' import type { NextRequest } from 'next/server' +import { authenticateApiKeyFromHeader, updateApiKeyLastUsed } from '@/lib/api-key/service' import { getSession } from '@/lib/auth' import { verifyInternalToken } from '@/lib/auth/internal' import { createLogger } from '@/lib/logs/console/logger' import { db } from '@/db' -import { apiKey as apiKeyTable, workflow } from '@/db/schema' +import { workflow } from '@/db/schema' const logger = createLogger('HybridAuth') @@ -105,19 +106,16 @@ export async function checkHybridAuth( // 3. Try API key auth const apiKeyHeader = request.headers.get('x-api-key') if (apiKeyHeader) { - const [apiKeyRecord] = await db - .select({ userId: apiKeyTable.userId }) - .from(apiKeyTable) - .where(eq(apiKeyTable.key, apiKeyHeader)) - .limit(1) - - if (apiKeyRecord) { + const result = await authenticateApiKeyFromHeader(apiKeyHeader) + if (result.success) { + await updateApiKeyLastUsed(result.keyId!) return { success: true, - userId: apiKeyRecord.userId, + userId: result.userId!, authType: 'api_key', } } + return { success: false, error: 'Invalid API key', diff --git a/apps/sim/lib/copilot/auth.ts b/apps/sim/lib/copilot/auth.ts index 5e3d05205..622632a29 100644 --- a/apps/sim/lib/copilot/auth.ts +++ b/apps/sim/lib/copilot/auth.ts @@ -1,9 +1,7 @@ -import { eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' +import { authenticateApiKeyFromHeader, updateApiKeyLastUsed } from '@/lib/api-key/service' import { getSession } from '@/lib/auth' import { generateRequestId } from '@/lib/utils' -import { db } from '@/db' -import { apiKey as apiKeyTable } from '@/db/schema' export type { NotificationStatus } from '@/lib/copilot/types' @@ -62,14 +60,10 @@ export async function authenticateCopilotRequest(req: NextRequest): Promise { diff --git a/apps/sim/lib/env.ts b/apps/sim/lib/env.ts index 8ec70950b..a2afdd069 100644 --- a/apps/sim/lib/env.ts +++ b/apps/sim/lib/env.ts @@ -23,6 +23,7 @@ export const env = createEnv({ ALLOWED_LOGIN_EMAILS: z.string().optional(), // Comma-separated list of allowed email addresses for login ALLOWED_LOGIN_DOMAINS: z.string().optional(), // Comma-separated list of allowed email domains for login ENCRYPTION_KEY: z.string().min(32), // Key for encrypting sensitive data + API_ENCRYPTION_KEY: z.string().min(32).optional(), // Dedicated key for encrypting API keys (optional for OSS) INTERNAL_API_SECRET: z.string().min(32), // Secret for internal API authentication // Copilot @@ -87,7 +88,6 @@ export const env = createEnv({ LOG_LEVEL: z.enum(['DEBUG', 'INFO', 'WARN', 'ERROR']).optional(), // Minimum log level to display (defaults to ERROR in production, DEBUG in development) // External Services - JWT_SECRET: z.string().min(1).optional(), // JWT signing secret for custom tokens BROWSERBASE_API_KEY: z.string().min(1).optional(), // Browserbase API key for browser automation BROWSERBASE_PROJECT_ID: z.string().min(1).optional(), // Browserbase project ID GITHUB_TOKEN: z.string().optional(), // GitHub personal access token for API access diff --git a/apps/sim/lib/schedules/utils.test.ts b/apps/sim/lib/schedules/utils.test.ts index e946237be..5e3b81810 100644 --- a/apps/sim/lib/schedules/utils.test.ts +++ b/apps/sim/lib/schedules/utils.test.ts @@ -1,7 +1,7 @@ /** * Tests for schedule utility functions */ -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { type BlockState, calculateNextRunTime, diff --git a/apps/sim/lib/uploads/blob/blob-client.test.ts b/apps/sim/lib/uploads/blob/blob-client.test.ts index 52608c1be..c82f89f46 100644 --- a/apps/sim/lib/uploads/blob/blob-client.test.ts +++ b/apps/sim/lib/uploads/blob/blob-client.test.ts @@ -210,7 +210,7 @@ describe('Azure Blob Storage Client', () => { { input: '', expected: 'file' }, ] - test.each(testCases)('should sanitize "$input" to "$expected"', async ({ input, expected }) => { + it.each(testCases)('should sanitize "$input" to "$expected"', async ({ input, expected }) => { const { sanitizeFilenameForMetadata } = await import('@/lib/uploads/blob/blob-client') expect(sanitizeFilenameForMetadata(input)).toBe(expected) }) diff --git a/apps/sim/lib/utils.test.ts b/apps/sim/lib/utils.test.ts index c0605ed49..e9fab46ba 100644 --- a/apps/sim/lib/utils.test.ts +++ b/apps/sim/lib/utils.test.ts @@ -8,7 +8,6 @@ import { formatDateTime, formatDuration, formatTime, - generateApiKey, getInvalidCharacters, getTimezoneAbbreviation, isValidName, @@ -44,25 +43,6 @@ afterEach(() => { vi.clearAllMocks() }) -describe('generateApiKey', () => { - it.concurrent('should generate API key with sim_ prefix', () => { - const key = generateApiKey() - expect(key).toMatch(/^sim_/) - }) - - it.concurrent('should generate unique API keys for each call', () => { - const key1 = generateApiKey() - const key2 = generateApiKey() - expect(key1).not.toBe(key2) - }) - - it.concurrent('should generate API keys of correct length', () => { - const key = generateApiKey() - // Expected format: 'sim_' + 32 random characters - expect(key.length).toBe(36) - }) -}) - describe('cn (class name utility)', () => { it.concurrent('should merge class names correctly', () => { const result = cn('class1', 'class2') diff --git a/apps/sim/lib/utils.ts b/apps/sim/lib/utils.ts index edf20df7e..e85dfe4aa 100644 --- a/apps/sim/lib/utils.ts +++ b/apps/sim/lib/utils.ts @@ -1,6 +1,5 @@ import { createCipheriv, createDecipheriv, randomBytes } from 'crypto' import { type ClassValue, clsx } from 'clsx' -import { nanoid } from 'nanoid' import { twMerge } from 'tailwind-merge' import { env } from '@/lib/env' import { createLogger } from '@/lib/logs/console/logger' @@ -265,14 +264,6 @@ export function formatDuration(durationMs: number): string { return `${hours}h ${remainingMinutes}m` } -/** - * Generates a standardized API key with the 'sim_' prefix - * @returns A new API key string - */ -export function generateApiKey(): string { - return `sim_${nanoid(32)}` -} - /** * Generates a secure random password * @param length - The length of the password (default: 24) diff --git a/apps/sim/lib/workflows/utils.ts b/apps/sim/lib/workflows/utils.ts index 63e2e5d1e..df301a702 100644 --- a/apps/sim/lib/workflows/utils.ts +++ b/apps/sim/lib/workflows/utils.ts @@ -3,14 +3,48 @@ import { NextResponse } from 'next/server' import { getEnv } from '@/lib/env' import { createLogger } from '@/lib/logs/console/logger' import { db } from '@/db' -import { userStats, workflow as workflowTable } from '@/db/schema' +import { apiKey, userStats, workflow as workflowTable } from '@/db/schema' import type { ExecutionResult } from '@/executor/types' import type { WorkflowState } from '@/stores/workflows/workflow/types' const logger = createLogger('WorkflowUtils') export async function getWorkflowById(id: string) { - const workflows = await db.select().from(workflowTable).where(eq(workflowTable.id, id)).limit(1) + const workflows = await db + .select({ + id: workflowTable.id, + userId: workflowTable.userId, + workspaceId: workflowTable.workspaceId, + folderId: workflowTable.folderId, + name: workflowTable.name, + description: workflowTable.description, + color: workflowTable.color, + lastSynced: workflowTable.lastSynced, + createdAt: workflowTable.createdAt, + updatedAt: workflowTable.updatedAt, + isDeployed: workflowTable.isDeployed, + deployedState: workflowTable.deployedState, + deployedAt: workflowTable.deployedAt, + pinnedApiKeyId: workflowTable.pinnedApiKeyId, + collaborators: workflowTable.collaborators, + runCount: workflowTable.runCount, + lastRunAt: workflowTable.lastRunAt, + variables: workflowTable.variables, + isPublished: workflowTable.isPublished, + marketplaceData: workflowTable.marketplaceData, + pinnedApiKey: { + id: apiKey.id, + name: apiKey.name, + key: apiKey.key, + type: apiKey.type, + workspaceId: apiKey.workspaceId, + }, + }) + .from(workflowTable) + .leftJoin(apiKey, eq(workflowTable.pinnedApiKeyId, apiKey.id)) + .where(eq(workflowTable.id, id)) + .limit(1) + return workflows[0] } @@ -45,7 +79,7 @@ export async function updateWorkflowRunCounts(workflowId: string, runs = 1) { await db .update(workflowTable) .set({ - runCount: workflow.runCount + runs, + runCount: (workflow.runCount as number) + runs, lastRunAt: new Date(), }) .where(eq(workflowTable.id, workflowId)) diff --git a/apps/sim/serializer/index.test.ts b/apps/sim/serializer/index.test.ts index 86588f79b..9d725acef 100644 --- a/apps/sim/serializer/index.test.ts +++ b/apps/sim/serializer/index.test.ts @@ -7,7 +7,7 @@ * converting between workflow state (blocks, edges, loops) and serialized format * used by the executor. */ -import { describe, expect, vi } from 'vitest' +import { describe, expect, it, vi } from 'vitest' import { getProviderFromModel } from '@/providers/utils' import { createAgentWithToolsWorkflowState, diff --git a/apps/sim/serializer/tests/dual-validation.test.ts b/apps/sim/serializer/tests/dual-validation.test.ts index a07e2f825..08cd74f27 100644 --- a/apps/sim/serializer/tests/dual-validation.test.ts +++ b/apps/sim/serializer/tests/dual-validation.test.ts @@ -7,7 +7,7 @@ * 1. Early validation (serialization) - user-only required fields * 2. Late validation (tool execution) - user-or-llm required fields */ -import { describe, expect, vi } from 'vitest' +import { describe, expect, it, vi } from 'vitest' import { Serializer } from '@/serializer/index' import { validateRequiredParametersAfterMerge } from '@/tools/utils' diff --git a/apps/sim/stores/workflows/workflow/utils.test.ts b/apps/sim/stores/workflows/workflow/utils.test.ts index eaf3cedcc..01403f95d 100644 --- a/apps/sim/stores/workflows/workflow/utils.test.ts +++ b/apps/sim/stores/workflows/workflow/utils.test.ts @@ -1,4 +1,4 @@ -import { describe, expect } from 'vitest' +import { describe, expect, it } from 'vitest' import type { BlockState } from '@/stores/workflows/workflow/types' import { convertLoopBlockToLoop } from '@/stores/workflows/workflow/utils' diff --git a/apps/sim/test-self-hosting.ts b/apps/sim/test-self-hosting.ts new file mode 100644 index 000000000..2e664a568 --- /dev/null +++ b/apps/sim/test-self-hosting.ts @@ -0,0 +1,22 @@ +import { createApiKey } from './lib/api-key/auth' + +console.log('=== Testing self-hosting scenario (no API_ENCRYPTION_KEY) ===') + +// Check environment +console.log('ENCRYPTION_KEY:', `${process.env.ENCRYPTION_KEY?.slice(0, 10)}...`) +console.log('API_ENCRYPTION_KEY:', process.env.API_ENCRYPTION_KEY) + +// Ensure API_ENCRYPTION_KEY is not set +process.env.API_ENCRYPTION_KEY = undefined +console.log('API_ENCRYPTION_KEY after delete:', process.env.API_ENCRYPTION_KEY) + +try { + const result = await createApiKey(true) + console.log('Key generated:', !!result.key) + console.log('Encrypted key generated:', !!result.encryptedKey) + console.log('Encrypted key value:', result.encryptedKey) + console.log('Are they the same?', result.key === result.encryptedKey) + console.log('Would validation pass?', !!result.encryptedKey) +} catch (error) { + console.error('Error in createApiKey:', error) +} diff --git a/bun.lock b/bun.lock index aabf1d0a3..c93bd57a8 100644 --- a/bun.lock +++ b/bun.lock @@ -7,6 +7,7 @@ "@linear/sdk": "40.0.0", "@t3-oss/env-nextjs": "0.13.4", "@vercel/analytics": "1.5.0", + "bcryptjs": "3.0.2", "geist": "^1.4.2", "mongodb": "6.19.0", "react-colorful": "5.6.1", @@ -16,6 +17,7 @@ "devDependencies": { "@biomejs/biome": "2.0.0-beta.5", "@next/env": "^15.4.1", + "@types/bcryptjs": "3.0.0", "husky": "9.1.7", "lint-staged": "16.0.0", "turbo": "2.5.6", @@ -1333,6 +1335,8 @@ "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], + "@types/bcryptjs": ["@types/bcryptjs@3.0.0", "", { "dependencies": { "bcryptjs": "*" } }, "sha512-WRZOuCuaz8UcZZE4R5HXTco2goQSI2XxjGY3hbM/xDvwmqFWd4ivooImsMx65OKM6CtNKbnZ5YL+YwAwK7c1dg=="], + "@types/chai": ["@types/chai@5.2.2", "", { "dependencies": { "@types/deep-eql": "*" } }, "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg=="], "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], @@ -1627,6 +1631,8 @@ "base64id": ["base64id@2.0.0", "", {}, "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog=="], + "bcryptjs": ["bcryptjs@3.0.2", "", { "bin": { "bcrypt": "bin/bcrypt" } }, "sha512-k38b3XOZKv60C4E2hVsXTolJWfkGRMbILBIe2IBITXciy5bOsTKot5kDrf3ZfufQtQOUN5mXceUEpU1rTl9Uog=="], + "better-auth": ["better-auth@1.3.7", "", { "dependencies": { "@better-auth/utils": "0.2.6", "@better-fetch/fetch": "^1.1.18", "@noble/ciphers": "^0.6.0", "@noble/hashes": "^1.8.0", "@simplewebauthn/browser": "^13.1.2", "@simplewebauthn/server": "^13.1.2", "better-call": "^1.0.13", "defu": "^6.1.4", "jose": "^5.10.0", "kysely": "^0.28.5", "nanostores": "^0.11.4" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["react", "react-dom"] }, "sha512-/1fEyx2SGgJQM5ujozDCh9eJksnVkNU/J7Fk/tG5Y390l8nKbrPvqiFlCjlMM+scR+UABJbQzA6An7HT50LHyQ=="], "better-call": ["better-call@1.0.16", "", { "dependencies": { "@better-fetch/fetch": "^1.1.4", "rou3": "^0.5.1", "set-cookie-parser": "^2.7.1", "uncrypto": "^0.1.3" } }, "sha512-42dgJ1rOtc0anOoxjXPOWuel/Z/4aeO7EJ2SiXNwvlkySSgjXhNjAjTMWa8DL1nt6EXS3jl3VKC3mPsU/lUgVA=="], diff --git a/package.json b/package.json index 438f32397..7615b5cb6 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "@linear/sdk": "40.0.0", "@t3-oss/env-nextjs": "0.13.4", "@vercel/analytics": "1.5.0", + "bcryptjs": "3.0.2", "geist": "^1.4.2", "mongodb": "6.19.0", "react-colorful": "5.6.1", @@ -44,6 +45,7 @@ "devDependencies": { "@biomejs/biome": "2.0.0-beta.5", "@next/env": "^15.4.1", + "@types/bcryptjs": "3.0.0", "husky": "9.1.7", "lint-staged": "16.0.0", "turbo": "2.5.6"