diff --git a/apps/sim/app/api/auth/accounts/route.ts b/apps/sim/app/api/auth/accounts/route.ts index a51d8585c..ed385b6d8 100644 --- a/apps/sim/app/api/auth/accounts/route.ts +++ b/apps/sim/app/api/auth/accounts/route.ts @@ -32,14 +32,11 @@ export async function GET(request: NextRequest) { .from(account) .where(and(...whereConditions)) - // Use the user's email as the display name (consistent with credential selector) - const userEmail = session.user.email - const accountsWithDisplayName = accounts.map((acc) => ({ id: acc.id, accountId: acc.accountId, providerId: acc.providerId, - displayName: userEmail || acc.providerId, + displayName: acc.accountId || acc.providerId, })) return NextResponse.json({ accounts: accountsWithDisplayName }) diff --git a/apps/sim/app/api/auth/oauth/credentials/route.ts b/apps/sim/app/api/auth/oauth/credentials/route.ts index 7809e5543..8c4b42dc1 100644 --- a/apps/sim/app/api/auth/oauth/credentials/route.ts +++ b/apps/sim/app/api/auth/oauth/credentials/route.ts @@ -1,5 +1,5 @@ import { db } from '@sim/db' -import { account, user } from '@sim/db/schema' +import { account, credential, credentialMember, user } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { and, eq } from 'drizzle-orm' import { jwtDecode } from 'jwt-decode' @@ -7,8 +7,10 @@ import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' +import { syncWorkspaceOAuthCredentialsForUser } from '@/lib/credentials/oauth' import { evaluateScopeCoverage, type OAuthProvider, parseProvider } from '@/lib/oauth' import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils' +import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils' export const dynamic = 'force-dynamic' @@ -18,6 +20,7 @@ const credentialsQuerySchema = z .object({ provider: z.string().nullish(), workflowId: z.string().uuid('Workflow ID must be a valid UUID').nullish(), + workspaceId: z.string().uuid('Workspace ID must be a valid UUID').nullish(), credentialId: z .string() .min(1, 'Credential ID must not be empty') @@ -35,6 +38,79 @@ interface GoogleIdToken { name?: string } +function toCredentialResponse( + id: string, + displayName: string, + providerId: string, + updatedAt: Date, + scope: string | null +) { + const storedScope = scope?.trim() + const grantedScopes = storedScope ? storedScope.split(/[\s,]+/).filter(Boolean) : [] + const scopeEvaluation = evaluateScopeCoverage(providerId, grantedScopes) + const [_, featureType = 'default'] = providerId.split('-') + + return { + id, + name: displayName, + provider: providerId, + lastUsed: updatedAt.toISOString(), + isDefault: featureType === 'default', + scopes: scopeEvaluation.grantedScopes, + canonicalScopes: scopeEvaluation.canonicalScopes, + missingScopes: scopeEvaluation.missingScopes, + extraScopes: scopeEvaluation.extraScopes, + requiresReauthorization: scopeEvaluation.requiresReauthorization, + } +} + +async function getFallbackDisplayName( + requestId: string, + providerParam: string | null | undefined, + accountRow: { + idToken: string | null + accountId: string + userId: string + } +) { + const providerForParse = (providerParam || 'google') as OAuthProvider + const { baseProvider } = parseProvider(providerForParse) + + if (accountRow.idToken) { + try { + const decoded = jwtDecode(accountRow.idToken) + if (decoded.email) return decoded.email + if (decoded.name) return decoded.name + } catch (_error) { + logger.warn(`[${requestId}] Error decoding ID token`, { + accountId: accountRow.accountId, + }) + } + } + + if (baseProvider === 'github') { + return `${accountRow.accountId} (GitHub)` + } + + try { + const userRecord = await db + .select({ email: user.email }) + .from(user) + .where(eq(user.id, accountRow.userId)) + .limit(1) + + if (userRecord.length > 0) { + return userRecord[0].email + } + } catch (_error) { + logger.warn(`[${requestId}] Error fetching user email`, { + userId: accountRow.userId, + }) + } + + return `${accountRow.accountId} (${baseProvider})` +} + /** * Get credentials for a specific provider */ @@ -46,6 +122,7 @@ export async function GET(request: NextRequest) { const rawQuery = { provider: searchParams.get('provider'), workflowId: searchParams.get('workflowId'), + workspaceId: searchParams.get('workspaceId'), credentialId: searchParams.get('credentialId'), } @@ -78,7 +155,7 @@ export async function GET(request: NextRequest) { ) } - const { provider: providerParam, workflowId, credentialId } = parseResult.data + const { provider: providerParam, workflowId, workspaceId, credentialId } = parseResult.data // Authenticate requester (supports session and internal JWT) const authResult = await checkSessionOrInternalAuth(request) @@ -88,7 +165,7 @@ export async function GET(request: NextRequest) { } const requesterUserId = authResult.userId - const effectiveUserId = requesterUserId + let effectiveWorkspaceId = workspaceId ?? undefined if (workflowId) { const workflowAuthorization = await authorizeWorkflowByWorkspacePermission({ workflowId, @@ -106,101 +183,145 @@ export async function GET(request: NextRequest) { { status: workflowAuthorization.status } ) } + effectiveWorkspaceId = workflowAuthorization.workflow?.workspaceId || undefined } - // Parse the provider to get base provider and feature type (if provider is present) - const { baseProvider } = parseProvider((providerParam || 'google') as OAuthProvider) + if (effectiveWorkspaceId) { + const workspaceAccess = await checkWorkspaceAccess(effectiveWorkspaceId, requesterUserId) + if (!workspaceAccess.hasAccess) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) + } + } let accountsData + if (credentialId) { + const [platformCredential] = await db + .select({ + id: credential.id, + workspaceId: credential.workspaceId, + type: credential.type, + displayName: credential.displayName, + providerId: credential.providerId, + accountId: credential.accountId, + accountProviderId: account.providerId, + accountScope: account.scope, + accountUpdatedAt: account.updatedAt, + }) + .from(credential) + .leftJoin(account, eq(credential.accountId, account.id)) + .where(eq(credential.id, credentialId)) + .limit(1) + + if (platformCredential) { + if (platformCredential.type !== 'oauth' || !platformCredential.accountId) { + return NextResponse.json({ credentials: [] }, { status: 200 }) + } + + if (workflowId) { + if (!effectiveWorkspaceId || platformCredential.workspaceId !== effectiveWorkspaceId) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) + } + } else { + const [membership] = await db + .select({ id: credentialMember.id }) + .from(credentialMember) + .where( + and( + eq(credentialMember.credentialId, platformCredential.id), + eq(credentialMember.userId, requesterUserId), + eq(credentialMember.status, 'active') + ) + ) + .limit(1) + + if (!membership) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) + } + } + + if (!platformCredential.accountProviderId || !platformCredential.accountUpdatedAt) { + return NextResponse.json({ credentials: [] }, { status: 200 }) + } + + return NextResponse.json( + { + credentials: [ + toCredentialResponse( + platformCredential.id, + platformCredential.displayName, + platformCredential.accountProviderId, + platformCredential.accountUpdatedAt, + platformCredential.accountScope + ), + ], + }, + { status: 200 } + ) + } + } + + if (effectiveWorkspaceId && providerParam) { + await syncWorkspaceOAuthCredentialsForUser({ + workspaceId: effectiveWorkspaceId, + userId: requesterUserId, + }) + + const credentialsData = await db + .select({ + id: credential.id, + displayName: credential.displayName, + providerId: account.providerId, + scope: account.scope, + updatedAt: account.updatedAt, + }) + .from(credential) + .innerJoin(account, eq(credential.accountId, account.id)) + .innerJoin( + credentialMember, + and( + eq(credentialMember.credentialId, credential.id), + eq(credentialMember.userId, requesterUserId), + eq(credentialMember.status, 'active') + ) + ) + .where( + and( + eq(credential.workspaceId, effectiveWorkspaceId), + eq(credential.type, 'oauth'), + eq(account.providerId, providerParam) + ) + ) + + return NextResponse.json( + { + credentials: credentialsData.map((row) => + toCredentialResponse(row.id, row.displayName, row.providerId, row.updatedAt, row.scope) + ), + }, + { status: 200 } + ) + } + if (credentialId && workflowId) { - // When both workflowId and credentialId are provided, fetch by ID only. - // Workspace authorization above already proves access; the credential - // may belong to another workspace member (e.g. for display name resolution). accountsData = await db.select().from(account).where(eq(account.id, credentialId)) } else if (credentialId) { accountsData = await db .select() .from(account) - .where(and(eq(account.userId, effectiveUserId), eq(account.id, credentialId))) + .where(and(eq(account.userId, requesterUserId), eq(account.id, credentialId))) } else { - // Fetch all credentials for provider and effective user accountsData = await db .select() .from(account) - .where(and(eq(account.userId, effectiveUserId), eq(account.providerId, providerParam!))) + .where(and(eq(account.userId, requesterUserId), eq(account.providerId, providerParam!))) } // Transform accounts into credentials const credentials = await Promise.all( accountsData.map(async (acc) => { - // Extract the feature type from providerId (e.g., 'google-default' -> 'default') - const [_, featureType = 'default'] = acc.providerId.split('-') - - // Try multiple methods to get a user-friendly display name - let displayName = '' - - // Method 1: Try to extract email from ID token (works for Google, etc.) - if (acc.idToken) { - try { - const decoded = jwtDecode(acc.idToken) - if (decoded.email) { - displayName = decoded.email - } else if (decoded.name) { - displayName = decoded.name - } - } catch (_error) { - logger.warn(`[${requestId}] Error decoding ID token`, { - accountId: acc.id, - }) - } - } - - // Method 2: For GitHub, the accountId might be the username - if (!displayName && baseProvider === 'github') { - displayName = `${acc.accountId} (GitHub)` - } - - // Method 3: Try to get the user's email from our database - if (!displayName) { - try { - const userRecord = await db - .select({ email: user.email }) - .from(user) - .where(eq(user.id, acc.userId)) - .limit(1) - - if (userRecord.length > 0) { - displayName = userRecord[0].email - } - } catch (_error) { - logger.warn(`[${requestId}] Error fetching user email`, { - userId: acc.userId, - }) - } - } - - // Fallback: Use accountId with provider type as context - if (!displayName) { - displayName = `${acc.accountId} (${baseProvider})` - } - - const storedScope = acc.scope?.trim() - const grantedScopes = storedScope ? storedScope.split(/[\s,]+/).filter(Boolean) : [] - const scopeEvaluation = evaluateScopeCoverage(acc.providerId, grantedScopes) - - return { - id: acc.id, - name: displayName, - provider: acc.providerId, - lastUsed: acc.updatedAt.toISOString(), - isDefault: featureType === 'default', - scopes: scopeEvaluation.grantedScopes, - canonicalScopes: scopeEvaluation.canonicalScopes, - missingScopes: scopeEvaluation.missingScopes, - extraScopes: scopeEvaluation.extraScopes, - requiresReauthorization: scopeEvaluation.requiresReauthorization, - } + const displayName = await getFallbackDisplayName(requestId, providerParam, acc) + return toCredentialResponse(acc.id, displayName, acc.providerId, acc.updatedAt, acc.scope) }) ) diff --git a/apps/sim/app/api/auth/oauth/disconnect/route.ts b/apps/sim/app/api/auth/oauth/disconnect/route.ts index be645aa73..2ac3ff2fc 100644 --- a/apps/sim/app/api/auth/oauth/disconnect/route.ts +++ b/apps/sim/app/api/auth/oauth/disconnect/route.ts @@ -15,6 +15,7 @@ const logger = createLogger('OAuthDisconnectAPI') const disconnectSchema = z.object({ provider: z.string({ required_error: 'Provider is required' }).min(1, 'Provider is required'), providerId: z.string().optional(), + accountId: z.string().optional(), }) /** @@ -50,15 +51,20 @@ export async function POST(request: NextRequest) { ) } - const { provider, providerId } = parseResult.data + const { provider, providerId, accountId } = parseResult.data logger.info(`[${requestId}] Processing OAuth disconnect request`, { provider, hasProviderId: !!providerId, }) - // If a specific providerId is provided, delete only that account - if (providerId) { + // If a specific account row ID is provided, delete that exact account + if (accountId) { + await db + .delete(account) + .where(and(eq(account.userId, session.user.id), eq(account.id, accountId))) + } else if (providerId) { + // If a specific providerId is provided, delete accounts for that provider ID await db .delete(account) .where(and(eq(account.userId, session.user.id), eq(account.providerId, providerId))) diff --git a/apps/sim/app/api/auth/oauth/microsoft/file/route.ts b/apps/sim/app/api/auth/oauth/microsoft/file/route.ts index af9d5d47e..c653d35bf 100644 --- a/apps/sim/app/api/auth/oauth/microsoft/file/route.ts +++ b/apps/sim/app/api/auth/oauth/microsoft/file/route.ts @@ -38,13 +38,18 @@ export async function GET(request: NextRequest) { return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status }) } - const credential = await getCredential(requestId, credentialId, authz.credentialOwnerUserId) + const resolvedCredentialId = authz.resolvedCredentialId || credentialId + const credential = await getCredential( + requestId, + resolvedCredentialId, + authz.credentialOwnerUserId + ) if (!credential) { return NextResponse.json({ error: 'Credential not found' }, { status: 404 }) } const accessToken = await refreshAccessTokenIfNeeded( - credentialId, + resolvedCredentialId, authz.credentialOwnerUserId, requestId ) diff --git a/apps/sim/app/api/auth/oauth/microsoft/files/route.ts b/apps/sim/app/api/auth/oauth/microsoft/files/route.ts index 1a689b808..23bd2e57e 100644 --- a/apps/sim/app/api/auth/oauth/microsoft/files/route.ts +++ b/apps/sim/app/api/auth/oauth/microsoft/files/route.ts @@ -37,14 +37,19 @@ export async function GET(request: NextRequest) { return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status }) } - const credential = await getCredential(requestId, credentialId, authz.credentialOwnerUserId) + const resolvedCredentialId = authz.resolvedCredentialId || credentialId + const credential = await getCredential( + requestId, + resolvedCredentialId, + authz.credentialOwnerUserId + ) if (!credential) { return NextResponse.json({ error: 'Credential not found' }, { status: 404 }) } // Refresh access token if needed using the utility function const accessToken = await refreshAccessTokenIfNeeded( - credentialId, + resolvedCredentialId, authz.credentialOwnerUserId, requestId ) diff --git a/apps/sim/app/api/auth/oauth/token/route.ts b/apps/sim/app/api/auth/oauth/token/route.ts index f6728fe69..0282495ae 100644 --- a/apps/sim/app/api/auth/oauth/token/route.ts +++ b/apps/sim/app/api/auth/oauth/token/route.ts @@ -119,14 +119,23 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 }) } - const credential = await getCredential(requestId, credentialId, authz.credentialOwnerUserId) + const resolvedCredentialId = authz.resolvedCredentialId || credentialId + const credential = await getCredential( + requestId, + resolvedCredentialId, + authz.credentialOwnerUserId + ) if (!credential) { return NextResponse.json({ error: 'Credential not found' }, { status: 404 }) } try { - const { accessToken } = await refreshTokenIfNeeded(requestId, credential, credentialId) + const { accessToken } = await refreshTokenIfNeeded( + requestId, + credential, + resolvedCredentialId + ) let instanceUrl: string | undefined if (credential.providerId === 'salesforce' && credential.scope) { @@ -186,13 +195,20 @@ export async function GET(request: NextRequest) { const { credentialId } = parseResult.data - // For GET requests, we only support session-based authentication - const auth = await checkSessionOrInternalAuth(request, { requireWorkflowId: false }) - if (!auth.success || auth.authType !== 'session' || !auth.userId) { - return NextResponse.json({ error: 'User not authenticated' }, { status: 401 }) + const authz = await authorizeCredentialUse(request, { + credentialId, + requireWorkflowIdForInternal: false, + }) + if (!authz.ok || authz.authType !== 'session' || !authz.credentialOwnerUserId) { + return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 }) } - const credential = await getCredential(requestId, credentialId, auth.userId) + const resolvedCredentialId = authz.resolvedCredentialId || credentialId + const credential = await getCredential( + requestId, + resolvedCredentialId, + authz.credentialOwnerUserId + ) if (!credential) { return NextResponse.json({ error: 'Credential not found' }, { status: 404 }) @@ -204,7 +220,11 @@ export async function GET(request: NextRequest) { } try { - const { accessToken } = await refreshTokenIfNeeded(requestId, credential, credentialId) + const { accessToken } = await refreshTokenIfNeeded( + requestId, + credential, + resolvedCredentialId + ) // For Salesforce, extract instanceUrl from the scope field let instanceUrl: string | undefined diff --git a/apps/sim/app/api/auth/oauth/utils.test.ts b/apps/sim/app/api/auth/oauth/utils.test.ts index ca1d2c8eb..902bcbadc 100644 --- a/apps/sim/app/api/auth/oauth/utils.test.ts +++ b/apps/sim/app/api/auth/oauth/utils.test.ts @@ -50,7 +50,7 @@ describe('OAuth Utils', () => { describe('getCredential', () => { it('should return credential when found', async () => { const mockCredential = { id: 'credential-id', userId: 'test-user-id' } - mockDbTyped.limit.mockReturnValueOnce([mockCredential]) + mockDbTyped.limit.mockReturnValueOnce([]).mockReturnValueOnce([mockCredential]) const credential = await getCredential('request-id', 'credential-id', 'test-user-id') @@ -59,7 +59,8 @@ describe('OAuth Utils', () => { expect(mockDbTyped.where).toHaveBeenCalled() expect(mockDbTyped.limit).toHaveBeenCalledWith(1) - expect(credential).toEqual(mockCredential) + expect(credential).toMatchObject(mockCredential) + expect(credential).toMatchObject({ resolvedCredentialId: 'credential-id' }) }) it('should return undefined when credential is not found', async () => { @@ -152,7 +153,7 @@ describe('OAuth Utils', () => { providerId: 'google', userId: 'test-user-id', } - mockDbTyped.limit.mockReturnValueOnce([mockCredential]) + mockDbTyped.limit.mockReturnValueOnce([]).mockReturnValueOnce([mockCredential]) const token = await refreshAccessTokenIfNeeded('credential-id', 'test-user-id', 'request-id') @@ -169,7 +170,7 @@ describe('OAuth Utils', () => { providerId: 'google', userId: 'test-user-id', } - mockDbTyped.limit.mockReturnValueOnce([mockCredential]) + mockDbTyped.limit.mockReturnValueOnce([]).mockReturnValueOnce([mockCredential]) mockRefreshOAuthToken.mockResolvedValueOnce({ accessToken: 'new-token', @@ -202,7 +203,7 @@ describe('OAuth Utils', () => { providerId: 'google', userId: 'test-user-id', } - mockDbTyped.limit.mockReturnValueOnce([mockCredential]) + mockDbTyped.limit.mockReturnValueOnce([]).mockReturnValueOnce([mockCredential]) mockRefreshOAuthToken.mockResolvedValueOnce(null) diff --git a/apps/sim/app/api/auth/oauth/utils.ts b/apps/sim/app/api/auth/oauth/utils.ts index 891e4ca4d..c6a462681 100644 --- a/apps/sim/app/api/auth/oauth/utils.ts +++ b/apps/sim/app/api/auth/oauth/utils.ts @@ -1,5 +1,5 @@ import { db } from '@sim/db' -import { account, credentialSetMember } from '@sim/db/schema' +import { account, credential, credentialSetMember } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { and, desc, eq, inArray } from 'drizzle-orm' import { refreshOAuthToken } from '@/lib/oauth' @@ -25,6 +25,28 @@ interface AccountInsertData { accessTokenExpiresAt?: Date } +async function resolveOAuthAccountId( + credentialId: string +): Promise<{ accountId: string; usedCredentialTable: boolean } | null> { + const [credentialRow] = await db + .select({ + type: credential.type, + accountId: credential.accountId, + }) + .from(credential) + .where(eq(credential.id, credentialId)) + .limit(1) + + if (credentialRow) { + if (credentialRow.type !== 'oauth' || !credentialRow.accountId) { + return null + } + return { accountId: credentialRow.accountId, usedCredentialTable: true } + } + + return { accountId: credentialId, usedCredentialTable: false } +} + /** * Safely inserts an account record, handling duplicate constraint violations gracefully. * If a duplicate is detected (unique constraint violation), logs a warning and returns success. @@ -52,10 +74,16 @@ export async function safeAccountInsert( * Get a credential by ID and verify it belongs to the user */ export async function getCredential(requestId: string, credentialId: string, userId: string) { + const resolved = await resolveOAuthAccountId(credentialId) + if (!resolved) { + logger.warn(`[${requestId}] Credential is not an OAuth credential`) + return undefined + } + const credentials = await db .select() .from(account) - .where(and(eq(account.id, credentialId), eq(account.userId, userId))) + .where(and(eq(account.id, resolved.accountId), eq(account.userId, userId))) .limit(1) if (!credentials.length) { @@ -63,7 +91,10 @@ export async function getCredential(requestId: string, credentialId: string, use return undefined } - return credentials[0] + return { + ...credentials[0], + resolvedCredentialId: resolved.accountId, + } } export async function getOAuthToken(userId: string, providerId: string): Promise { @@ -238,7 +269,9 @@ export async function refreshAccessTokenIfNeeded( } // Update the token in the database - await db.update(account).set(updateData).where(eq(account.id, credentialId)) + const resolvedCredentialId = + (credential as { resolvedCredentialId?: string }).resolvedCredentialId ?? credentialId + await db.update(account).set(updateData).where(eq(account.id, resolvedCredentialId)) logger.info(`[${requestId}] Successfully refreshed access token for credential`) return refreshedToken.accessToken @@ -274,6 +307,8 @@ export async function refreshTokenIfNeeded( credential: any, credentialId: string ): Promise<{ accessToken: string; refreshed: boolean }> { + const resolvedCredentialId = credential.resolvedCredentialId ?? credentialId + // Decide if we should refresh: token missing OR expired const accessTokenExpiresAt = credential.accessTokenExpiresAt const refreshTokenExpiresAt = credential.refreshTokenExpiresAt @@ -334,7 +369,7 @@ export async function refreshTokenIfNeeded( updateData.refreshTokenExpiresAt = getMicrosoftRefreshTokenExpiry() } - await db.update(account).set(updateData).where(eq(account.id, credentialId)) + await db.update(account).set(updateData).where(eq(account.id, resolvedCredentialId)) logger.info(`[${requestId}] Successfully refreshed access token`) return { accessToken: refreshedToken, refreshed: true } @@ -343,7 +378,7 @@ export async function refreshTokenIfNeeded( `[${requestId}] Refresh attempt failed, checking if another concurrent request succeeded` ) - const freshCredential = await getCredential(requestId, credentialId, credential.userId) + const freshCredential = await getCredential(requestId, resolvedCredentialId, credential.userId) if (freshCredential?.accessToken) { const freshExpiresAt = freshCredential.accessTokenExpiresAt const stillValid = !freshExpiresAt || freshExpiresAt > new Date() diff --git a/apps/sim/app/api/credentials/[id]/members/route.ts b/apps/sim/app/api/credentials/[id]/members/route.ts new file mode 100644 index 000000000..1d4a1dd17 --- /dev/null +++ b/apps/sim/app/api/credentials/[id]/members/route.ts @@ -0,0 +1,220 @@ +import { db } from '@sim/db' +import { credentialMember, user } from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { and, eq } from 'drizzle-orm' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { getSession } from '@/lib/auth' +import { getCredentialActorContext } from '@/lib/credentials/access' +import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils' + +const logger = createLogger('CredentialMembersAPI') + +const upsertMemberSchema = z.object({ + userId: z.string().min(1), + role: z.enum(['admin', 'member']), +}) + +const deleteMemberSchema = z.object({ + userId: z.string().min(1), +}) + +export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { + const session = await getSession() + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const { id } = await params + + try { + const access = await getCredentialActorContext(id, session.user.id) + if (!access.credential) { + return NextResponse.json({ error: 'Credential not found' }, { status: 404 }) + } + if (!access.hasWorkspaceAccess || !access.isAdmin) { + return NextResponse.json({ error: 'Credential admin permission required' }, { status: 403 }) + } + + const members = await db + .select({ + id: credentialMember.id, + userId: credentialMember.userId, + role: credentialMember.role, + status: credentialMember.status, + joinedAt: credentialMember.joinedAt, + invitedBy: credentialMember.invitedBy, + createdAt: credentialMember.createdAt, + updatedAt: credentialMember.updatedAt, + userName: user.name, + userEmail: user.email, + userImage: user.image, + }) + .from(credentialMember) + .leftJoin(user, eq(credentialMember.userId, user.id)) + .where(eq(credentialMember.credentialId, id)) + + return NextResponse.json({ members }, { status: 200 }) + } catch (error) { + logger.error('Failed to list credential members', error) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + } +} + +export async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { + const session = await getSession() + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const { id } = await params + + try { + const parseResult = upsertMemberSchema.safeParse(await request.json()) + if (!parseResult.success) { + return NextResponse.json({ error: parseResult.error.errors[0]?.message }, { status: 400 }) + } + + const access = await getCredentialActorContext(id, session.user.id) + if (!access.credential) { + return NextResponse.json({ error: 'Credential not found' }, { status: 404 }) + } + if (!access.hasWorkspaceAccess || !access.isAdmin) { + return NextResponse.json({ error: 'Credential admin permission required' }, { status: 403 }) + } + + const targetWorkspaceAccess = await checkWorkspaceAccess( + access.credential.workspaceId, + parseResult.data.userId + ) + if (!targetWorkspaceAccess.hasAccess) { + return NextResponse.json( + { error: 'User must have workspace access before being added to a credential' }, + { status: 400 } + ) + } + + const now = new Date() + const [existingMember] = await db + .select() + .from(credentialMember) + .where( + and( + eq(credentialMember.credentialId, id), + eq(credentialMember.userId, parseResult.data.userId) + ) + ) + .limit(1) + + if (existingMember) { + await db + .update(credentialMember) + .set({ + role: parseResult.data.role, + status: 'active', + joinedAt: existingMember.joinedAt ?? now, + invitedBy: session.user.id, + updatedAt: now, + }) + .where(eq(credentialMember.id, existingMember.id)) + } else { + await db.insert(credentialMember).values({ + id: crypto.randomUUID(), + credentialId: id, + userId: parseResult.data.userId, + role: parseResult.data.role, + status: 'active', + joinedAt: now, + invitedBy: session.user.id, + createdAt: now, + updatedAt: now, + }) + } + + return NextResponse.json({ success: true }, { status: 200 }) + } catch (error) { + logger.error('Failed to upsert credential member', error) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + } +} + +export async function DELETE( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + const session = await getSession() + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const { id } = await params + + try { + const parseResult = deleteMemberSchema.safeParse({ + userId: new URL(request.url).searchParams.get('userId'), + }) + if (!parseResult.success) { + return NextResponse.json({ error: parseResult.error.errors[0]?.message }, { status: 400 }) + } + + const access = await getCredentialActorContext(id, session.user.id) + if (!access.credential) { + return NextResponse.json({ error: 'Credential not found' }, { status: 404 }) + } + if (!access.hasWorkspaceAccess || !access.isAdmin) { + return NextResponse.json({ error: 'Credential admin permission required' }, { status: 403 }) + } + + const [memberToRevoke] = await db + .select() + .from(credentialMember) + .where( + and( + eq(credentialMember.credentialId, id), + eq(credentialMember.userId, parseResult.data.userId) + ) + ) + .limit(1) + + if (!memberToRevoke) { + return NextResponse.json({ error: 'Member not found' }, { status: 404 }) + } + + if (memberToRevoke.status !== 'active') { + return NextResponse.json({ success: true }, { status: 200 }) + } + + if (memberToRevoke.role === 'admin') { + const activeAdmins = await db + .select({ id: credentialMember.id }) + .from(credentialMember) + .where( + and( + eq(credentialMember.credentialId, id), + eq(credentialMember.role, 'admin'), + eq(credentialMember.status, 'active') + ) + ) + + if (activeAdmins.length <= 1) { + return NextResponse.json( + { error: 'Cannot revoke the last active admin from a credential' }, + { status: 400 } + ) + } + } + + await db + .update(credentialMember) + .set({ + status: 'revoked', + updatedAt: new Date(), + }) + .where(eq(credentialMember.id, memberToRevoke.id)) + + return NextResponse.json({ success: true }, { status: 200 }) + } catch (error) { + logger.error('Failed to revoke credential member', error) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + } +} diff --git a/apps/sim/app/api/credentials/[id]/route.ts b/apps/sim/app/api/credentials/[id]/route.ts new file mode 100644 index 000000000..1b2066b0c --- /dev/null +++ b/apps/sim/app/api/credentials/[id]/route.ts @@ -0,0 +1,147 @@ +import { db } from '@sim/db' +import { credential, credentialMember } from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { and, eq } from 'drizzle-orm' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { getSession } from '@/lib/auth' +import { getCredentialActorContext } from '@/lib/credentials/access' + +const logger = createLogger('CredentialByIdAPI') + +const updateCredentialSchema = z + .object({ + displayName: z.string().trim().min(1).max(255).optional(), + accountId: z.string().trim().min(1).optional(), + }) + .strict() + .refine((data) => Boolean(data.displayName || data.accountId), { + message: 'At least one field must be provided', + path: ['displayName'], + }) + +async function getCredentialResponse(credentialId: string, userId: string) { + const [row] = await db + .select({ + id: credential.id, + workspaceId: credential.workspaceId, + type: credential.type, + displayName: credential.displayName, + providerId: credential.providerId, + accountId: credential.accountId, + envKey: credential.envKey, + envOwnerUserId: credential.envOwnerUserId, + createdBy: credential.createdBy, + createdAt: credential.createdAt, + updatedAt: credential.updatedAt, + role: credentialMember.role, + status: credentialMember.status, + }) + .from(credential) + .innerJoin( + credentialMember, + and(eq(credentialMember.credentialId, credential.id), eq(credentialMember.userId, userId)) + ) + .where(eq(credential.id, credentialId)) + .limit(1) + + return row ?? null +} + +export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { + const session = await getSession() + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const { id } = await params + + try { + const access = await getCredentialActorContext(id, session.user.id) + if (!access.credential) { + return NextResponse.json({ error: 'Credential not found' }, { status: 404 }) + } + if (!access.hasWorkspaceAccess || !access.member) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) + } + + const row = await getCredentialResponse(id, session.user.id) + return NextResponse.json({ credential: row }, { status: 200 }) + } catch (error) { + logger.error('Failed to fetch credential', error) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + } +} + +export async function PUT(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { + const session = await getSession() + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const { id } = await params + + try { + const parseResult = updateCredentialSchema.safeParse(await request.json()) + if (!parseResult.success) { + return NextResponse.json({ error: parseResult.error.errors[0]?.message }, { status: 400 }) + } + + const access = await getCredentialActorContext(id, session.user.id) + if (!access.credential) { + return NextResponse.json({ error: 'Credential not found' }, { status: 404 }) + } + if (!access.hasWorkspaceAccess || !access.isAdmin) { + return NextResponse.json({ error: 'Credential admin permission required' }, { status: 403 }) + } + + if (access.credential.type === 'oauth') { + return NextResponse.json( + { + error: + 'OAuth credential editing is disabled. Connect an account and create or use its linked credential.', + }, + { status: 400 } + ) + } + + return NextResponse.json( + { + error: + 'Environment credentials cannot be updated via this endpoint. Use the environment value editor in credentials settings.', + }, + { status: 400 } + ) + } catch (error) { + logger.error('Failed to update credential', error) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + } +} + +export async function DELETE( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + const session = await getSession() + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const { id } = await params + + try { + const access = await getCredentialActorContext(id, session.user.id) + if (!access.credential) { + return NextResponse.json({ error: 'Credential not found' }, { status: 404 }) + } + if (!access.hasWorkspaceAccess || !access.isAdmin) { + return NextResponse.json({ error: 'Credential admin permission required' }, { status: 403 }) + } + + await db.delete(credential).where(eq(credential.id, id)) + return NextResponse.json({ success: true }, { status: 200 }) + } catch (error) { + logger.error('Failed to delete credential', error) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + } +} diff --git a/apps/sim/app/api/credentials/bootstrap/route.ts b/apps/sim/app/api/credentials/bootstrap/route.ts new file mode 100644 index 000000000..b36026f5c --- /dev/null +++ b/apps/sim/app/api/credentials/bootstrap/route.ts @@ -0,0 +1,81 @@ +import { db } from '@sim/db' +import { environment, workspaceEnvironment } from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { eq } from 'drizzle-orm' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { getSession } from '@/lib/auth' +import { + syncPersonalEnvCredentialsForUser, + syncWorkspaceEnvCredentials, +} from '@/lib/credentials/environment' +import { syncWorkspaceOAuthCredentialsForUser } from '@/lib/credentials/oauth' +import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils' + +const logger = createLogger('CredentialsBootstrapAPI') + +const bootstrapSchema = z.object({ + workspaceId: z.string().uuid('Workspace ID must be a valid UUID'), +}) + +/** + * Ensures the current user's connected accounts and env vars are reflected as workspace credentials. + */ +export async function POST(request: NextRequest) { + const session = await getSession() + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + try { + const parseResult = bootstrapSchema.safeParse(await request.json()) + if (!parseResult.success) { + return NextResponse.json({ error: parseResult.error.errors[0]?.message }, { status: 400 }) + } + + const { workspaceId } = parseResult.data + const workspaceAccess = await checkWorkspaceAccess(workspaceId, session.user.id) + if (!workspaceAccess.hasAccess) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) + } + + const [personalRow, workspaceRow] = await Promise.all([ + db + .select({ variables: environment.variables }) + .from(environment) + .where(eq(environment.userId, session.user.id)) + .limit(1), + db + .select({ variables: workspaceEnvironment.variables }) + .from(workspaceEnvironment) + .where(eq(workspaceEnvironment.workspaceId, workspaceId)) + .limit(1), + ]) + + const personalKeys = Object.keys((personalRow[0]?.variables as Record) || {}) + const workspaceKeys = Object.keys((workspaceRow[0]?.variables as Record) || {}) + + const [oauthSyncResult] = await Promise.all([ + syncWorkspaceOAuthCredentialsForUser({ workspaceId, userId: session.user.id }), + syncPersonalEnvCredentialsForUser({ userId: session.user.id, envKeys: personalKeys }), + syncWorkspaceEnvCredentials({ + workspaceId, + envKeys: workspaceKeys, + actingUserId: session.user.id, + }), + ]) + + return NextResponse.json({ + success: true, + synced: { + oauthCreated: oauthSyncResult.createdCredentials, + oauthMembershipsUpdated: oauthSyncResult.updatedMemberships, + personalEnvKeys: personalKeys.length, + workspaceEnvKeys: workspaceKeys.length, + }, + }) + } catch (error) { + logger.error('Failed to bootstrap workspace credentials', error) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + } +} diff --git a/apps/sim/app/api/credentials/memberships/route.ts b/apps/sim/app/api/credentials/memberships/route.ts new file mode 100644 index 000000000..4fdf8379f --- /dev/null +++ b/apps/sim/app/api/credentials/memberships/route.ts @@ -0,0 +1,112 @@ +import { db } from '@sim/db' +import { credential, credentialMember } from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { and, eq } from 'drizzle-orm' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { getSession } from '@/lib/auth' + +const logger = createLogger('CredentialMembershipsAPI') + +const leaveCredentialSchema = z.object({ + credentialId: z.string().min(1), +}) + +export async function GET() { + const session = await getSession() + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + try { + const memberships = await db + .select({ + membershipId: credentialMember.id, + credentialId: credential.id, + workspaceId: credential.workspaceId, + type: credential.type, + displayName: credential.displayName, + providerId: credential.providerId, + role: credentialMember.role, + status: credentialMember.status, + joinedAt: credentialMember.joinedAt, + }) + .from(credentialMember) + .innerJoin(credential, eq(credentialMember.credentialId, credential.id)) + .where(eq(credentialMember.userId, session.user.id)) + + return NextResponse.json({ memberships }, { status: 200 }) + } catch (error) { + logger.error('Failed to list credential memberships', error) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + } +} + +export async function DELETE(request: NextRequest) { + const session = await getSession() + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + try { + const parseResult = leaveCredentialSchema.safeParse({ + credentialId: new URL(request.url).searchParams.get('credentialId'), + }) + if (!parseResult.success) { + return NextResponse.json({ error: parseResult.error.errors[0]?.message }, { status: 400 }) + } + + const { credentialId } = parseResult.data + const [membership] = await db + .select() + .from(credentialMember) + .where( + and( + eq(credentialMember.credentialId, credentialId), + eq(credentialMember.userId, session.user.id) + ) + ) + .limit(1) + + if (!membership) { + return NextResponse.json({ error: 'Membership not found' }, { status: 404 }) + } + + if (membership.status !== 'active') { + return NextResponse.json({ success: true }, { status: 200 }) + } + + if (membership.role === 'admin') { + const activeAdmins = await db + .select({ id: credentialMember.id }) + .from(credentialMember) + .where( + and( + eq(credentialMember.credentialId, credentialId), + eq(credentialMember.role, 'admin'), + eq(credentialMember.status, 'active') + ) + ) + + if (activeAdmins.length <= 1) { + return NextResponse.json( + { error: 'Cannot leave credential as the last active admin' }, + { status: 400 } + ) + } + } + + await db + .update(credentialMember) + .set({ + status: 'revoked', + updatedAt: new Date(), + }) + .where(eq(credentialMember.id, membership.id)) + + return NextResponse.json({ success: true }, { status: 200 }) + } catch (error) { + logger.error('Failed to leave credential', error) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + } +} diff --git a/apps/sim/app/api/credentials/route.ts b/apps/sim/app/api/credentials/route.ts new file mode 100644 index 000000000..87d71d3b0 --- /dev/null +++ b/apps/sim/app/api/credentials/route.ts @@ -0,0 +1,468 @@ +import { db } from '@sim/db' +import { account, credential, credentialMember, workspace } from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { and, eq } from 'drizzle-orm' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { getSession } from '@/lib/auth' +import { generateRequestId } from '@/lib/core/utils/request' +import { getWorkspaceMemberUserIds } from '@/lib/credentials/environment' +import { syncWorkspaceOAuthCredentialsForUser } from '@/lib/credentials/oauth' +import { getServiceConfigByProviderId } from '@/lib/oauth' +import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils' +import { isValidEnvVarName } from '@/executor/constants' + +const logger = createLogger('CredentialsAPI') + +const credentialTypeSchema = z.enum(['oauth', 'env_workspace', 'env_personal']) + +function normalizeEnvKeyInput(raw: string): string { + const trimmed = raw.trim() + const wrappedMatch = /^\{\{\s*([A-Za-z0-9_]+)\s*\}\}$/.exec(trimmed) + return wrappedMatch ? wrappedMatch[1] : trimmed +} + +const listCredentialsSchema = z.object({ + workspaceId: z.string().uuid('Workspace ID must be a valid UUID'), + type: credentialTypeSchema.optional(), + providerId: z.string().optional(), +}) + +const createCredentialSchema = z + .object({ + workspaceId: z.string().uuid('Workspace ID must be a valid UUID'), + type: credentialTypeSchema, + displayName: z.string().trim().min(1).max(255).optional(), + providerId: z.string().trim().min(1).optional(), + accountId: z.string().trim().min(1).optional(), + envKey: z.string().trim().min(1).optional(), + envOwnerUserId: z.string().trim().min(1).optional(), + }) + .superRefine((data, ctx) => { + if (data.type === 'oauth') { + if (!data.accountId) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'accountId is required for oauth credentials', + path: ['accountId'], + }) + } + if (!data.providerId) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'providerId is required for oauth credentials', + path: ['providerId'], + }) + } + if (!data.displayName) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'displayName is required for oauth credentials', + path: ['displayName'], + }) + } + return + } + + const normalizedEnvKey = data.envKey ? normalizeEnvKeyInput(data.envKey) : '' + if (!normalizedEnvKey) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'envKey is required for env credentials', + path: ['envKey'], + }) + return + } + + if (!isValidEnvVarName(normalizedEnvKey)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'envKey must contain only letters, numbers, and underscores', + path: ['envKey'], + }) + } + }) + +interface ExistingCredentialSourceParams { + workspaceId: string + type: 'oauth' | 'env_workspace' | 'env_personal' + accountId?: string | null + envKey?: string | null + envOwnerUserId?: string | null +} + +async function findExistingCredentialBySource(params: ExistingCredentialSourceParams) { + const { workspaceId, type, accountId, envKey, envOwnerUserId } = params + + if (type === 'oauth' && accountId) { + const [row] = await db + .select() + .from(credential) + .where( + and( + eq(credential.workspaceId, workspaceId), + eq(credential.type, 'oauth'), + eq(credential.accountId, accountId) + ) + ) + .limit(1) + return row ?? null + } + + if (type === 'env_workspace' && envKey) { + const [row] = await db + .select() + .from(credential) + .where( + and( + eq(credential.workspaceId, workspaceId), + eq(credential.type, 'env_workspace'), + eq(credential.envKey, envKey) + ) + ) + .limit(1) + return row ?? null + } + + if (type === 'env_personal' && envKey && envOwnerUserId) { + const [row] = await db + .select() + .from(credential) + .where( + and( + eq(credential.workspaceId, workspaceId), + eq(credential.type, 'env_personal'), + eq(credential.envKey, envKey), + eq(credential.envOwnerUserId, envOwnerUserId) + ) + ) + .limit(1) + return row ?? null + } + + return null +} + +export async function GET(request: NextRequest) { + const requestId = generateRequestId() + const session = await getSession() + + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + try { + const { searchParams } = new URL(request.url) + const rawWorkspaceId = searchParams.get('workspaceId') + const rawType = searchParams.get('type') + const rawProviderId = searchParams.get('providerId') + const parseResult = listCredentialsSchema.safeParse({ + workspaceId: rawWorkspaceId?.trim(), + type: rawType?.trim() || undefined, + providerId: rawProviderId?.trim() || undefined, + }) + + if (!parseResult.success) { + logger.warn(`[${requestId}] Invalid credential list request`, { + workspaceId: rawWorkspaceId, + type: rawType, + providerId: rawProviderId, + errors: parseResult.error.errors, + }) + return NextResponse.json({ error: parseResult.error.errors[0]?.message }, { status: 400 }) + } + + const { workspaceId, type, providerId } = parseResult.data + const workspaceAccess = await checkWorkspaceAccess(workspaceId, session.user.id) + + if (!workspaceAccess.hasAccess) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) + } + + if (!type || type === 'oauth') { + await syncWorkspaceOAuthCredentialsForUser({ workspaceId, userId: session.user.id }) + } + + const whereClauses = [ + eq(credential.workspaceId, workspaceId), + eq(credentialMember.userId, session.user.id), + eq(credentialMember.status, 'active'), + ] + + if (type) { + whereClauses.push(eq(credential.type, type)) + } + if (providerId) { + whereClauses.push(eq(credential.providerId, providerId)) + } + + const credentials = await db + .select({ + id: credential.id, + workspaceId: credential.workspaceId, + type: credential.type, + displayName: credential.displayName, + providerId: credential.providerId, + accountId: credential.accountId, + envKey: credential.envKey, + envOwnerUserId: credential.envOwnerUserId, + createdBy: credential.createdBy, + createdAt: credential.createdAt, + updatedAt: credential.updatedAt, + role: credentialMember.role, + }) + .from(credential) + .innerJoin( + credentialMember, + and( + eq(credentialMember.credentialId, credential.id), + eq(credentialMember.userId, session.user.id), + eq(credentialMember.status, 'active') + ) + ) + .where(and(...whereClauses)) + + return NextResponse.json({ credentials }) + } catch (error) { + logger.error(`[${requestId}] Failed to list credentials`, error) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + } +} + +export async function POST(request: NextRequest) { + const requestId = generateRequestId() + const session = await getSession() + + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + try { + const body = await request.json() + const parseResult = createCredentialSchema.safeParse(body) + + if (!parseResult.success) { + return NextResponse.json({ error: parseResult.error.errors[0]?.message }, { status: 400 }) + } + + const { workspaceId, type, displayName, providerId, accountId, envKey, envOwnerUserId } = + parseResult.data + + const workspaceAccess = await checkWorkspaceAccess(workspaceId, session.user.id) + if (!workspaceAccess.canWrite) { + return NextResponse.json({ error: 'Write permission required' }, { status: 403 }) + } + + let resolvedDisplayName = displayName?.trim() ?? '' + let resolvedProviderId: string | null = providerId ?? null + let resolvedAccountId: string | null = accountId ?? null + const resolvedEnvKey: string | null = envKey ? normalizeEnvKeyInput(envKey) : null + let resolvedEnvOwnerUserId: string | null = null + + if (type === 'oauth') { + const [accountRow] = await db + .select({ + id: account.id, + userId: account.userId, + providerId: account.providerId, + accountId: account.accountId, + }) + .from(account) + .where(eq(account.id, accountId!)) + .limit(1) + + if (!accountRow) { + return NextResponse.json({ error: 'OAuth account not found' }, { status: 404 }) + } + + if (accountRow.userId !== session.user.id) { + return NextResponse.json( + { error: 'Only account owners can create oauth credentials for an account' }, + { status: 403 } + ) + } + + if (providerId !== accountRow.providerId) { + return NextResponse.json( + { error: 'providerId does not match the selected OAuth account' }, + { status: 400 } + ) + } + if (!resolvedDisplayName) { + resolvedDisplayName = + getServiceConfigByProviderId(accountRow.providerId)?.name || accountRow.providerId + } + } else if (type === 'env_personal') { + resolvedEnvOwnerUserId = envOwnerUserId ?? session.user.id + if (resolvedEnvOwnerUserId !== session.user.id) { + return NextResponse.json( + { error: 'Only the current user can create personal env credentials for themselves' }, + { status: 403 } + ) + } + resolvedProviderId = null + resolvedAccountId = null + resolvedDisplayName = resolvedEnvKey || '' + } else { + resolvedProviderId = null + resolvedAccountId = null + resolvedEnvOwnerUserId = null + resolvedDisplayName = resolvedEnvKey || '' + } + + if (!resolvedDisplayName) { + return NextResponse.json({ error: 'Display name is required' }, { status: 400 }) + } + + const existingCredential = await findExistingCredentialBySource({ + workspaceId, + type, + accountId: resolvedAccountId, + envKey: resolvedEnvKey, + envOwnerUserId: resolvedEnvOwnerUserId, + }) + + if (existingCredential) { + const [membership] = await db + .select({ + id: credentialMember.id, + status: credentialMember.status, + role: credentialMember.role, + }) + .from(credentialMember) + .where( + and( + eq(credentialMember.credentialId, existingCredential.id), + eq(credentialMember.userId, session.user.id) + ) + ) + .limit(1) + + if (!membership || membership.status !== 'active') { + return NextResponse.json( + { error: 'A credential with this source already exists in this workspace' }, + { status: 409 } + ) + } + + if ( + type === 'oauth' && + membership.role === 'admin' && + resolvedDisplayName && + resolvedDisplayName !== existingCredential.displayName + ) { + await db + .update(credential) + .set({ + displayName: resolvedDisplayName, + updatedAt: new Date(), + }) + .where(eq(credential.id, existingCredential.id)) + + const [updatedCredential] = await db + .select() + .from(credential) + .where(eq(credential.id, existingCredential.id)) + .limit(1) + + return NextResponse.json( + { credential: updatedCredential ?? existingCredential }, + { status: 200 } + ) + } + + return NextResponse.json({ credential: existingCredential }, { status: 200 }) + } + + const now = new Date() + const credentialId = crypto.randomUUID() + const [workspaceRow] = await db + .select({ ownerId: workspace.ownerId }) + .from(workspace) + .where(eq(workspace.id, workspaceId)) + .limit(1) + + await db.transaction(async (tx) => { + await tx.insert(credential).values({ + id: credentialId, + workspaceId, + type, + displayName: resolvedDisplayName, + providerId: resolvedProviderId, + accountId: resolvedAccountId, + envKey: resolvedEnvKey, + envOwnerUserId: resolvedEnvOwnerUserId, + createdBy: session.user.id, + createdAt: now, + updatedAt: now, + }) + + if (type === 'env_workspace' && workspaceRow?.ownerId) { + const workspaceUserIds = await getWorkspaceMemberUserIds(workspaceId) + if (workspaceUserIds.length > 0) { + for (const memberUserId of workspaceUserIds) { + await tx.insert(credentialMember).values({ + id: crypto.randomUUID(), + credentialId, + userId: memberUserId, + role: memberUserId === workspaceRow.ownerId ? 'admin' : 'member', + status: 'active', + joinedAt: now, + invitedBy: session.user.id, + createdAt: now, + updatedAt: now, + }) + } + } + } else { + await tx.insert(credentialMember).values({ + id: crypto.randomUUID(), + credentialId, + userId: session.user.id, + role: 'admin', + status: 'active', + joinedAt: now, + invitedBy: session.user.id, + createdAt: now, + updatedAt: now, + }) + } + }) + + const [created] = await db + .select() + .from(credential) + .where(eq(credential.id, credentialId)) + .limit(1) + + return NextResponse.json({ credential: created }, { status: 201 }) + } catch (error: any) { + if (error?.code === '23505') { + return NextResponse.json( + { error: 'A credential with this source already exists' }, + { status: 409 } + ) + } + if (error?.code === '23503') { + return NextResponse.json( + { error: 'Invalid credential reference or membership target' }, + { status: 400 } + ) + } + if (error?.code === '23514') { + return NextResponse.json( + { error: 'Credential source data failed validation checks' }, + { status: 400 } + ) + } + logger.error(`[${requestId}] Credential create failure details`, { + code: error?.code, + detail: error?.detail, + constraint: error?.constraint, + table: error?.table, + message: error?.message, + }) + logger.error(`[${requestId}] Failed to create credential`, error) + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) + } +} diff --git a/apps/sim/app/api/environment/route.ts b/apps/sim/app/api/environment/route.ts index ad2818b0d..c8e8604d2 100644 --- a/apps/sim/app/api/environment/route.ts +++ b/apps/sim/app/api/environment/route.ts @@ -7,6 +7,7 @@ import { z } from 'zod' import { getSession } from '@/lib/auth' import { decryptSecret, encryptSecret } from '@/lib/core/security/encryption' import { generateRequestId } from '@/lib/core/utils/request' +import { syncPersonalEnvCredentialsForUser } from '@/lib/credentials/environment' import type { EnvironmentVariable } from '@/stores/settings/environment' const logger = createLogger('EnvironmentAPI') @@ -53,6 +54,11 @@ export async function POST(req: NextRequest) { }, }) + await syncPersonalEnvCredentialsForUser({ + userId: session.user.id, + envKeys: Object.keys(variables), + }) + return NextResponse.json({ success: true }) } catch (validationError) { if (validationError instanceof z.ZodError) { diff --git a/apps/sim/app/api/workspaces/[id]/environment/route.ts b/apps/sim/app/api/workspaces/[id]/environment/route.ts index f11da0ecc..a66849448 100644 --- a/apps/sim/app/api/workspaces/[id]/environment/route.ts +++ b/apps/sim/app/api/workspaces/[id]/environment/route.ts @@ -1,12 +1,14 @@ import { db } from '@sim/db' -import { environment, workspaceEnvironment } from '@sim/db/schema' +import { workspaceEnvironment } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { getSession } from '@/lib/auth' -import { decryptSecret, encryptSecret } from '@/lib/core/security/encryption' +import { encryptSecret } from '@/lib/core/security/encryption' import { generateRequestId } from '@/lib/core/utils/request' +import { syncWorkspaceEnvCredentials } from '@/lib/credentials/environment' +import { getPersonalAndWorkspaceEnv } from '@/lib/environment/utils' import { getUserEntityPermissions, getWorkspaceById } from '@/lib/workspaces/permissions/utils' const logger = createLogger('WorkspaceEnvironmentAPI') @@ -44,44 +46,10 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } - // Workspace env (encrypted) - const wsEnvRow = await db - .select() - .from(workspaceEnvironment) - .where(eq(workspaceEnvironment.workspaceId, workspaceId)) - .limit(1) - - const wsEncrypted: Record = (wsEnvRow[0]?.variables as any) || {} - - // Personal env (encrypted) - const personalRow = await db - .select() - .from(environment) - .where(eq(environment.userId, userId)) - .limit(1) - - const personalEncrypted: Record = (personalRow[0]?.variables as any) || {} - - // Decrypt both for UI - const decryptAll = async (src: Record) => { - const out: Record = {} - for (const [k, v] of Object.entries(src)) { - try { - const { decrypted } = await decryptSecret(v) - out[k] = decrypted - } catch { - out[k] = '' - } - } - return out - } - - const [workspaceDecrypted, personalDecrypted] = await Promise.all([ - decryptAll(wsEncrypted), - decryptAll(personalEncrypted), - ]) - - const conflicts = Object.keys(personalDecrypted).filter((k) => k in workspaceDecrypted) + const { workspaceDecrypted, personalDecrypted, conflicts } = await getPersonalAndWorkspaceEnv( + userId, + workspaceId + ) return NextResponse.json( { @@ -156,6 +124,12 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{ set: { variables: merged, updatedAt: new Date() }, }) + await syncWorkspaceEnvCredentials({ + workspaceId, + envKeys: Object.keys(merged), + actingUserId: userId, + }) + return NextResponse.json({ success: true }) } catch (error: any) { logger.error(`[${requestId}] Workspace env PUT error`, error) @@ -222,6 +196,12 @@ export async function DELETE( set: { variables: current, updatedAt: new Date() }, }) + await syncWorkspaceEnvCredentials({ + workspaceId, + envKeys: Object.keys(current), + actingUserId: userId, + }) + return NextResponse.json({ success: true }) } catch (error: any) { logger.error(`[${requestId}] Workspace env DELETE error`, error) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx index 4888a9684..671a7c3f6 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx @@ -30,6 +30,7 @@ export interface OAuthRequiredModalProps { requiredScopes?: string[] serviceId: string newScopes?: string[] + onConnect?: () => Promise | void } const SCOPE_DESCRIPTIONS: Record = { @@ -314,6 +315,7 @@ export function OAuthRequiredModal({ requiredScopes = [], serviceId, newScopes = [], + onConnect, }: OAuthRequiredModalProps) { const [error, setError] = useState(null) const { baseProvider } = parseProvider(provider) @@ -359,6 +361,12 @@ export function OAuthRequiredModal({ setError(null) try { + if (onConnect) { + await onConnect() + onClose() + return + } + const providerId = getProviderIdFromServiceId(serviceId) logger.info('Linking OAuth2:', { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/credential-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/credential-selector.tsx index 378a9baed..e8f5684e1 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/credential-selector.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/credential-selector.tsx @@ -3,10 +3,12 @@ import { createElement, useCallback, useEffect, useMemo, useState } from 'react' import { createLogger } from '@sim/logger' import { ExternalLink, Users } from 'lucide-react' +import { useParams } from 'next/navigation' import { Button, Combobox } from '@/components/emcn/components' import { getSubscriptionStatus } from '@/lib/billing/client' import { getEnv, isTruthy } from '@/lib/core/config/env' import { getPollingProviderFromOAuth } from '@/lib/credential-sets/providers' +import { writePendingCredentialCreateRequest } from '@/lib/credentials/client-state' import { getCanonicalScopesForProvider, getProviderIdFromServiceId, @@ -18,9 +20,9 @@ import { OAuthRequiredModal } from '@/app/workspace/[workspaceId]/w/[workflowId] import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate' import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value' import type { SubBlockConfig } from '@/blocks/types' -import { CREDENTIAL, CREDENTIAL_SET } from '@/executor/constants' +import { CREDENTIAL_SET } from '@/executor/constants' import { useCredentialSets } from '@/hooks/queries/credential-sets' -import { useOAuthCredentialDetail, useOAuthCredentials } from '@/hooks/queries/oauth-credentials' +import { useOAuthCredentials } from '@/hooks/queries/oauth-credentials' import { useOrganizations } from '@/hooks/queries/organization' import { useSubscriptionData } from '@/hooks/queries/subscription' import { getMissingRequiredScopes } from '@/hooks/use-oauth-scope-status' @@ -46,6 +48,8 @@ export function CredentialSelector({ previewValue, previewContextValues, }: CredentialSelectorProps) { + const params = useParams() + const workspaceId = (params?.workspaceId as string) || '' const [showOAuthModal, setShowOAuthModal] = useState(false) const [editingValue, setEditingValue] = useState('') const [isEditing, setIsEditing] = useState(false) @@ -96,53 +100,32 @@ export function CredentialSelector({ data: credentials = [], isFetching: credentialsLoading, refetch: refetchCredentials, - } = useOAuthCredentials(effectiveProviderId, Boolean(effectiveProviderId)) + } = useOAuthCredentials(effectiveProviderId, { + enabled: Boolean(effectiveProviderId), + workspaceId, + workflowId: activeWorkflowId || undefined, + }) const selectedCredential = useMemo( () => credentials.find((cred) => cred.id === selectedId), [credentials, selectedId] ) - const shouldFetchForeignMeta = - Boolean(selectedId) && - !selectedCredential && - Boolean(activeWorkflowId) && - Boolean(effectiveProviderId) - - const { data: foreignCredentials = [], isFetching: foreignMetaLoading } = - useOAuthCredentialDetail( - shouldFetchForeignMeta ? selectedId : undefined, - activeWorkflowId || undefined, - shouldFetchForeignMeta - ) - - const hasForeignMeta = foreignCredentials.length > 0 - const isForeign = Boolean(selectedId && !selectedCredential && hasForeignMeta) - const selectedCredentialSet = useMemo( () => credentialSets.find((cs) => cs.id === selectedCredentialSetId), [credentialSets, selectedCredentialSetId] ) - const isForeignCredentialSet = Boolean(isCredentialSetSelected && !selectedCredentialSet) - const resolvedLabel = useMemo(() => { if (selectedCredentialSet) return selectedCredentialSet.name - if (isForeignCredentialSet) return CREDENTIAL.FOREIGN_LABEL if (selectedCredential) return selectedCredential.name - if (isForeign) return CREDENTIAL.FOREIGN_LABEL return '' - }, [selectedCredentialSet, isForeignCredentialSet, selectedCredential, isForeign]) + }, [selectedCredentialSet, selectedCredential]) const displayValue = isEditing ? editingValue : resolvedLabel const invalidSelection = - !isPreview && - Boolean(selectedId) && - !selectedCredential && - !hasForeignMeta && - !credentialsLoading && - !foreignMetaLoading + !isPreview && Boolean(selectedId) && !selectedCredential && !credentialsLoading useEffect(() => { if (!invalidSelection) return @@ -153,7 +136,7 @@ export function CredentialSelector({ setStoreValue('') }, [invalidSelection, selectedId, effectiveProviderId, setStoreValue]) - useCredentialRefreshTriggers(refetchCredentials) + useCredentialRefreshTriggers(refetchCredentials, effectiveProviderId, workspaceId) const handleOpenChange = useCallback( (isOpen: boolean) => { @@ -195,8 +178,18 @@ export function CredentialSelector({ ) const handleAddCredential = useCallback(() => { - setShowOAuthModal(true) - }, []) + writePendingCredentialCreateRequest({ + workspaceId, + type: 'oauth', + providerId: effectiveProviderId, + displayName: '', + serviceId, + requiredScopes: getCanonicalScopesForProvider(effectiveProviderId), + requestedAt: Date.now(), + }) + + window.dispatchEvent(new CustomEvent('open-settings', { detail: { tab: 'credentials' } })) + }, [workspaceId, effectiveProviderId, serviceId]) const getProviderIcon = useCallback((providerName: OAuthProvider) => { const { baseProvider } = parseProvider(providerName) @@ -251,23 +244,18 @@ export function CredentialSelector({ label: cred.name, value: cred.id, })) + credentialItems.push({ + label: + credentials.length > 0 + ? `Connect another ${getProviderName(provider)} account` + : `Connect ${getProviderName(provider)} account`, + value: '__connect_account__', + }) - if (credentialItems.length > 0) { - groups.push({ - section: 'Personal Credential', - items: credentialItems, - }) - } else { - groups.push({ - section: 'Personal Credential', - items: [ - { - label: `Connect ${getProviderName(provider)} account`, - value: '__connect_account__', - }, - ], - }) - } + groups.push({ + section: 'Personal Credential', + items: credentialItems, + }) return { comboboxOptions: [], comboboxGroups: groups } } @@ -277,12 +265,13 @@ export function CredentialSelector({ value: cred.id, })) - if (credentials.length === 0) { - options.push({ - label: `Connect ${getProviderName(provider)} account`, - value: '__connect_account__', - }) - } + options.push({ + label: + credentials.length > 0 + ? `Connect another ${getProviderName(provider)} account` + : `Connect ${getProviderName(provider)} account`, + value: '__connect_account__', + }) return { comboboxOptions: options, comboboxGroups: undefined } }, [ @@ -368,7 +357,7 @@ export function CredentialSelector({ } disabled={effectiveDisabled} editable={true} - filterOptions={!isForeign && !isForeignCredentialSet} + filterOptions={true} isLoading={credentialsLoading} overlayContent={overlayContent} className={selectedId || isCredentialSetSelected ? 'pl-[28px]' : ''} @@ -380,15 +369,13 @@ export function CredentialSelector({ Additional permissions required - {!isForeign && ( - - )} + )} @@ -407,7 +394,11 @@ export function CredentialSelector({ ) } -function useCredentialRefreshTriggers(refetchCredentials: () => Promise) { +function useCredentialRefreshTriggers( + refetchCredentials: () => Promise, + providerId: string, + workspaceId: string +) { useEffect(() => { const refresh = () => { void refetchCredentials() @@ -425,12 +416,29 @@ function useCredentialRefreshTriggers(refetchCredentials: () => Promise } } + const handleCredentialsUpdated = ( + event: CustomEvent<{ providerId?: string; workspaceId?: string }> + ) => { + if (event.detail?.providerId && event.detail.providerId !== providerId) { + return + } + if (event.detail?.workspaceId && workspaceId && event.detail.workspaceId !== workspaceId) { + return + } + refresh() + } + document.addEventListener('visibilitychange', handleVisibilityChange) window.addEventListener('pageshow', handlePageShow) + window.addEventListener('oauth-credentials-updated', handleCredentialsUpdated as EventListener) return () => { document.removeEventListener('visibilitychange', handleVisibilityChange) window.removeEventListener('pageshow', handlePageShow) + window.removeEventListener( + 'oauth-credentials-updated', + handleCredentialsUpdated as EventListener + ) } - }, [refetchCredentials]) + }, [providerId, workspaceId, refetchCredentials]) } diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/env-var-dropdown.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/env-var-dropdown.tsx index 32a6dd33c..96b11ebf2 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/env-var-dropdown.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/env-var-dropdown.tsx @@ -168,7 +168,7 @@ export const EnvVarDropdown: React.FC = ({ }, [searchTerm]) const openEnvironmentSettings = () => { - window.dispatchEvent(new CustomEvent('open-settings', { detail: { tab: 'environment' } })) + window.dispatchEvent(new CustomEvent('open-settings', { detail: { tab: 'credentials' } })) onClose?.() } diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/file-selector/file-selector-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/file-selector/file-selector-input.tsx index 730f01b24..506eacc0d 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/file-selector/file-selector-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/file-selector/file-selector-input.tsx @@ -7,7 +7,6 @@ import { getProviderIdFromServiceId } from '@/lib/oauth' import { buildCanonicalIndex, resolveDependencyValue } from '@/lib/workflows/subblocks/visibility' import { SelectorCombobox } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/selector-combobox/selector-combobox' import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate' -import { useForeignCredential } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-foreign-credential' import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value' import { resolvePreviewContextValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/utils' import { getBlock } from '@/blocks/registry' @@ -125,8 +124,6 @@ export function FileSelectorInput({ const serviceId = subBlock.serviceId || '' const effectiveProviderId = useMemo(() => getProviderIdFromServiceId(serviceId), [serviceId]) - const { isForeignCredential } = useForeignCredential(effectiveProviderId, normalizedCredentialId) - const selectorResolution = useMemo(() => { return resolveSelectorForSubBlock(subBlock, { workflowId: workflowIdFromUrl, @@ -168,7 +165,6 @@ export function FileSelectorInput({ const disabledReason = finalDisabled || - isForeignCredential || missingCredential || missingDomain || missingProject || diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/folder-selector/components/folder-selector-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/folder-selector/components/folder-selector-input.tsx index 4be4a8da3..25fec739b 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/folder-selector/components/folder-selector-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/folder-selector/components/folder-selector-input.tsx @@ -4,7 +4,6 @@ import { useCallback, useEffect, useMemo, useState } from 'react' import { getProviderIdFromServiceId } from '@/lib/oauth' import { SelectorCombobox } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/selector-combobox/selector-combobox' import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate' -import { useForeignCredential } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-foreign-credential' import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value' import { resolvePreviewContextValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/utils' import type { SubBlockConfig } from '@/blocks/types' @@ -47,10 +46,6 @@ export function FolderSelectorInput({ subBlock.canonicalParamId === 'copyDestinationId' || subBlock.id === 'copyDestinationFolder' || subBlock.id === 'manualCopyDestinationFolder' - const { isForeignCredential } = useForeignCredential( - effectiveProviderId, - (connectedCredential as string) || '' - ) // Central dependsOn gating const { finalDisabled } = useDependsOnGate(blockId, subBlock, { @@ -119,9 +114,7 @@ export function FolderSelectorInput({ selectorContext={ selectorResolution?.context ?? { credentialId, workflowId: activeWorkflowId || '' } } - disabled={ - finalDisabled || isForeignCredential || missingCredential || !selectorResolution?.key - } + disabled={finalDisabled || missingCredential || !selectorResolution?.key} isPreview={isPreview} previewValue={previewValue ?? null} placeholder={subBlock.placeholder || 'Select folder'} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/project-selector/project-selector-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/project-selector/project-selector-input.tsx index e5b7c5d93..3df3acd46 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/project-selector/project-selector-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/project-selector/project-selector-input.tsx @@ -7,7 +7,6 @@ import { getProviderIdFromServiceId } from '@/lib/oauth' import { buildCanonicalIndex, resolveDependencyValue } from '@/lib/workflows/subblocks/visibility' import { SelectorCombobox } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/selector-combobox/selector-combobox' import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate' -import { useForeignCredential } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-foreign-credential' import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value' import { resolvePreviewContextValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/utils' import { getBlock } from '@/blocks/registry' @@ -73,11 +72,6 @@ export function ProjectSelectorInput({ const serviceId = subBlock.serviceId || '' const effectiveProviderId = useMemo(() => getProviderIdFromServiceId(serviceId), [serviceId]) - - const { isForeignCredential } = useForeignCredential( - effectiveProviderId, - (connectedCredential as string) || '' - ) const workflowIdFromUrl = (params?.workflowId as string) || activeWorkflowId || '' const { finalDisabled } = useDependsOnGate(blockId, subBlock, { disabled, @@ -123,7 +117,7 @@ export function ProjectSelectorInput({ subBlock={subBlock} selectorKey={selectorResolution.key} selectorContext={selectorResolution.context} - disabled={finalDisabled || isForeignCredential || missingCredential} + disabled={finalDisabled || missingCredential} isPreview={isPreview} previewValue={previewValue ?? null} placeholder={subBlock.placeholder || 'Select project'} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/sheet-selector/sheet-selector-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/sheet-selector/sheet-selector-input.tsx index bfb9dbe4f..ee33b320a 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/sheet-selector/sheet-selector-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/sheet-selector/sheet-selector-input.tsx @@ -7,7 +7,6 @@ import { getProviderIdFromServiceId } from '@/lib/oauth' import { buildCanonicalIndex, resolveDependencyValue } from '@/lib/workflows/subblocks/visibility' import { SelectorCombobox } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/selector-combobox/selector-combobox' import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate' -import { useForeignCredential } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-foreign-credential' import { resolvePreviewContextValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/utils' import { getBlock } from '@/blocks/registry' import type { SubBlockConfig } from '@/blocks/types' @@ -87,8 +86,6 @@ export function SheetSelectorInput({ const serviceId = subBlock.serviceId || '' const effectiveProviderId = useMemo(() => getProviderIdFromServiceId(serviceId), [serviceId]) - const { isForeignCredential } = useForeignCredential(effectiveProviderId, normalizedCredentialId) - const selectorResolution = useMemo(() => { return resolveSelectorForSubBlock(subBlock, { workflowId: workflowIdFromUrl, @@ -101,11 +98,7 @@ export function SheetSelectorInput({ const missingSpreadsheet = !normalizedSpreadsheetId const disabledReason = - finalDisabled || - isForeignCredential || - missingCredential || - missingSpreadsheet || - !selectorResolution?.key + finalDisabled || missingCredential || missingSpreadsheet || !selectorResolution?.key if (!selectorResolution?.key) { return ( diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/slack-selector/slack-selector-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/slack-selector/slack-selector-input.tsx index b99c26bff..e3e4e2148 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/slack-selector/slack-selector-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/slack-selector/slack-selector-input.tsx @@ -6,7 +6,6 @@ import { Tooltip } from '@/components/emcn' import { getProviderIdFromServiceId } from '@/lib/oauth' import { SelectorCombobox } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/selector-combobox/selector-combobox' import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate' -import { useForeignCredential } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-foreign-credential' import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value' import { resolvePreviewContextValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/utils' import type { SubBlockConfig } from '@/blocks/types' @@ -85,11 +84,6 @@ export function SlackSelectorInput({ ? (effectiveBotToken as string) || '' : (effectiveCredential as string) || '' - const { isForeignCredential } = useForeignCredential( - effectiveProviderId, - (effectiveAuthMethod as string) === 'bot_token' ? '' : (effectiveCredential as string) || '' - ) - useEffect(() => { const val = isPreview && previewValue !== undefined ? previewValue : storeValue if (typeof val === 'string') { @@ -99,7 +93,7 @@ export function SlackSelectorInput({ const requiresCredential = dependsOn.includes('credential') const missingCredential = !credential || credential.trim().length === 0 - const shouldForceDisable = requiresCredential && (missingCredential || isForeignCredential) + const shouldForceDisable = requiresCredential && missingCredential const context: SelectorContext = useMemo( () => ({ @@ -136,7 +130,7 @@ export function SlackSelectorInput({ subBlock={subBlock} selectorKey={config.selectorKey} selectorContext={context} - disabled={finalDisabled || shouldForceDisable || isForeignCredential} + disabled={finalDisabled || shouldForceDisable} isPreview={isPreview} previewValue={previewValue ?? null} placeholder={subBlock.placeholder || config.placeholder} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tool-credential-selector.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tool-credential-selector.tsx index 0496489d4..488e7f12b 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tool-credential-selector.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/components/tool-credential-selector.tsx @@ -1,6 +1,8 @@ import { createElement, useCallback, useEffect, useMemo, useState } from 'react' import { ExternalLink } from 'lucide-react' +import { useParams } from 'next/navigation' import { Button, Combobox } from '@/components/emcn/components' +import { writePendingCredentialCreateRequest } from '@/lib/credentials/client-state' import { getCanonicalScopesForProvider, getProviderIdFromServiceId, @@ -10,8 +12,7 @@ import { parseProvider, } from '@/lib/oauth' import { OAuthRequiredModal } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/components/oauth-required-modal' -import { CREDENTIAL } from '@/executor/constants' -import { useOAuthCredentialDetail, useOAuthCredentials } from '@/hooks/queries/oauth-credentials' +import { useOAuthCredentials } from '@/hooks/queries/oauth-credentials' import { getMissingRequiredScopes } from '@/hooks/use-oauth-scope-status' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' @@ -54,10 +55,12 @@ export function ToolCredentialSelector({ onChange, provider, requiredScopes = [], - label = 'Select account', + label = 'Select credential', serviceId, disabled = false, }: ToolCredentialSelectorProps) { + const params = useParams() + const workspaceId = (params?.workspaceId as string) || '' const [showOAuthModal, setShowOAuthModal] = useState(false) const [editingInputValue, setEditingInputValue] = useState('') const [isEditing, setIsEditing] = useState(false) @@ -71,50 +74,32 @@ export function ToolCredentialSelector({ data: credentials = [], isFetching: credentialsLoading, refetch: refetchCredentials, - } = useOAuthCredentials(effectiveProviderId, Boolean(effectiveProviderId)) + } = useOAuthCredentials(effectiveProviderId, { + enabled: Boolean(effectiveProviderId), + workspaceId, + workflowId: activeWorkflowId || undefined, + }) const selectedCredential = useMemo( () => credentials.find((cred) => cred.id === selectedId), [credentials, selectedId] ) - const shouldFetchForeignMeta = - Boolean(selectedId) && - !selectedCredential && - Boolean(activeWorkflowId) && - Boolean(effectiveProviderId) - - const { data: foreignCredentials = [], isFetching: foreignMetaLoading } = - useOAuthCredentialDetail( - shouldFetchForeignMeta ? selectedId : undefined, - activeWorkflowId || undefined, - shouldFetchForeignMeta - ) - - const hasForeignMeta = foreignCredentials.length > 0 - const isForeign = Boolean(selectedId && !selectedCredential && hasForeignMeta) - const resolvedLabel = useMemo(() => { if (selectedCredential) return selectedCredential.name - if (isForeign) return CREDENTIAL.FOREIGN_LABEL return '' - }, [selectedCredential, isForeign]) + }, [selectedCredential]) const inputValue = isEditing ? editingInputValue : resolvedLabel - const invalidSelection = - Boolean(selectedId) && - !selectedCredential && - !hasForeignMeta && - !credentialsLoading && - !foreignMetaLoading + const invalidSelection = Boolean(selectedId) && !selectedCredential && !credentialsLoading useEffect(() => { if (!invalidSelection) return onChange('') }, [invalidSelection, onChange]) - useCredentialRefreshTriggers(refetchCredentials) + useCredentialRefreshTriggers(refetchCredentials, effectiveProviderId, workspaceId) const handleOpenChange = useCallback( (isOpen: boolean) => { @@ -142,8 +127,18 @@ export function ToolCredentialSelector({ ) const handleAddCredential = useCallback(() => { - setShowOAuthModal(true) - }, []) + writePendingCredentialCreateRequest({ + workspaceId, + type: 'oauth', + providerId: effectiveProviderId, + displayName: '', + serviceId, + requiredScopes: getCanonicalScopesForProvider(effectiveProviderId), + requestedAt: Date.now(), + }) + + window.dispatchEvent(new CustomEvent('open-settings', { detail: { tab: 'credentials' } })) + }, [workspaceId, effectiveProviderId, serviceId]) const comboboxOptions = useMemo(() => { const options = credentials.map((cred) => ({ @@ -151,12 +146,13 @@ export function ToolCredentialSelector({ value: cred.id, })) - if (credentials.length === 0) { - options.push({ - label: `Connect ${getProviderName(provider)} account`, - value: '__connect_account__', - }) - } + options.push({ + label: + credentials.length > 0 + ? `Connect another ${getProviderName(provider)} account` + : `Connect ${getProviderName(provider)} account`, + value: '__connect_account__', + }) return options }, [credentials, provider]) @@ -206,7 +202,7 @@ export function ToolCredentialSelector({ placeholder={label} disabled={disabled} editable={true} - filterOptions={!isForeign} + filterOptions={true} isLoading={credentialsLoading} overlayContent={overlayContent} className={selectedId ? 'pl-[28px]' : ''} @@ -218,15 +214,13 @@ export function ToolCredentialSelector({ Additional permissions required - {!isForeign && ( - - )} + )} @@ -245,7 +239,11 @@ export function ToolCredentialSelector({ ) } -function useCredentialRefreshTriggers(refetchCredentials: () => Promise) { +function useCredentialRefreshTriggers( + refetchCredentials: () => Promise, + providerId: string, + workspaceId: string +) { useEffect(() => { const refresh = () => { void refetchCredentials() @@ -263,12 +261,29 @@ function useCredentialRefreshTriggers(refetchCredentials: () => Promise } } + const handleCredentialsUpdated = ( + event: CustomEvent<{ providerId?: string; workspaceId?: string }> + ) => { + if (event.detail?.providerId && event.detail.providerId !== providerId) { + return + } + if (event.detail?.workspaceId && workspaceId && event.detail.workspaceId !== workspaceId) { + return + } + refresh() + } + document.addEventListener('visibilitychange', handleVisibilityChange) window.addEventListener('pageshow', handlePageShow) + window.addEventListener('oauth-credentials-updated', handleCredentialsUpdated as EventListener) return () => { document.removeEventListener('visibilitychange', handleVisibilityChange) window.removeEventListener('pageshow', handlePageShow) + window.removeEventListener( + 'oauth-credentials-updated', + handleCredentialsUpdated as EventListener + ) } - }, [refetchCredentials]) + }, [providerId, workspaceId, refetchCredentials]) } diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-foreign-credential.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-foreign-credential.ts deleted file mode 100644 index 727b09da2..000000000 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-foreign-credential.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { useEffect, useMemo, useState } from 'react' - -export function useForeignCredential( - provider: string | undefined, - credentialId: string | undefined -) { - const [isForeign, setIsForeign] = useState(false) - const [loading, setLoading] = useState(false) - const [error, setError] = useState(null) - - const normalizedProvider = useMemo(() => (provider || '').toString(), [provider]) - const normalizedCredentialId = useMemo(() => credentialId || '', [credentialId]) - - useEffect(() => { - let cancelled = false - async function check() { - setLoading(true) - setError(null) - try { - if (!normalizedProvider || !normalizedCredentialId) { - if (!cancelled) setIsForeign(false) - return - } - const res = await fetch( - `/api/auth/oauth/credentials?provider=${encodeURIComponent(normalizedProvider)}` - ) - if (!res.ok) { - if (!cancelled) setIsForeign(true) - return - } - const data = await res.json() - const isOwn = (data.credentials || []).some((c: any) => c.id === normalizedCredentialId) - if (!cancelled) setIsForeign(!isOwn) - } catch (e) { - if (!cancelled) { - setIsForeign(true) - setError((e as Error).message) - } - } finally { - if (!cancelled) setLoading(false) - } - } - void check() - return () => { - cancelled = true - } - }, [normalizedProvider, normalizedCredentialId]) - - return { isForeignCredential: isForeign, loading, error } -} diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/credentials/credentials-manager.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/credentials/credentials-manager.tsx new file mode 100644 index 000000000..2c4965d3a --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/credentials/credentials-manager.tsx @@ -0,0 +1,1247 @@ +'use client' + +import { createElement, useEffect, useMemo, useState } from 'react' +import { createLogger } from '@sim/logger' +import { useMutation, useQueryClient } from '@tanstack/react-query' +import { Plus, Search, Trash2 } from 'lucide-react' +import { useParams } from 'next/navigation' +import { + Badge, + Button, + Combobox, + Input, + Label, + Modal, + ModalBody, + ModalContent, + ModalFooter, + ModalHeader, +} from '@/components/emcn' +import { Skeleton } from '@/components/ui' +import { useSession } from '@/lib/auth/auth-client' +import { cn } from '@/lib/core/utils/cn' +import { + clearPendingCredentialCreateRequest, + clearPendingOAuthCredentialDraft, + readPendingCredentialCreateRequest, + readPendingOAuthCredentialDraft, + writePendingOAuthCredentialDraft, +} from '@/lib/credentials/client-state' +import { + getCanonicalScopesForProvider, + getServiceConfigByProviderId, + type OAuthProvider, +} from '@/lib/oauth' +import { OAuthRequiredModal } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/credential-selector/components/oauth-required-modal' +import { isValidEnvVarName } from '@/executor/constants' +import { + useCreateWorkspaceCredential, + useDeleteWorkspaceCredential, + useRemoveWorkspaceCredentialMember, + useUpsertWorkspaceCredentialMember, + useWorkspaceCredentialMembers, + useWorkspaceCredentials, + type WorkspaceCredential, + type WorkspaceCredentialRole, + workspaceCredentialKeys, +} from '@/hooks/queries/credentials' +import { + usePersonalEnvironment, + useSavePersonalEnvironment, + useUpsertWorkspaceEnvironment, + useWorkspaceEnvironment, +} from '@/hooks/queries/environment' +import { + useConnectOAuthService, + useDisconnectOAuthService, + useOAuthConnections, +} from '@/hooks/queries/oauth-connections' +import { useWorkspacePermissionsQuery } from '@/hooks/queries/workspace' + +const logger = createLogger('CredentialsManager') + +interface AuthAccount { + id: string + accountId: string + providerId: string + displayName: string +} + +interface AuthAccountsResponse { + accounts?: AuthAccount[] +} + +const roleOptions = [ + { value: 'member', label: 'Member' }, + { value: 'admin', label: 'Admin' }, +] as const + +const typeOptions = [ + { value: 'oauth', label: 'OAuth Account' }, + { value: 'env_workspace', label: 'Workspace Environment' }, + { value: 'env_personal', label: 'Personal Environment' }, +] as const + +function typeBadgeVariant(type: WorkspaceCredential['type']): 'blue' | 'amber' | 'gray-secondary' { + if (type === 'oauth') return 'blue' + if (type === 'env_workspace') return 'amber' + return 'gray-secondary' +} + +function typeLabel(type: WorkspaceCredential['type']): string { + if (type === 'oauth') return 'OAuth' + if (type === 'env_workspace') return 'Workspace Env' + return 'Personal Env' +} + +function normalizeEnvKeyInput(raw: string): string { + const trimmed = raw.trim() + const wrappedMatch = /^\{\{\s*([A-Za-z0-9_]+)\s*\}\}$/.exec(trimmed) + return wrappedMatch ? wrappedMatch[1] : trimmed +} + +export function CredentialsManager() { + const params = useParams() + const workspaceId = (params?.workspaceId as string) || '' + const queryClient = useQueryClient() + + const [searchTerm, setSearchTerm] = useState('') + const [selectedCredentialId, setSelectedCredentialId] = useState(null) + const [memberRole, setMemberRole] = useState('member') + const [memberUserId, setMemberUserId] = useState('') + const [showCreateModal, setShowCreateModal] = useState(false) + const [createType, setCreateType] = useState('oauth') + const [createDisplayName, setCreateDisplayName] = useState('') + const [createEnvKey, setCreateEnvKey] = useState('') + const [createEnvValue, setCreateEnvValue] = useState('') + const [createOAuthProviderId, setCreateOAuthProviderId] = useState('') + const [createError, setCreateError] = useState(null) + const [detailsError, setDetailsError] = useState(null) + const [selectedEnvValueDraft, setSelectedEnvValueDraft] = useState('') + const [isEditingEnvValue, setIsEditingEnvValue] = useState(false) + const [isFinalizingOAuthDraft, setIsFinalizingOAuthDraft] = useState(false) + const [showCreateOAuthRequiredModal, setShowCreateOAuthRequiredModal] = useState(false) + const { data: session } = useSession() + const currentUserId = session?.user?.id || '' + + const { + data: credentials = [], + isPending: credentialsLoading, + refetch: refetchCredentials, + } = useWorkspaceCredentials({ + workspaceId, + enabled: Boolean(workspaceId), + }) + + const { data: oauthConnections = [] } = useOAuthConnections() + const connectOAuthService = useConnectOAuthService() + const disconnectOAuthService = useDisconnectOAuthService() + const savePersonalEnvironment = useSavePersonalEnvironment() + const upsertWorkspaceEnvironment = useUpsertWorkspaceEnvironment() + const { data: personalEnvironment = {} } = usePersonalEnvironment() + const { data: workspaceEnvironmentData } = useWorkspaceEnvironment(workspaceId, { + select: (data) => data, + }) + + const bootstrapCredentials = useMutation({ + mutationFn: async () => { + if (!workspaceId) return null + const response = await fetch('/api/credentials/bootstrap', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ workspaceId }), + }) + if (!response.ok) { + const data = await response.json().catch(() => ({})) + throw new Error(data.error || 'Failed to bootstrap credentials') + } + return response.json() + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: workspaceCredentialKeys.list(workspaceId) }) + }, + }) + const runBootstrapCredentials = bootstrapCredentials.mutate + + useEffect(() => { + if (!workspaceId) return + runBootstrapCredentials() + }, [workspaceId, runBootstrapCredentials]) + + const fetchOAuthAccountsForProvider = async (providerId: string): Promise => { + const response = await fetch(`/api/auth/accounts?provider=${encodeURIComponent(providerId)}`) + if (!response.ok) { + const data = await response.json().catch(() => ({})) + throw new Error(data.error || 'Failed to fetch OAuth accounts') + } + const data = (await response.json()) as AuthAccountsResponse + return data.accounts ?? [] + } + + const { data: workspacePermissions } = useWorkspacePermissionsQuery(workspaceId || null) + const selectedCredential = useMemo( + () => credentials.find((credential) => credential.id === selectedCredentialId) || null, + [credentials, selectedCredentialId] + ) + + const { data: members = [], isPending: membersLoading } = useWorkspaceCredentialMembers( + selectedCredential?.id + ) + + const createCredential = useCreateWorkspaceCredential() + const deleteCredential = useDeleteWorkspaceCredential() + const upsertMember = useUpsertWorkspaceCredentialMember() + const removeMember = useRemoveWorkspaceCredentialMember() + const oauthServiceNameByProviderId = useMemo( + () => new Map(oauthConnections.map((service) => [service.providerId, service.name])), + [oauthConnections] + ) + const resolveProviderLabel = (providerId?: string | null): string => { + if (!providerId) return '' + return oauthServiceNameByProviderId.get(providerId) || providerId + } + + const filteredCredentials = useMemo(() => { + if (!searchTerm.trim()) return credentials + const normalized = searchTerm.toLowerCase() + return credentials.filter((credential) => { + return ( + credential.displayName.toLowerCase().includes(normalized) || + (credential.providerId || '').toLowerCase().includes(normalized) || + resolveProviderLabel(credential.providerId).toLowerCase().includes(normalized) || + typeLabel(credential.type).toLowerCase().includes(normalized) + ) + }) + }, [credentials, searchTerm, oauthConnections]) + + const sortedCredentials = useMemo(() => { + return [...filteredCredentials].sort((a, b) => { + const aDate = new Date(a.updatedAt).getTime() + const bDate = new Date(b.updatedAt).getTime() + return bDate - aDate + }) + }, [filteredCredentials]) + + const oauthServiceOptions = useMemo( + () => + oauthConnections.map((service) => ({ + value: service.providerId, + label: service.name, + })), + [oauthConnections] + ) + + const activeMembers = useMemo( + () => members.filter((member) => member.status === 'active'), + [members] + ) + const adminMemberCount = useMemo( + () => activeMembers.filter((member) => member.role === 'admin').length, + [activeMembers] + ) + + const workspaceUserOptions = useMemo(() => { + const activeMemberUserIds = new Set(activeMembers.map((member) => member.userId)) + return (workspacePermissions?.users || []) + .filter((user) => !activeMemberUserIds.has(user.userId)) + .map((user) => ({ + value: user.userId, + label: user.name || user.email, + })) + }, [workspacePermissions?.users, activeMembers]) + + const selectedOAuthService = useMemo( + () => oauthConnections.find((service) => service.providerId === createOAuthProviderId) || null, + [oauthConnections, createOAuthProviderId] + ) + const createOAuthRequiredScopes = useMemo(() => { + if (!createOAuthProviderId) return [] + if (selectedOAuthService?.scopes?.length) { + return selectedOAuthService.scopes + } + return getCanonicalScopesForProvider(createOAuthProviderId) + }, [selectedOAuthService, createOAuthProviderId]) + const selectedExistingEnvCredential = useMemo(() => { + const envKey = normalizeEnvKeyInput(createEnvKey) + if (!envKey) return null + return ( + credentials.find( + (row) => + row.type === createType && + row.type !== 'oauth' && + (row.envKey || '').toLowerCase() === envKey.toLowerCase() + ) ?? null + ) + }, [credentials, createEnvKey, createType]) + const selectedEnvCurrentValue = useMemo(() => { + if (!selectedCredential || selectedCredential.type === 'oauth') return '' + const envKey = selectedCredential.envKey || '' + if (!envKey) return '' + + if (selectedCredential.type === 'env_workspace') { + return workspaceEnvironmentData?.workspace?.[envKey] || '' + } + + if (selectedCredential.envOwnerUserId && selectedCredential.envOwnerUserId !== currentUserId) { + return '' + } + + return personalEnvironment[envKey]?.value || workspaceEnvironmentData?.personal?.[envKey] || '' + }, [selectedCredential, workspaceEnvironmentData, personalEnvironment, currentUserId]) + const isEnvValueDirty = useMemo(() => { + if (!selectedCredential || selectedCredential.type === 'oauth') return false + return selectedEnvValueDraft !== selectedEnvCurrentValue + }, [selectedCredential, selectedEnvValueDraft, selectedEnvCurrentValue]) + + useEffect(() => { + if (createType !== 'oauth') return + if (createOAuthProviderId || oauthConnections.length === 0) return + setCreateOAuthProviderId(oauthConnections[0]?.providerId || '') + }, [createType, createOAuthProviderId, oauthConnections]) + + useEffect(() => { + setCreateError(null) + }, [createOAuthProviderId]) + + useEffect(() => { + if (!workspaceId) return + const request = readPendingCredentialCreateRequest() + if (!request) return + + if (request.workspaceId !== workspaceId || request.type !== 'oauth') { + return + } + + if (Date.now() - request.requestedAt > 15 * 60 * 1000) { + clearPendingCredentialCreateRequest() + return + } + + setShowCreateModal(true) + setCreateType('oauth') + setCreateOAuthProviderId(request.providerId) + setCreateDisplayName(request.displayName) + setCreateError(null) + clearPendingCredentialCreateRequest() + }, [workspaceId]) + + useEffect(() => { + if (!workspaceId) return + if (isFinalizingOAuthDraft) return + + const draft = readPendingOAuthCredentialDraft() + if (!draft) return + + if (draft.workspaceId !== workspaceId) { + return + } + + const draftAgeMs = Date.now() - draft.requestedAt + if (draftAgeMs > 15 * 60 * 1000) { + clearPendingOAuthCredentialDraft() + return + } + + const finalize = async () => { + setIsFinalizingOAuthDraft(true) + try { + await bootstrapCredentials.mutateAsync() + const refetched = await refetchCredentials() + const latestCredentials = refetched.data ?? credentials + + const providerCredentials = latestCredentials + .filter( + (row): row is WorkspaceCredential & { accountId: string; providerId: string } => + row.type === 'oauth' && Boolean(row.accountId) && Boolean(row.providerId) + ) + .filter((row) => row.providerId === draft.providerId) + .sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()) + + const newAccountCredential = providerCredentials.find( + (row) => !draft.existingAccountIds.includes(row.accountId) + ) + const newCredential = providerCredentials.find( + (row) => !draft.existingCredentialIds.includes(row.id) + ) + const targetCredential = newAccountCredential || newCredential || providerCredentials[0] + + if (!targetCredential?.accountId || !targetCredential.providerId) { + return + } + + const response = await createCredential.mutateAsync({ + workspaceId, + type: 'oauth', + displayName: draft.displayName, + providerId: targetCredential.providerId, + accountId: targetCredential.accountId, + }) + + const credentialId = response?.credential?.id || targetCredential.id + if (credentialId) { + setSelectedCredentialId(credentialId) + } + + if (typeof window !== 'undefined') { + window.dispatchEvent( + new CustomEvent('oauth-credentials-updated', { + detail: { providerId: draft.providerId, workspaceId }, + }) + ) + } + + setShowCreateModal(false) + setCreateDisplayName('') + setCreateError(null) + clearPendingOAuthCredentialDraft() + } catch (error) { + logger.error('Failed to finalize OAuth credential draft', error) + } finally { + setIsFinalizingOAuthDraft(false) + } + } + + void finalize() + }, [ + workspaceId, + credentials, + isFinalizingOAuthDraft, + bootstrapCredentials, + refetchCredentials, + createCredential, + ]) + + useEffect(() => { + if (!selectedCredential) { + setSelectedEnvValueDraft('') + setIsEditingEnvValue(false) + return + } + + setDetailsError(null) + if (selectedCredential.type === 'oauth') { + setSelectedEnvValueDraft('') + setIsEditingEnvValue(false) + return + } + + const envKey = selectedCredential.envKey || '' + if (!envKey) { + setSelectedEnvValueDraft('') + return + } + + setSelectedEnvValueDraft(selectedEnvCurrentValue) + setIsEditingEnvValue(false) + }, [selectedCredential, selectedEnvCurrentValue]) + + const isSelectedAdmin = selectedCredential?.role === 'admin' + const selectedOAuthServiceConfig = useMemo(() => { + if ( + !selectedCredential || + selectedCredential.type !== 'oauth' || + !selectedCredential.providerId + ) { + return null + } + + return getServiceConfigByProviderId(selectedCredential.providerId) + }, [selectedCredential]) + + const resetCreateForm = () => { + setCreateType('oauth') + setCreateDisplayName('') + setCreateEnvKey('') + setCreateEnvValue('') + setCreateOAuthProviderId('') + setCreateError(null) + setShowCreateOAuthRequiredModal(false) + } + + const handleSelectCredential = (credential: WorkspaceCredential) => { + setSelectedCredentialId(credential.id) + setDetailsError(null) + } + + const canEditSelectedEnvValue = useMemo(() => { + if (!selectedCredential || selectedCredential.type === 'oauth') return false + if (!isSelectedAdmin) return false + if (selectedCredential.type === 'env_workspace') return true + return Boolean( + selectedCredential.envOwnerUserId && + currentUserId && + selectedCredential.envOwnerUserId === currentUserId + ) + }, [selectedCredential, isSelectedAdmin, currentUserId]) + + const handleSaveEnvCredentialValue = async () => { + if (!selectedCredential || selectedCredential.type === 'oauth') return + const envKey = selectedCredential.envKey || '' + if (!envKey) return + if (!canEditSelectedEnvValue) { + setDetailsError('You do not have permission to edit this environment value') + return + } + + try { + setDetailsError(null) + const nextValue = selectedEnvValueDraft + + if (selectedCredential.type === 'env_workspace') { + await upsertWorkspaceEnvironment.mutateAsync({ + workspaceId, + variables: { + [envKey]: nextValue, + }, + }) + } else { + const personalVariables = Object.entries(personalEnvironment).reduce( + (acc, [key, value]) => ({ + ...acc, + [key]: value.value, + }), + {} as Record + ) + + await savePersonalEnvironment.mutateAsync({ + variables: { + ...personalVariables, + [envKey]: nextValue, + }, + }) + } + + await bootstrapCredentials.mutateAsync() + await refetchCredentials() + setIsEditingEnvValue(false) + } catch (error: unknown) { + const message = error instanceof Error ? error.message : 'Failed to update environment value' + setDetailsError(message) + logger.error('Failed to update environment credential value', error) + } + } + + const handleCreateCredential = async () => { + if (!workspaceId) return + setCreateError(null) + + try { + if (createType === 'oauth') { + if (!selectedOAuthService) { + setCreateError('Select an OAuth service before connecting.') + return + } + if (!createDisplayName.trim()) { + setCreateError('Display name is required.') + return + } + setShowCreateOAuthRequiredModal(true) + return + } + + if (!createEnvKey.trim()) return + const normalizedEnvKey = normalizeEnvKeyInput(createEnvKey) + if (!isValidEnvVarName(normalizedEnvKey)) { + setCreateError( + 'Environment variable key must contain only letters, numbers, and underscores.' + ) + return + } + if (!createEnvValue.trim()) { + setCreateError('Environment variable value is required.') + return + } + + if (createType === 'env_personal') { + const personalVariables = Object.entries(personalEnvironment).reduce( + (acc, [key, value]) => ({ + ...acc, + [key]: value.value, + }), + {} as Record + ) + + await savePersonalEnvironment.mutateAsync({ + variables: { + ...personalVariables, + [normalizedEnvKey]: createEnvValue.trim(), + }, + }) + } else { + const workspaceVariables = workspaceEnvironmentData?.workspace ?? {} + await upsertWorkspaceEnvironment.mutateAsync({ + workspaceId, + variables: { + ...workspaceVariables, + [normalizedEnvKey]: createEnvValue.trim(), + }, + }) + } + if (selectedExistingEnvCredential) { + setSelectedCredentialId(selectedExistingEnvCredential.id) + } else { + const response = await createCredential.mutateAsync({ + workspaceId, + type: createType, + envKey: normalizedEnvKey, + }) + const credentialId = response?.credential?.id + if (credentialId) { + setSelectedCredentialId(credentialId) + } + } + + await bootstrapCredentials.mutateAsync() + await refetchCredentials() + + setShowCreateModal(false) + resetCreateForm() + } catch (error: unknown) { + const message = error instanceof Error ? error.message : 'Failed to create credential' + setCreateError(message) + logger.error('Failed to create credential', error) + } + } + + const handleConnectOAuthService = async () => { + if (!selectedOAuthService) { + setCreateError('Select an OAuth service before connecting.') + return + } + + const displayName = createDisplayName.trim() + if (!displayName) { + setCreateError('Display name is required.') + return + } + + setCreateError(null) + try { + let existingAccountIds: string[] = [] + try { + const accounts = await fetchOAuthAccountsForProvider(selectedOAuthService.providerId) + existingAccountIds = accounts.map((account) => account.id) + } catch (error) { + logger.warn('Failed to capture OAuth account snapshot before connect', { error }) + } + + const existingCredentialIds = credentials + .filter( + (row): row is WorkspaceCredential & { providerId: string } => + row.type === 'oauth' && Boolean(row.providerId) + ) + .filter((row) => row.providerId === selectedOAuthService.providerId) + .map((row) => row.id) + + writePendingOAuthCredentialDraft({ + workspaceId, + providerId: selectedOAuthService.providerId, + displayName, + existingCredentialIds, + existingAccountIds, + requestedAt: Date.now(), + }) + + await connectOAuthService.mutateAsync({ + providerId: selectedOAuthService.providerId, + callbackURL: window.location.href, + }) + } catch (error: unknown) { + clearPendingOAuthCredentialDraft() + const message = error instanceof Error ? error.message : 'Failed to start OAuth connection' + setCreateError(message) + logger.error('Failed to connect OAuth service', error) + } + } + + const handleDeleteCredential = async () => { + if (!selectedCredential) return + if (selectedCredential.type === 'oauth') { + await handleDisconnectSelectedCredential() + return + } + try { + await deleteCredential.mutateAsync(selectedCredential.id) + setSelectedCredentialId(null) + } catch (error) { + logger.error('Failed to delete credential', error) + } + } + + const handleDisconnectSelectedCredential = async () => { + if (!selectedCredential || selectedCredential.type !== 'oauth' || !selectedCredential.accountId) + return + if (!selectedCredential.providerId) return + + try { + await disconnectOAuthService.mutateAsync({ + provider: selectedCredential.providerId.split('-')[0] || selectedCredential.providerId, + providerId: selectedCredential.providerId, + serviceId: selectedCredential.providerId, + accountId: selectedCredential.accountId, + }) + + setSelectedCredentialId(null) + await bootstrapCredentials.mutateAsync() + await refetchCredentials() + window.dispatchEvent( + new CustomEvent('oauth-credentials-updated', { + detail: { providerId: selectedCredential.providerId, workspaceId }, + }) + ) + } catch (error) { + logger.error('Failed to disconnect credential account', error) + } + } + + const handleAddMember = async () => { + if (!selectedCredential || !memberUserId) return + try { + await upsertMember.mutateAsync({ + credentialId: selectedCredential.id, + userId: memberUserId, + role: memberRole, + }) + setMemberUserId('') + setMemberRole('member') + } catch (error) { + logger.error('Failed to add credential member', error) + } + } + + const handleChangeMemberRole = async (userId: string, role: WorkspaceCredentialRole) => { + if (!selectedCredential) return + const currentMember = activeMembers.find((member) => member.userId === userId) + if (currentMember?.role === role) return + try { + await upsertMember.mutateAsync({ + credentialId: selectedCredential.id, + userId, + role, + }) + } catch (error) { + logger.error('Failed to change member role', error) + } + } + + const handleRemoveMember = async (userId: string) => { + if (!selectedCredential) return + try { + await removeMember.mutateAsync({ + credentialId: selectedCredential.id, + userId, + }) + } catch (error) { + logger.error('Failed to remove credential member', error) + } + } + + return ( +
+
+
+
+ + setSearchTerm(event.target.value)} + placeholder='Search credentials...' + className='pl-[32px]' + /> +
+ +
+ +
+ {credentialsLoading ? ( +
+ + + +
+ ) : sortedCredentials.length === 0 ? ( +
+ {bootstrapCredentials.isPending + ? 'Syncing credentials from connected accounts and env vars...' + : 'No credentials available for this workspace.'} +
+ ) : ( +
+ {sortedCredentials.map((credential) => ( + + ))} +
+ )} +
+
+ +
+ {!selectedCredential ? ( +
+ Select a credential to manage members. +
+ ) : ( +
+
+
+
+ + {typeLabel(selectedCredential.type)} + + {selectedCredential.role && ( + + {selectedCredential.role} + + )} +
+ {isSelectedAdmin && ( +
+ {selectedCredential.type === 'oauth' && ( + + )} + {selectedCredential.type !== 'oauth' && ( + + )} +
+ )} +
+ + {selectedCredential.type === 'oauth' ? ( +
+
+ + +
+
+ +
+
+ {selectedOAuthServiceConfig ? ( + createElement(selectedOAuthServiceConfig.icon, { className: 'h-4 w-4' }) + ) : ( + + {resolveProviderLabel(selectedCredential.providerId).slice(0, 1)} + + )} +
+ + {resolveProviderLabel(selectedCredential.providerId) || 'Unknown service'} + +
+
+
+ ) : ( +
+ + +
+
+ + {canEditSelectedEnvValue && ( + + )} +
+ setSelectedEnvValueDraft(event.target.value)} + onFocus={() => { + if (canEditSelectedEnvValue) { + setIsEditingEnvValue(true) + } + }} + autoComplete='new-password' + autoCapitalize='none' + autoCorrect='off' + spellCheck={false} + data-lpignore='true' + data-1p-ignore='true' + readOnly={!canEditSelectedEnvValue || !isEditingEnvValue} + disabled={!canEditSelectedEnvValue} + className='mt-[6px]' + /> +
+ {isSelectedAdmin && ( + + )} +
+ )} + {selectedCredential.type !== 'oauth' && ( +

+ {`Linked env key: ${selectedCredential.envKey || 'unknown'} (${selectedCredential.type === 'env_workspace' ? 'workspace' : 'personal'})`} +

+ )} + {detailsError && ( +
+ {detailsError} +
+ )} +
+ +
+

+ Members +

+ + {membersLoading ? ( +
+ + +
+ ) : ( +
+ {activeMembers.map((member) => ( +
+
+

+ {member.userName || member.userEmail || member.userId} +

+

+ {member.userEmail || member.userId} +

+
+ +
+ {isSelectedAdmin ? ( + <> + ({ + value: option.value, + label: option.label, + }))} + value={ + roleOptions.find((option) => option.value === member.role)?.label || + '' + } + selectedValue={member.role} + onChange={(value) => + handleChangeMemberRole( + member.userId, + value as WorkspaceCredentialRole + ) + } + placeholder='Role' + disabled={member.role === 'admin' && adminMemberCount <= 1} + size='sm' + className='min-w-[120px]' + /> + + + ) : ( + + {member.role} + + )} +
+
+ ))} +
+ )} + + {isSelectedAdmin && ( +
+ +
+ option.value === memberUserId) + ?.label || '' + } + selectedValue={memberUserId} + onChange={setMemberUserId} + placeholder='Select user' + /> + ({ + value: option.value, + label: option.label, + }))} + value={roleOptions.find((option) => option.value === memberRole)?.label || ''} + selectedValue={memberRole} + onChange={(value) => setMemberRole(value as WorkspaceCredentialRole)} + placeholder='Role' + /> + +
+
+ )} +
+
+ )} +
+ + { + setShowCreateModal(open) + if (!open) resetCreateForm() + }} + > + + Create Credential + +
+
+ +
+ ({ + value: option.value, + label: option.label, + }))} + value={typeOptions.find((option) => option.value === createType)?.label || ''} + selectedValue={createType} + onChange={(value) => { + setCreateType(value as WorkspaceCredential['type']) + setCreateError(null) + }} + placeholder='Select credential type' + /> +
+
+ + {createType === 'oauth' ? ( +
+
+ + setCreateDisplayName(event.target.value)} + placeholder='Credential name' + autoComplete='off' + className='mt-[6px]' + /> +
+
+ +
+ option.value === createOAuthProviderId + )?.label || '' + } + selectedValue={createOAuthProviderId} + onChange={setCreateOAuthProviderId} + placeholder='Select OAuth service' + /> +
+
+ +
+

+ Connecting creates a credential for this workspace. Disconnecting from that + credential removes it. +

+
+
+ ) : ( +
+
+ + { + setCreateEnvKey(event.target.value) + }} + placeholder='API_KEY' + autoComplete='off' + autoCapitalize='none' + autoCorrect='off' + spellCheck={false} + data-lpignore='true' + data-1p-ignore='true' + className='mt-[6px]' + /> +

+ Use it in blocks as {'{{KEY}}'}, for example {'{{API_KEY}}'}. +

+
+
+ + setCreateEnvValue(event.target.value)} + placeholder='Enter secret value' + autoComplete='new-password' + autoCapitalize='none' + autoCorrect='off' + spellCheck={false} + data-lpignore='true' + data-1p-ignore='true' + className='mt-[6px]' + /> +
+ + {selectedExistingEnvCredential && ( +
+

+ This env key already maps to credential{' '} + + {selectedExistingEnvCredential.displayName} + + . +

+

+ Create will update the env value and reuse the existing credential. +

+ +
+ )} +
+ )} + + {createError && ( +
+ {createError} +
+ )} +
+
+ + + + +
+
+ {showCreateOAuthRequiredModal && createOAuthProviderId && ( + setShowCreateOAuthRequiredModal(false)} + provider={createOAuthProviderId as OAuthProvider} + toolName={resolveProviderLabel(createOAuthProviderId)} + requiredScopes={createOAuthRequiredScopes} + newScopes={createOAuthRequiredScopes} + serviceId={selectedOAuthService?.id || createOAuthProviderId} + onConnect={async () => { + await handleConnectOAuthService() + }} + /> + )} +
+ ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/credentials/credentials.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/credentials/credentials.tsx new file mode 100644 index 000000000..da5076821 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/credentials/credentials.tsx @@ -0,0 +1,17 @@ +'use client' + +import { CredentialsManager } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/credentials/credentials-manager' + +interface CredentialsProps { + onOpenChange?: (open: boolean) => void + registerCloseHandler?: (handler: (open: boolean) => void) => void + registerBeforeLeaveHandler?: (handler: (onProceed: () => void) => void) => void +} + +export function Credentials(_props: CredentialsProps) { + return ( +
+ +
+ ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/index.ts b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/index.ts index 744e1be4e..0621308ac 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/index.ts @@ -2,6 +2,7 @@ export { ApiKeys } from './api-keys/api-keys' export { BYOK } from './byok/byok' export { Copilot } from './copilot/copilot' export { CredentialSets } from './credential-sets/credential-sets' +export { Credentials } from './credentials/credentials' export { CustomTools } from './custom-tools/custom-tools' export { Debug } from './debug/debug' export { EnvironmentVariables } from './environment/environment' diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/settings-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/settings-modal.tsx index 84ef6a61e..ff8781c17 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/settings-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/settings-modal.tsx @@ -20,7 +20,6 @@ import { import { Card, Connections, - FolderCode, HexSimple, Key, SModal, @@ -45,12 +44,11 @@ import { BYOK, Copilot, CredentialSets, + Credentials, CustomTools, Debug, - EnvironmentVariables, FileUploads, General, - Integrations, MCP, Skills, Subscription, @@ -80,6 +78,7 @@ interface SettingsModalProps { type SettingsSection = | 'general' + | 'credentials' | 'environment' | 'template-profile' | 'integrations' @@ -156,11 +155,10 @@ const allNavigationItems: NavigationItem[] = [ requiresHosted: true, requiresTeam: true, }, - { id: 'integrations', label: 'Integrations', icon: Connections, section: 'tools' }, + { id: 'credentials', label: 'Credentials', icon: Connections, section: 'tools' }, { id: 'custom-tools', label: 'Custom Tools', icon: Wrench, section: 'tools' }, { id: 'skills', label: 'Skills', icon: AgentSkillsIcon, section: 'tools' }, { id: 'mcp', label: 'MCP Tools', icon: McpIcon, section: 'tools' }, - { id: 'environment', label: 'Environment', icon: FolderCode, section: 'system' }, { id: 'apikeys', label: 'API Keys', icon: Key, section: 'system' }, { id: 'workflow-mcp-servers', label: 'MCP Servers', icon: Server, section: 'system' }, { @@ -256,9 +254,6 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) { if (item.id === 'apikeys' && permissionConfig.hideApiKeysTab) { return false } - if (item.id === 'environment' && permissionConfig.hideEnvironmentTab) { - return false - } if (item.id === 'files' && permissionConfig.hideFilesTab) { return false } @@ -324,6 +319,9 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) { if (!isBillingEnabled && (activeSection === 'subscription' || activeSection === 'team')) { return 'general' } + if (activeSection === 'environment' || activeSection === 'integrations') { + return 'credentials' + } return activeSection }, [activeSection]) @@ -342,7 +340,7 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) { (sectionId: SettingsSection) => { if (sectionId === effectiveActiveSection) return - if (effectiveActiveSection === 'environment' && environmentBeforeLeaveHandler.current) { + if (effectiveActiveSection === 'credentials' && environmentBeforeLeaveHandler.current) { environmentBeforeLeaveHandler.current(() => setActiveSection(sectionId)) return } @@ -370,7 +368,11 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) { useEffect(() => { const handleOpenSettings = (event: CustomEvent<{ tab: SettingsSection }>) => { - setActiveSection(event.detail.tab) + if (event.detail.tab === 'environment' || event.detail.tab === 'integrations') { + setActiveSection('credentials') + } else { + setActiveSection(event.detail.tab) + } onOpenChange(true) } @@ -479,13 +481,19 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) { const handleDialogOpenChange = (newOpen: boolean) => { if ( !newOpen && - effectiveActiveSection === 'environment' && + effectiveActiveSection === 'credentials' && environmentBeforeLeaveHandler.current ) { - environmentBeforeLeaveHandler.current(() => onOpenChange(false)) + environmentBeforeLeaveHandler.current(() => { + if (integrationsCloseHandler.current) { + integrationsCloseHandler.current(newOpen) + } else { + onOpenChange(false) + } + }) } else if ( !newOpen && - effectiveActiveSection === 'integrations' && + effectiveActiveSection === 'credentials' && integrationsCloseHandler.current ) { integrationsCloseHandler.current(newOpen) @@ -502,7 +510,7 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) { - Configure your workspace settings, environment variables, integrations, and preferences + Configure your workspace settings, credentials, and preferences @@ -539,18 +547,14 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) { {effectiveActiveSection === 'general' && } - {effectiveActiveSection === 'environment' && ( - )} {effectiveActiveSection === 'template-profile' && } - {effectiveActiveSection === 'integrations' && ( - - )} {effectiveActiveSection === 'credential-sets' && } {effectiveActiveSection === 'access-control' && } {effectiveActiveSection === 'apikeys' && } diff --git a/apps/sim/executor/constants.ts b/apps/sim/executor/constants.ts index b5f97dd47..bac3dccc6 100644 --- a/apps/sim/executor/constants.ts +++ b/apps/sim/executor/constants.ts @@ -205,10 +205,6 @@ export const CREDENTIAL_SET = { PREFIX: 'credentialSet:', } as const -export const CREDENTIAL = { - FOREIGN_LABEL: 'Saved by collaborator', -} as const - export function isCredentialSetValue(value: string | null | undefined): boolean { return typeof value === 'string' && value.startsWith(CREDENTIAL_SET.PREFIX) } diff --git a/apps/sim/hooks/queries/credentials.ts b/apps/sim/hooks/queries/credentials.ts new file mode 100644 index 000000000..707c71c9a --- /dev/null +++ b/apps/sim/hooks/queries/credentials.ts @@ -0,0 +1,266 @@ +'use client' + +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import { fetchJson } from '@/hooks/selectors/helpers' + +export type WorkspaceCredentialType = 'oauth' | 'env_workspace' | 'env_personal' +export type WorkspaceCredentialRole = 'admin' | 'member' +export type WorkspaceCredentialMemberStatus = 'active' | 'pending' | 'revoked' + +export interface WorkspaceCredential { + id: string + workspaceId: string + type: WorkspaceCredentialType + displayName: string + providerId: string | null + accountId: string | null + envKey: string | null + envOwnerUserId: string | null + createdBy: string + createdAt: string + updatedAt: string + role?: WorkspaceCredentialRole + status?: WorkspaceCredentialMemberStatus +} + +export interface WorkspaceCredentialMember { + id: string + userId: string + role: WorkspaceCredentialRole + status: WorkspaceCredentialMemberStatus + joinedAt: string | null + invitedBy: string | null + createdAt: string + updatedAt: string + userName: string | null + userEmail: string | null + userImage: string | null +} + +interface CredentialListResponse { + credentials?: WorkspaceCredential[] +} + +interface CredentialResponse { + credential?: WorkspaceCredential | null +} + +interface MembersResponse { + members?: WorkspaceCredentialMember[] +} + +export const workspaceCredentialKeys = { + all: ['workspaceCredentials'] as const, + list: (workspaceId?: string, type?: string, providerId?: string) => + ['workspaceCredentials', workspaceId ?? 'none', type ?? 'all', providerId ?? 'all'] as const, + detail: (credentialId?: string) => + ['workspaceCredentials', 'detail', credentialId ?? 'none'] as const, + members: (credentialId?: string) => + ['workspaceCredentials', 'detail', credentialId ?? 'none', 'members'] as const, +} + +export function useWorkspaceCredentials(params: { + workspaceId?: string + type?: WorkspaceCredentialType + providerId?: string + enabled?: boolean +}) { + const { workspaceId, type, providerId, enabled = true } = params + + return useQuery({ + queryKey: workspaceCredentialKeys.list(workspaceId, type, providerId), + queryFn: async () => { + if (!workspaceId) return [] + const data = await fetchJson('/api/credentials', { + searchParams: { + workspaceId, + type, + providerId, + }, + }) + return data.credentials ?? [] + }, + enabled: Boolean(workspaceId) && enabled, + staleTime: 60 * 1000, + }) +} + +export function useWorkspaceCredential(credentialId?: string, enabled = true) { + return useQuery({ + queryKey: workspaceCredentialKeys.detail(credentialId), + queryFn: async () => { + if (!credentialId) return null + const data = await fetchJson(`/api/credentials/${credentialId}`) + return data.credential ?? null + }, + enabled: Boolean(credentialId) && enabled, + staleTime: 60 * 1000, + }) +} + +export function useCreateWorkspaceCredential() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: async (payload: { + workspaceId: string + type: WorkspaceCredentialType + displayName?: string + providerId?: string + accountId?: string + envKey?: string + envOwnerUserId?: string + }) => { + const response = await fetch('/api/credentials', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + }) + + if (!response.ok) { + const data = await response.json() + throw new Error(data.error || 'Failed to create credential') + } + + return response.json() + }, + onSuccess: (_data, variables) => { + queryClient.invalidateQueries({ + queryKey: workspaceCredentialKeys.list(variables.workspaceId), + }) + queryClient.invalidateQueries({ + queryKey: workspaceCredentialKeys.all, + }) + }, + }) +} + +export function useUpdateWorkspaceCredential() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: async (payload: { + credentialId: string + displayName?: string + accountId?: string + }) => { + const response = await fetch(`/api/credentials/${payload.credentialId}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + displayName: payload.displayName, + accountId: payload.accountId, + }), + }) + if (!response.ok) { + const data = await response.json() + throw new Error(data.error || 'Failed to update credential') + } + return response.json() + }, + onSuccess: (_data, variables) => { + queryClient.invalidateQueries({ + queryKey: workspaceCredentialKeys.detail(variables.credentialId), + }) + queryClient.invalidateQueries({ + queryKey: workspaceCredentialKeys.all, + }) + }, + }) +} + +export function useDeleteWorkspaceCredential() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: async (credentialId: string) => { + const response = await fetch(`/api/credentials/${credentialId}`, { + method: 'DELETE', + }) + if (!response.ok) { + const data = await response.json() + throw new Error(data.error || 'Failed to delete credential') + } + return response.json() + }, + onSuccess: (_data, credentialId) => { + queryClient.invalidateQueries({ queryKey: workspaceCredentialKeys.detail(credentialId) }) + queryClient.invalidateQueries({ queryKey: workspaceCredentialKeys.all }) + }, + }) +} + +export function useWorkspaceCredentialMembers(credentialId?: string) { + return useQuery({ + queryKey: workspaceCredentialKeys.members(credentialId), + queryFn: async () => { + if (!credentialId) return [] + const data = await fetchJson(`/api/credentials/${credentialId}/members`) + return data.members ?? [] + }, + enabled: Boolean(credentialId), + staleTime: 30 * 1000, + }) +} + +export function useUpsertWorkspaceCredentialMember() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: async (payload: { + credentialId: string + userId: string + role: WorkspaceCredentialRole + }) => { + const response = await fetch(`/api/credentials/${payload.credentialId}/members`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + userId: payload.userId, + role: payload.role, + }), + }) + if (!response.ok) { + const data = await response.json() + throw new Error(data.error || 'Failed to update credential member') + } + return response.json() + }, + onSuccess: (_data, variables) => { + queryClient.invalidateQueries({ + queryKey: workspaceCredentialKeys.members(variables.credentialId), + }) + queryClient.invalidateQueries({ + queryKey: workspaceCredentialKeys.detail(variables.credentialId), + }) + queryClient.invalidateQueries({ queryKey: workspaceCredentialKeys.all }) + }, + }) +} + +export function useRemoveWorkspaceCredentialMember() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: async (payload: { credentialId: string; userId: string }) => { + const response = await fetch( + `/api/credentials/${payload.credentialId}/members?userId=${encodeURIComponent(payload.userId)}`, + { method: 'DELETE' } + ) + if (!response.ok) { + const data = await response.json() + throw new Error(data.error || 'Failed to remove credential member') + } + return response.json() + }, + onSuccess: (_data, variables) => { + queryClient.invalidateQueries({ + queryKey: workspaceCredentialKeys.members(variables.credentialId), + }) + queryClient.invalidateQueries({ + queryKey: workspaceCredentialKeys.detail(variables.credentialId), + }) + queryClient.invalidateQueries({ queryKey: workspaceCredentialKeys.all }) + }, + }) +} diff --git a/apps/sim/hooks/queries/oauth-connections.ts b/apps/sim/hooks/queries/oauth-connections.ts index d6243f854..02dfe1dfe 100644 --- a/apps/sim/hooks/queries/oauth-connections.ts +++ b/apps/sim/hooks/queries/oauth-connections.ts @@ -169,9 +169,9 @@ export function useConnectOAuthService() { interface DisconnectServiceParams { provider: string - providerId: string + providerId?: string serviceId: string - accountId: string + accountId?: string } /** @@ -182,7 +182,7 @@ export function useDisconnectOAuthService() { const queryClient = useQueryClient() return useMutation({ - mutationFn: async ({ provider, providerId }: DisconnectServiceParams) => { + mutationFn: async ({ provider, providerId, accountId }: DisconnectServiceParams) => { const response = await fetch('/api/auth/oauth/disconnect', { method: 'POST', headers: { @@ -191,6 +191,7 @@ export function useDisconnectOAuthService() { body: JSON.stringify({ provider, providerId, + accountId, }), }) @@ -212,7 +213,8 @@ export function useDisconnectOAuthService() { oauthConnectionsKeys.connections(), previousServices.map((svc) => { if (svc.id === serviceId) { - const updatedAccounts = svc.accounts?.filter((acc) => acc.id !== accountId) || [] + const updatedAccounts = + accountId && svc.accounts ? svc.accounts.filter((acc) => acc.id !== accountId) : [] return { ...svc, accounts: updatedAccounts, diff --git a/apps/sim/hooks/queries/oauth-credentials.ts b/apps/sim/hooks/queries/oauth-credentials.ts index 414fae2d9..6a66fa4b6 100644 --- a/apps/sim/hooks/queries/oauth-credentials.ts +++ b/apps/sim/hooks/queries/oauth-credentials.ts @@ -1,6 +1,10 @@ import { useQuery } from '@tanstack/react-query' +import { + clearPendingOAuthCredentialDraft, + readPendingOAuthCredentialDraft, +} from '@/lib/credentials/client-state' import type { Credential } from '@/lib/oauth' -import { CREDENTIAL, CREDENTIAL_SET } from '@/executor/constants' +import { CREDENTIAL_SET } from '@/executor/constants' import { useCredentialSetDetail } from '@/hooks/queries/credential-sets' import { fetchJson } from '@/hooks/selectors/helpers' @@ -12,16 +16,108 @@ interface CredentialDetailResponse { credentials?: Credential[] } +interface AuthAccountsResponse { + accounts?: Array<{ id: string }> +} + export const oauthCredentialKeys = { - list: (providerId?: string) => ['oauthCredentials', providerId ?? 'none'] as const, + list: (providerId?: string, workspaceId?: string, workflowId?: string) => + [ + 'oauthCredentials', + providerId ?? 'none', + workspaceId ?? 'none', + workflowId ?? 'none', + ] as const, detail: (credentialId?: string, workflowId?: string) => ['oauthCredentialDetail', credentialId ?? 'none', workflowId ?? 'none'] as const, } -export async function fetchOAuthCredentials(providerId: string): Promise { +interface FetchOAuthCredentialsParams { + providerId: string + workspaceId?: string + workflowId?: string +} + +async function finalizePendingOAuthCredentialDraftIfNeeded(params: { + providerId: string + workspaceId?: string +}) { + const { providerId, workspaceId } = params + if (!workspaceId || !providerId) return + if (typeof window === 'undefined') return + + const draft = readPendingOAuthCredentialDraft() + if (!draft) return + if (draft.workspaceId !== workspaceId || draft.providerId !== providerId) return + + const draftAgeMs = Date.now() - draft.requestedAt + if (draftAgeMs > 15 * 60 * 1000) { + clearPendingOAuthCredentialDraft() + return + } + + const bootstrapResponse = await fetch('/api/credentials/bootstrap', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ workspaceId }), + }) + if (!bootstrapResponse.ok) { + return + } + + const accountsResponse = await fetch( + `/api/auth/accounts?provider=${encodeURIComponent(providerId)}` + ) + if (!accountsResponse.ok) { + return + } + const accountsData = (await accountsResponse.json()) as AuthAccountsResponse + const accountIds = (accountsData.accounts ?? []).map((account) => account.id) + if (accountIds.length === 0) { + return + } + + const targetAccountId = + accountIds.find((accountId) => !draft.existingAccountIds.includes(accountId)) ?? accountIds[0] + if (!targetAccountId) { + return + } + + const createResponse = await fetch('/api/credentials', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + workspaceId, + type: 'oauth', + displayName: draft.displayName, + providerId, + accountId: targetAccountId, + }), + }) + if (!createResponse.ok) { + return + } + + clearPendingOAuthCredentialDraft() + window.dispatchEvent( + new CustomEvent('oauth-credentials-updated', { + detail: { providerId, workspaceId }, + }) + ) +} + +export async function fetchOAuthCredentials( + params: FetchOAuthCredentialsParams +): Promise { + const { providerId, workspaceId, workflowId } = params if (!providerId) return [] + await finalizePendingOAuthCredentialDraftIfNeeded({ providerId, workspaceId }) const data = await fetchJson('/api/auth/oauth/credentials', { - searchParams: { provider: providerId }, + searchParams: { + provider: providerId, + workspaceId, + workflowId, + }, }) return data.credentials ?? [] } @@ -40,10 +136,44 @@ export async function fetchOAuthCredentialDetail( return data.credentials ?? [] } -export function useOAuthCredentials(providerId?: string, enabled = true) { +interface UseOAuthCredentialsOptions { + enabled?: boolean + workspaceId?: string + workflowId?: string +} + +function resolveOptions( + enabledOrOptions?: boolean | UseOAuthCredentialsOptions +): Required { + if (typeof enabledOrOptions === 'boolean') { + return { + enabled: enabledOrOptions, + workspaceId: '', + workflowId: '', + } + } + + return { + enabled: enabledOrOptions?.enabled ?? true, + workspaceId: enabledOrOptions?.workspaceId ?? '', + workflowId: enabledOrOptions?.workflowId ?? '', + } +} + +export function useOAuthCredentials( + providerId?: string, + enabledOrOptions?: boolean | UseOAuthCredentialsOptions +) { + const { enabled, workspaceId, workflowId } = resolveOptions(enabledOrOptions) + return useQuery({ - queryKey: oauthCredentialKeys.list(providerId), - queryFn: () => fetchOAuthCredentials(providerId ?? ''), + queryKey: oauthCredentialKeys.list(providerId, workspaceId, workflowId), + queryFn: () => + fetchOAuthCredentials({ + providerId: providerId ?? '', + workspaceId: workspaceId || undefined, + workflowId: workflowId || undefined, + }), enabled: Boolean(providerId) && enabled, staleTime: 60 * 1000, }) @@ -62,7 +192,12 @@ export function useOAuthCredentialDetail( }) } -export function useCredentialName(credentialId?: string, providerId?: string, workflowId?: string) { +export function useCredentialName( + credentialId?: string, + providerId?: string, + workflowId?: string, + workspaceId?: string +) { // Check if this is a credential set value const isCredentialSet = credentialId?.startsWith(CREDENTIAL_SET.PREFIX) ?? false const credentialSetId = isCredentialSet @@ -77,7 +212,11 @@ export function useCredentialName(credentialId?: string, providerId?: string, wo const { data: credentials = [], isFetching: credentialsLoading } = useOAuthCredentials( providerId, - Boolean(providerId) && !isCredentialSet + { + enabled: Boolean(providerId) && !isCredentialSet, + workspaceId, + workflowId, + } ) const selectedCredential = credentials.find((cred) => cred.id === credentialId) @@ -92,18 +231,18 @@ export function useCredentialName(credentialId?: string, providerId?: string, wo shouldFetchDetail ) + const detailCredential = foreignCredentials[0] const hasForeignMeta = foreignCredentials.length > 0 - const isForeignCredentialSet = isCredentialSet && !credentialSetData && !credentialSetLoading const displayName = - credentialSetData?.name ?? - selectedCredential?.name ?? - (hasForeignMeta ? CREDENTIAL.FOREIGN_LABEL : null) ?? - (isForeignCredentialSet ? CREDENTIAL.FOREIGN_LABEL : null) + credentialSetData?.name ?? selectedCredential?.name ?? detailCredential?.name ?? null return { displayName, - isLoading: credentialsLoading || foreignLoading || (isCredentialSet && credentialSetLoading), + isLoading: + credentialsLoading || + foreignLoading || + (isCredentialSet && credentialSetLoading && !credentialSetData), hasForeignMeta, } } diff --git a/apps/sim/lib/auth/credential-access.ts b/apps/sim/lib/auth/credential-access.ts index 61b0f655a..42fa2eb3c 100644 --- a/apps/sim/lib/auth/credential-access.ts +++ b/apps/sim/lib/auth/credential-access.ts @@ -1,6 +1,6 @@ import { db } from '@sim/db' -import { account, workflow as workflowTable } from '@sim/db/schema' -import { eq } from 'drizzle-orm' +import { account, credential, credentialMember, workflow as workflowTable } from '@sim/db/schema' +import { and, eq } from 'drizzle-orm' import type { NextRequest } from 'next/server' import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils' @@ -12,17 +12,14 @@ export interface CredentialAccessResult { requesterUserId?: string credentialOwnerUserId?: string workspaceId?: string + resolvedCredentialId?: string } /** - * Centralizes auth + collaboration rules for credential use. - * - Uses checkSessionOrInternalAuth to authenticate the caller - * - Fetches credential owner - * - Authorization rules: - * - session: allow if requester owns the credential; otherwise require workflowId and - * verify BOTH requester and owner have access to the workflow's workspace - * - internal_jwt: require workflowId (by default) and verify credential owner has access to the - * workflow's workspace (requester identity is the system/workflow) + * Centralizes auth + credential membership checks for OAuth usage. + * - Workspace-scoped credential IDs enforce active credential_member access. + * - Legacy account IDs are resolved to workspace-scoped credentials when workflowId is provided. + * - Direct legacy account-ID access without workflowId is restricted to account owners only. */ export async function authorizeCredentialUse( request: NextRequest, @@ -37,71 +34,173 @@ export async function authorizeCredentialUse( return { ok: false, error: auth.error || 'Authentication required' } } - // Lookup credential owner - const [credRow] = await db + const [workflowContext] = workflowId + ? await db + .select({ workspaceId: workflowTable.workspaceId }) + .from(workflowTable) + .where(eq(workflowTable.id, workflowId)) + .limit(1) + : [null] + + if (workflowId && (!workflowContext || !workflowContext.workspaceId)) { + return { ok: false, error: 'Workflow not found' } + } + + const [platformCredential] = await db + .select({ + id: credential.id, + workspaceId: credential.workspaceId, + type: credential.type, + accountId: credential.accountId, + }) + .from(credential) + .where(eq(credential.id, credentialId)) + .limit(1) + + if (platformCredential) { + if (platformCredential.type !== 'oauth' || !platformCredential.accountId) { + return { ok: false, error: 'Unsupported credential type for OAuth access' } + } + + if (workflowContext && workflowContext.workspaceId !== platformCredential.workspaceId) { + return { ok: false, error: 'Credential is not accessible from this workflow workspace' } + } + + const [accountRow] = await db + .select({ userId: account.userId }) + .from(account) + .where(eq(account.id, platformCredential.accountId)) + .limit(1) + + if (!accountRow) { + return { ok: false, error: 'Credential account not found' } + } + + const requesterPerm = + auth.authType === 'internal_jwt' + ? null + : await getUserEntityPermissions(auth.userId, 'workspace', platformCredential.workspaceId) + + if (auth.authType !== 'internal_jwt') { + const [membership] = await db + .select({ id: credentialMember.id }) + .from(credentialMember) + .where( + and( + eq(credentialMember.credentialId, platformCredential.id), + eq(credentialMember.userId, auth.userId), + eq(credentialMember.status, 'active') + ) + ) + .limit(1) + + if (!membership || requesterPerm === null) { + return { ok: false, error: 'Unauthorized' } + } + } + + const ownerPerm = await getUserEntityPermissions( + accountRow.userId, + 'workspace', + platformCredential.workspaceId + ) + if (ownerPerm === null) { + return { ok: false, error: 'Unauthorized' } + } + + return { + ok: true, + authType: auth.authType as CredentialAccessResult['authType'], + requesterUserId: auth.userId, + credentialOwnerUserId: accountRow.userId, + workspaceId: platformCredential.workspaceId, + resolvedCredentialId: platformCredential.accountId, + } + } + + if (workflowContext?.workspaceId) { + const [workspaceCredential] = await db + .select({ + id: credential.id, + workspaceId: credential.workspaceId, + accountId: credential.accountId, + }) + .from(credential) + .where( + and( + eq(credential.type, 'oauth'), + eq(credential.workspaceId, workflowContext.workspaceId), + eq(credential.accountId, credentialId) + ) + ) + .limit(1) + + if (!workspaceCredential?.accountId) { + return { ok: false, error: 'Credential not found' } + } + + const [accountRow] = await db + .select({ userId: account.userId }) + .from(account) + .where(eq(account.id, workspaceCredential.accountId)) + .limit(1) + + if (!accountRow) { + return { ok: false, error: 'Credential account not found' } + } + + if (auth.authType !== 'internal_jwt') { + const [membership] = await db + .select({ id: credentialMember.id }) + .from(credentialMember) + .where( + and( + eq(credentialMember.credentialId, workspaceCredential.id), + eq(credentialMember.userId, auth.userId), + eq(credentialMember.status, 'active') + ) + ) + .limit(1) + + if (!membership) { + return { ok: false, error: 'Unauthorized' } + } + } + + const ownerPerm = await getUserEntityPermissions( + accountRow.userId, + 'workspace', + workflowContext.workspaceId + ) + if (ownerPerm === null) { + return { ok: false, error: 'Unauthorized' } + } + + return { + ok: true, + authType: auth.authType as CredentialAccessResult['authType'], + requesterUserId: auth.userId, + credentialOwnerUserId: accountRow.userId, + workspaceId: workflowContext.workspaceId, + resolvedCredentialId: workspaceCredential.accountId, + } + } + + const [legacyAccount] = await db .select({ userId: account.userId }) .from(account) .where(eq(account.id, credentialId)) .limit(1) - if (!credRow) { + if (!legacyAccount) { return { ok: false, error: 'Credential not found' } } - const credentialOwnerUserId = credRow.userId - - // If requester owns the credential, allow immediately - if (auth.authType !== 'internal_jwt' && auth.userId === credentialOwnerUserId) { - return { - ok: true, - authType: auth.authType as CredentialAccessResult['authType'], - requesterUserId: auth.userId, - credentialOwnerUserId, - } - } - - // For collaboration paths, workflowId is required to scope to a workspace - if (!workflowId) { + if (auth.authType === 'internal_jwt') { return { ok: false, error: 'workflowId is required' } } - const [wf] = await db - .select({ workspaceId: workflowTable.workspaceId }) - .from(workflowTable) - .where(eq(workflowTable.id, workflowId)) - .limit(1) - - if (!wf || !wf.workspaceId) { - return { ok: false, error: 'Workflow not found' } - } - - if (auth.authType === 'internal_jwt') { - // Internal calls: verify credential owner belongs to the workflow's workspace - const ownerPerm = await getUserEntityPermissions( - credentialOwnerUserId, - 'workspace', - wf.workspaceId - ) - if (ownerPerm === null) { - return { ok: false, error: 'Unauthorized' } - } - return { - ok: true, - authType: auth.authType as CredentialAccessResult['authType'], - requesterUserId: auth.userId, - credentialOwnerUserId, - workspaceId: wf.workspaceId, - } - } - - // Session: verify BOTH requester and owner belong to the workflow's workspace - const requesterPerm = await getUserEntityPermissions(auth.userId, 'workspace', wf.workspaceId) - const ownerPerm = await getUserEntityPermissions( - credentialOwnerUserId, - 'workspace', - wf.workspaceId - ) - if (requesterPerm === null || ownerPerm === null) { + if (auth.userId !== legacyAccount.userId) { return { ok: false, error: 'Unauthorized' } } @@ -109,7 +208,7 @@ export async function authorizeCredentialUse( ok: true, authType: auth.authType as CredentialAccessResult['authType'], requesterUserId: auth.userId, - credentialOwnerUserId, - workspaceId: wf.workspaceId, + credentialOwnerUserId: legacyAccount.userId, + resolvedCredentialId: credentialId, } } diff --git a/apps/sim/lib/credentials/access.ts b/apps/sim/lib/credentials/access.ts new file mode 100644 index 000000000..21b12b04d --- /dev/null +++ b/apps/sim/lib/credentials/access.ts @@ -0,0 +1,62 @@ +import { db } from '@sim/db' +import { credential, credentialMember } from '@sim/db/schema' +import { and, eq } from 'drizzle-orm' +import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils' + +type ActiveCredentialMember = typeof credentialMember.$inferSelect +type CredentialRecord = typeof credential.$inferSelect + +export interface CredentialActorContext { + credential: CredentialRecord | null + member: ActiveCredentialMember | null + hasWorkspaceAccess: boolean + canWriteWorkspace: boolean + isAdmin: boolean +} + +/** + * Resolves user access context for a credential. + */ +export async function getCredentialActorContext( + credentialId: string, + userId: string +): Promise { + const [credentialRow] = await db + .select() + .from(credential) + .where(eq(credential.id, credentialId)) + .limit(1) + + if (!credentialRow) { + return { + credential: null, + member: null, + hasWorkspaceAccess: false, + canWriteWorkspace: false, + isAdmin: false, + } + } + + const workspaceAccess = await checkWorkspaceAccess(credentialRow.workspaceId, userId) + const [memberRow] = await db + .select() + .from(credentialMember) + .where( + and( + eq(credentialMember.credentialId, credentialId), + eq(credentialMember.userId, userId), + eq(credentialMember.status, 'active') + ) + ) + .limit(1) + + const isAdmin = memberRow?.role === 'admin' + + return { + credential: credentialRow, + member: memberRow ?? null, + hasWorkspaceAccess: workspaceAccess.hasAccess, + canWriteWorkspace: workspaceAccess.canWrite, + isAdmin, + } +} diff --git a/apps/sim/lib/credentials/client-state.ts b/apps/sim/lib/credentials/client-state.ts new file mode 100644 index 000000000..4760f74fb --- /dev/null +++ b/apps/sim/lib/credentials/client-state.ts @@ -0,0 +1,66 @@ +'use client' + +export const PENDING_OAUTH_CREDENTIAL_DRAFT_KEY = 'sim.pending-oauth-credential-draft' +export const PENDING_CREDENTIAL_CREATE_REQUEST_KEY = 'sim.pending-credential-create-request' + +export interface PendingOAuthCredentialDraft { + workspaceId: string + providerId: string + displayName: string + existingCredentialIds: string[] + existingAccountIds: string[] + requestedAt: number +} + +export interface PendingCredentialCreateRequest { + workspaceId: string + type: 'oauth' + providerId: string + displayName: string + serviceId: string + requiredScopes: string[] + requestedAt: number +} + +function parseJson(raw: string | null): T | null { + if (!raw) return null + try { + return JSON.parse(raw) as T + } catch { + return null + } +} + +export function readPendingOAuthCredentialDraft(): PendingOAuthCredentialDraft | null { + if (typeof window === 'undefined') return null + return parseJson( + window.sessionStorage.getItem(PENDING_OAUTH_CREDENTIAL_DRAFT_KEY) + ) +} + +export function writePendingOAuthCredentialDraft(payload: PendingOAuthCredentialDraft) { + if (typeof window === 'undefined') return + window.sessionStorage.setItem(PENDING_OAUTH_CREDENTIAL_DRAFT_KEY, JSON.stringify(payload)) +} + +export function clearPendingOAuthCredentialDraft() { + if (typeof window === 'undefined') return + window.sessionStorage.removeItem(PENDING_OAUTH_CREDENTIAL_DRAFT_KEY) +} + +export function readPendingCredentialCreateRequest(): PendingCredentialCreateRequest | null { + if (typeof window === 'undefined') return null + return parseJson( + window.sessionStorage.getItem(PENDING_CREDENTIAL_CREATE_REQUEST_KEY) + ) +} + +export function writePendingCredentialCreateRequest(payload: PendingCredentialCreateRequest) { + if (typeof window === 'undefined') return + window.sessionStorage.setItem(PENDING_CREDENTIAL_CREATE_REQUEST_KEY, JSON.stringify(payload)) +} + +export function clearPendingCredentialCreateRequest() { + if (typeof window === 'undefined') return + window.sessionStorage.removeItem(PENDING_CREDENTIAL_CREATE_REQUEST_KEY) +} diff --git a/apps/sim/lib/credentials/environment.ts b/apps/sim/lib/credentials/environment.ts new file mode 100644 index 000000000..dffeb9277 --- /dev/null +++ b/apps/sim/lib/credentials/environment.ts @@ -0,0 +1,340 @@ +import { db } from '@sim/db' +import { credential, credentialMember, permissions, workspace } from '@sim/db/schema' +import { and, eq, inArray, notInArray } from 'drizzle-orm' + +interface AccessibleEnvCredential { + type: 'env_workspace' | 'env_personal' + envKey: string + envOwnerUserId: string | null + updatedAt: Date +} + +export async function getWorkspaceMemberUserIds(workspaceId: string): Promise { + const [workspaceRows, permissionRows] = await Promise.all([ + db + .select({ ownerId: workspace.ownerId }) + .from(workspace) + .where(eq(workspace.id, workspaceId)) + .limit(1), + db + .select({ userId: permissions.userId }) + .from(permissions) + .where(and(eq(permissions.entityType, 'workspace'), eq(permissions.entityId, workspaceId))), + ]) + const workspaceRow = workspaceRows[0] + + const memberIds = new Set(permissionRows.map((row) => row.userId)) + if (workspaceRow?.ownerId) { + memberIds.add(workspaceRow.ownerId) + } + return Array.from(memberIds) +} + +export async function getUserWorkspaceIds(userId: string): Promise { + const [permissionRows, ownedWorkspaceRows] = await Promise.all([ + db + .select({ workspaceId: workspace.id }) + .from(permissions) + .innerJoin( + workspace, + and(eq(permissions.entityType, 'workspace'), eq(permissions.entityId, workspace.id)) + ) + .where(eq(permissions.userId, userId)), + db.select({ workspaceId: workspace.id }).from(workspace).where(eq(workspace.ownerId, userId)), + ]) + + const workspaceIds = new Set(permissionRows.map((row) => row.workspaceId)) + for (const row of ownedWorkspaceRows) { + workspaceIds.add(row.workspaceId) + } + + return Array.from(workspaceIds) +} + +async function upsertCredentialAdminMember(credentialId: string, adminUserId: string) { + const now = new Date() + const [existingMembership] = await db + .select({ id: credentialMember.id, joinedAt: credentialMember.joinedAt }) + .from(credentialMember) + .where( + and(eq(credentialMember.credentialId, credentialId), eq(credentialMember.userId, adminUserId)) + ) + .limit(1) + + if (existingMembership) { + await db + .update(credentialMember) + .set({ + role: 'admin', + status: 'active', + joinedAt: existingMembership.joinedAt ?? now, + invitedBy: adminUserId, + updatedAt: now, + }) + .where(eq(credentialMember.id, existingMembership.id)) + return + } + + await db.insert(credentialMember).values({ + id: crypto.randomUUID(), + credentialId, + userId: adminUserId, + role: 'admin', + status: 'active', + joinedAt: now, + invitedBy: adminUserId, + createdAt: now, + updatedAt: now, + }) +} + +async function ensureWorkspaceCredentialMemberships( + credentialId: string, + workspaceId: string, + ownerUserId: string +) { + const workspaceMemberUserIds = await getWorkspaceMemberUserIds(workspaceId) + if (!workspaceMemberUserIds.length) return + + const existingMemberships = await db + .select({ + id: credentialMember.id, + userId: credentialMember.userId, + joinedAt: credentialMember.joinedAt, + }) + .from(credentialMember) + .where( + and( + eq(credentialMember.credentialId, credentialId), + inArray(credentialMember.userId, workspaceMemberUserIds) + ) + ) + + const byUserId = new Map(existingMemberships.map((row) => [row.userId, row])) + const now = new Date() + + for (const memberUserId of workspaceMemberUserIds) { + const targetRole = memberUserId === ownerUserId ? 'admin' : 'member' + const existing = byUserId.get(memberUserId) + if (existing) { + await db + .update(credentialMember) + .set({ + role: targetRole, + status: 'active', + joinedAt: existing.joinedAt ?? now, + invitedBy: ownerUserId, + updatedAt: now, + }) + .where(eq(credentialMember.id, existing.id)) + continue + } + + await db.insert(credentialMember).values({ + id: crypto.randomUUID(), + credentialId, + userId: memberUserId, + role: targetRole, + status: 'active', + joinedAt: now, + invitedBy: ownerUserId, + createdAt: now, + updatedAt: now, + }) + } +} + +export async function syncWorkspaceEnvCredentials(params: { + workspaceId: string + envKeys: string[] + actingUserId: string +}) { + const { workspaceId, envKeys, actingUserId } = params + const [workspaceRow] = await db + .select({ ownerId: workspace.ownerId }) + .from(workspace) + .where(eq(workspace.id, workspaceId)) + .limit(1) + + if (!workspaceRow) return + + const normalizedKeys = Array.from(new Set(envKeys.filter(Boolean))) + const existingCredentials = await db + .select({ + id: credential.id, + envKey: credential.envKey, + }) + .from(credential) + .where(and(eq(credential.workspaceId, workspaceId), eq(credential.type, 'env_workspace'))) + + const existingByKey = new Map( + existingCredentials + .filter((row): row is { id: string; envKey: string } => Boolean(row.envKey)) + .map((row) => [row.envKey, row.id]) + ) + + const credentialIdsToEnsureMembership = new Set() + const now = new Date() + + for (const envKey of normalizedKeys) { + const existingId = existingByKey.get(envKey) + if (existingId) { + credentialIdsToEnsureMembership.add(existingId) + continue + } + + const createdId = crypto.randomUUID() + await db.insert(credential).values({ + id: createdId, + workspaceId, + type: 'env_workspace', + displayName: envKey, + envKey, + createdBy: actingUserId, + createdAt: now, + updatedAt: now, + }) + credentialIdsToEnsureMembership.add(createdId) + } + + for (const credentialId of credentialIdsToEnsureMembership) { + await ensureWorkspaceCredentialMemberships(credentialId, workspaceId, workspaceRow.ownerId) + } + + if (normalizedKeys.length > 0) { + await db + .delete(credential) + .where( + and( + eq(credential.workspaceId, workspaceId), + eq(credential.type, 'env_workspace'), + notInArray(credential.envKey, normalizedKeys) + ) + ) + return + } + + await db + .delete(credential) + .where(and(eq(credential.workspaceId, workspaceId), eq(credential.type, 'env_workspace'))) +} + +export async function syncPersonalEnvCredentialsForUser(params: { + userId: string + envKeys: string[] +}) { + const { userId, envKeys } = params + const workspaceIds = await getUserWorkspaceIds(userId) + if (!workspaceIds.length) return + + const normalizedKeys = Array.from(new Set(envKeys.filter(Boolean))) + const now = new Date() + + for (const workspaceId of workspaceIds) { + const existingCredentials = await db + .select({ + id: credential.id, + envKey: credential.envKey, + }) + .from(credential) + .where( + and( + eq(credential.workspaceId, workspaceId), + eq(credential.type, 'env_personal'), + eq(credential.envOwnerUserId, userId) + ) + ) + + const existingByKey = new Map( + existingCredentials + .filter((row): row is { id: string; envKey: string } => Boolean(row.envKey)) + .map((row) => [row.envKey, row.id]) + ) + + for (const envKey of normalizedKeys) { + const existingId = existingByKey.get(envKey) + if (existingId) { + await upsertCredentialAdminMember(existingId, userId) + continue + } + + const createdId = crypto.randomUUID() + await db.insert(credential).values({ + id: createdId, + workspaceId, + type: 'env_personal', + displayName: envKey, + envKey, + envOwnerUserId: userId, + createdBy: userId, + createdAt: now, + updatedAt: now, + }) + await upsertCredentialAdminMember(createdId, userId) + } + + if (normalizedKeys.length > 0) { + await db + .delete(credential) + .where( + and( + eq(credential.workspaceId, workspaceId), + eq(credential.type, 'env_personal'), + eq(credential.envOwnerUserId, userId), + notInArray(credential.envKey, normalizedKeys) + ) + ) + continue + } + + await db + .delete(credential) + .where( + and( + eq(credential.workspaceId, workspaceId), + eq(credential.type, 'env_personal'), + eq(credential.envOwnerUserId, userId) + ) + ) + } +} + +export async function getAccessibleEnvCredentials( + workspaceId: string, + userId: string +): Promise { + const rows = await db + .select({ + type: credential.type, + envKey: credential.envKey, + envOwnerUserId: credential.envOwnerUserId, + updatedAt: credential.updatedAt, + }) + .from(credential) + .innerJoin( + credentialMember, + and( + eq(credentialMember.credentialId, credential.id), + eq(credentialMember.userId, userId), + eq(credentialMember.status, 'active') + ) + ) + .where( + and( + eq(credential.workspaceId, workspaceId), + inArray(credential.type, ['env_workspace', 'env_personal']) + ) + ) + + return rows + .filter( + (row): row is AccessibleEnvCredential => + (row.type === 'env_workspace' || row.type === 'env_personal') && Boolean(row.envKey) + ) + .map((row) => ({ + type: row.type, + envKey: row.envKey!, + envOwnerUserId: row.envOwnerUserId, + updatedAt: row.updatedAt, + })) +} diff --git a/apps/sim/lib/credentials/oauth.ts b/apps/sim/lib/credentials/oauth.ts new file mode 100644 index 000000000..6d434fc9e --- /dev/null +++ b/apps/sim/lib/credentials/oauth.ts @@ -0,0 +1,157 @@ +import { db } from '@sim/db' +import { account, credential, credentialMember } from '@sim/db/schema' +import { and, eq, inArray } from 'drizzle-orm' + +interface SyncWorkspaceOAuthCredentialsForUserParams { + workspaceId: string + userId: string +} + +interface SyncWorkspaceOAuthCredentialsForUserResult { + createdCredentials: number + updatedMemberships: number +} + +/** + * Ensures connected OAuth accounts for a user exist as workspace-scoped credentials. + */ +export async function syncWorkspaceOAuthCredentialsForUser( + params: SyncWorkspaceOAuthCredentialsForUserParams +): Promise { + const { workspaceId, userId } = params + + const userAccounts = await db + .select({ + id: account.id, + providerId: account.providerId, + accountId: account.accountId, + }) + .from(account) + .where(eq(account.userId, userId)) + + if (userAccounts.length === 0) { + return { createdCredentials: 0, updatedMemberships: 0 } + } + + const accountIds = userAccounts.map((row) => row.id) + const existingCredentials = await db + .select({ + id: credential.id, + accountId: credential.accountId, + }) + .from(credential) + .where( + and( + eq(credential.workspaceId, workspaceId), + eq(credential.type, 'oauth'), + inArray(credential.accountId, accountIds) + ) + ) + + const existingByAccountId = new Map( + existingCredentials + .filter((row): row is { id: string; accountId: string } => Boolean(row.accountId)) + .map((row) => [row.accountId, row.id]) + ) + + let createdCredentials = 0 + const now = new Date() + + for (const acc of userAccounts) { + if (existingByAccountId.has(acc.id)) { + continue + } + + try { + await db.insert(credential).values({ + id: crypto.randomUUID(), + workspaceId, + type: 'oauth', + displayName: acc.accountId || acc.providerId, + providerId: acc.providerId, + accountId: acc.id, + createdBy: userId, + createdAt: now, + updatedAt: now, + }) + createdCredentials += 1 + } catch (error: any) { + if (error?.code !== '23505') { + throw error + } + } + } + + const credentialRows = await db + .select({ id: credential.id, accountId: credential.accountId }) + .from(credential) + .where( + and( + eq(credential.workspaceId, workspaceId), + eq(credential.type, 'oauth'), + inArray(credential.accountId, accountIds) + ) + ) + + const credentialIdByAccountId = new Map( + credentialRows + .filter((row): row is { id: string; accountId: string } => Boolean(row.accountId)) + .map((row) => [row.accountId, row.id]) + ) + const allCredentialIds = Array.from(credentialIdByAccountId.values()) + if (allCredentialIds.length === 0) { + return { createdCredentials, updatedMemberships: 0 } + } + + const existingMemberships = await db + .select({ + id: credentialMember.id, + credentialId: credentialMember.credentialId, + joinedAt: credentialMember.joinedAt, + }) + .from(credentialMember) + .where( + and( + inArray(credentialMember.credentialId, allCredentialIds), + eq(credentialMember.userId, userId) + ) + ) + + const membershipByCredentialId = new Map( + existingMemberships.map((row) => [row.credentialId, row]) + ) + let updatedMemberships = 0 + + for (const credentialId of allCredentialIds) { + const existingMembership = membershipByCredentialId.get(credentialId) + if (existingMembership) { + await db + .update(credentialMember) + .set({ + role: 'admin', + status: 'active', + joinedAt: existingMembership.joinedAt ?? now, + invitedBy: userId, + updatedAt: now, + }) + .where(eq(credentialMember.id, existingMembership.id)) + updatedMemberships += 1 + continue + } + + await db.insert(credentialMember).values({ + id: crypto.randomUUID(), + credentialId, + userId, + role: 'admin', + status: 'active', + joinedAt: now, + invitedBy: userId, + createdAt: now, + updatedAt: now, + }) + updatedMemberships += 1 + } + + return { createdCredentials, updatedMemberships } +} diff --git a/apps/sim/lib/environment/utils.ts b/apps/sim/lib/environment/utils.ts index 7dbf63c1c..e10be496e 100644 --- a/apps/sim/lib/environment/utils.ts +++ b/apps/sim/lib/environment/utils.ts @@ -1,8 +1,9 @@ import { db } from '@sim/db' import { environment, workspaceEnvironment } from '@sim/db/schema' import { createLogger } from '@sim/logger' -import { eq } from 'drizzle-orm' +import { eq, inArray } from 'drizzle-orm' import { decryptSecret } from '@/lib/core/security/encryption' +import { getAccessibleEnvCredentials } from '@/lib/credentials/environment' const logger = createLogger('EnvironmentUtils') @@ -53,7 +54,7 @@ export async function getPersonalAndWorkspaceEnv( conflicts: string[] decryptionFailures: string[] }> { - const [personalRows, workspaceRows] = await Promise.all([ + const [personalRows, workspaceRows, accessibleEnvCredentials] = await Promise.all([ db.select().from(environment).where(eq(environment.userId, userId)).limit(1), workspaceId ? db @@ -62,10 +63,69 @@ export async function getPersonalAndWorkspaceEnv( .where(eq(workspaceEnvironment.workspaceId, workspaceId)) .limit(1) : Promise.resolve([] as any[]), + workspaceId ? getAccessibleEnvCredentials(workspaceId, userId) : Promise.resolve([]), ]) - const personalEncrypted: Record = (personalRows[0]?.variables as any) || {} - const workspaceEncrypted: Record = (workspaceRows[0]?.variables as any) || {} + const ownPersonalEncrypted: Record = (personalRows[0]?.variables as any) || {} + const allWorkspaceEncrypted: Record = (workspaceRows[0]?.variables as any) || {} + + const hasCredentialFiltering = Boolean(workspaceId) && accessibleEnvCredentials.length > 0 + const workspaceCredentialKeys = new Set( + accessibleEnvCredentials.filter((row) => row.type === 'env_workspace').map((row) => row.envKey) + ) + + const personalCredentialRows = accessibleEnvCredentials + .filter((row) => row.type === 'env_personal' && row.envOwnerUserId) + .sort((a, b) => { + const aIsRequester = a.envOwnerUserId === userId + const bIsRequester = b.envOwnerUserId === userId + if (aIsRequester && !bIsRequester) return -1 + if (!aIsRequester && bIsRequester) return 1 + return b.updatedAt.getTime() - a.updatedAt.getTime() + }) + + const selectedPersonalOwners = new Map() + for (const row of personalCredentialRows) { + if (!selectedPersonalOwners.has(row.envKey) && row.envOwnerUserId) { + selectedPersonalOwners.set(row.envKey, row.envOwnerUserId) + } + } + + const ownerUserIds = Array.from(new Set(selectedPersonalOwners.values())) + const ownerEnvironmentRows = + ownerUserIds.length > 0 + ? await db + .select({ + userId: environment.userId, + variables: environment.variables, + }) + .from(environment) + .where(inArray(environment.userId, ownerUserIds)) + : [] + + const ownerVariablesByUserId = new Map>( + ownerEnvironmentRows.map((row) => [row.userId, (row.variables as Record) || {}]) + ) + + let personalEncrypted: Record = ownPersonalEncrypted + let workspaceEncrypted: Record = allWorkspaceEncrypted + + if (hasCredentialFiltering) { + personalEncrypted = {} + for (const [envKey, ownerUserId] of selectedPersonalOwners.entries()) { + const ownerVariables = ownerVariablesByUserId.get(ownerUserId) + const encryptedValue = ownerVariables?.[envKey] + if (encryptedValue) { + personalEncrypted[envKey] = encryptedValue + } + } + + workspaceEncrypted = Object.fromEntries( + Object.entries(allWorkspaceEncrypted).filter(([envKey]) => + workspaceCredentialKeys.has(envKey) + ) + ) + } const decryptionFailures: string[] = [] diff --git a/apps/sim/stores/modals/settings/types.ts b/apps/sim/stores/modals/settings/types.ts index 2903bff8d..e010c6fa3 100644 --- a/apps/sim/stores/modals/settings/types.ts +++ b/apps/sim/stores/modals/settings/types.ts @@ -1,5 +1,6 @@ export type SettingsSection = | 'general' + | 'credentials' | 'environment' | 'template-profile' | 'integrations' diff --git a/packages/db/migrations/0154_luxuriant_maria_hill.sql b/packages/db/migrations/0154_luxuriant_maria_hill.sql new file mode 100644 index 000000000..6e1d2fc75 --- /dev/null +++ b/packages/db/migrations/0154_luxuriant_maria_hill.sql @@ -0,0 +1,260 @@ +CREATE TYPE "public"."credential_member_role" AS ENUM('admin', 'member');--> statement-breakpoint +CREATE TYPE "public"."credential_member_status" AS ENUM('active', 'pending', 'revoked');--> statement-breakpoint +CREATE TYPE "public"."credential_type" AS ENUM('oauth', 'env_workspace', 'env_personal');--> statement-breakpoint +CREATE TABLE "credential" ( + "id" text PRIMARY KEY NOT NULL, + "workspace_id" text NOT NULL, + "type" "credential_type" NOT NULL, + "display_name" text NOT NULL, + "provider_id" text, + "account_id" text, + "env_key" text, + "env_owner_user_id" text, + "created_by" text NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "credential_oauth_source_check" CHECK ((type <> 'oauth') OR (account_id IS NOT NULL AND provider_id IS NOT NULL)), + CONSTRAINT "credential_workspace_env_source_check" CHECK ((type <> 'env_workspace') OR (env_key IS NOT NULL AND env_owner_user_id IS NULL)), + CONSTRAINT "credential_personal_env_source_check" CHECK ((type <> 'env_personal') OR (env_key IS NOT NULL AND env_owner_user_id IS NOT NULL)) +); +--> statement-breakpoint +CREATE TABLE "credential_member" ( + "id" text PRIMARY KEY NOT NULL, + "credential_id" text NOT NULL, + "user_id" text NOT NULL, + "role" "credential_member_role" DEFAULT 'member' NOT NULL, + "status" "credential_member_status" DEFAULT 'active' NOT NULL, + "joined_at" timestamp, + "invited_by" text, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "credential" ADD CONSTRAINT "credential_workspace_id_workspace_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspace"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "credential" ADD CONSTRAINT "credential_account_id_account_id_fk" FOREIGN KEY ("account_id") REFERENCES "public"."account"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "credential" ADD CONSTRAINT "credential_env_owner_user_id_user_id_fk" FOREIGN KEY ("env_owner_user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "credential" ADD CONSTRAINT "credential_created_by_user_id_fk" FOREIGN KEY ("created_by") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "credential_member" ADD CONSTRAINT "credential_member_credential_id_credential_id_fk" FOREIGN KEY ("credential_id") REFERENCES "public"."credential"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "credential_member" ADD CONSTRAINT "credential_member_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "credential_member" ADD CONSTRAINT "credential_member_invited_by_user_id_fk" FOREIGN KEY ("invited_by") REFERENCES "public"."user"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "credential_workspace_id_idx" ON "credential" USING btree ("workspace_id");--> statement-breakpoint +CREATE INDEX "credential_type_idx" ON "credential" USING btree ("type");--> statement-breakpoint +CREATE INDEX "credential_provider_id_idx" ON "credential" USING btree ("provider_id");--> statement-breakpoint +CREATE INDEX "credential_account_id_idx" ON "credential" USING btree ("account_id");--> statement-breakpoint +CREATE INDEX "credential_env_owner_user_id_idx" ON "credential" USING btree ("env_owner_user_id");--> statement-breakpoint +CREATE UNIQUE INDEX "credential_workspace_account_unique" ON "credential" USING btree ("workspace_id","account_id") WHERE account_id IS NOT NULL;--> statement-breakpoint +CREATE UNIQUE INDEX "credential_workspace_env_unique" ON "credential" USING btree ("workspace_id","type","env_key") WHERE type = 'env_workspace';--> statement-breakpoint +CREATE UNIQUE INDEX "credential_workspace_personal_env_unique" ON "credential" USING btree ("workspace_id","type","env_key","env_owner_user_id") WHERE type = 'env_personal';--> statement-breakpoint +CREATE INDEX "credential_member_credential_id_idx" ON "credential_member" USING btree ("credential_id");--> statement-breakpoint +CREATE INDEX "credential_member_user_id_idx" ON "credential_member" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "credential_member_role_idx" ON "credential_member" USING btree ("role");--> statement-breakpoint +CREATE INDEX "credential_member_status_idx" ON "credential_member" USING btree ("status");--> statement-breakpoint +CREATE UNIQUE INDEX "credential_member_unique" ON "credential_member" USING btree ("credential_id","user_id"); +--> statement-breakpoint +DROP INDEX IF EXISTS "account_user_provider_unique"; +--> statement-breakpoint +WITH workspace_user_access AS ( + SELECT DISTINCT w.id AS workspace_id, p.user_id + FROM "permissions" p + INNER JOIN "workspace" w + ON w.id = p.entity_id + WHERE p.entity_type = 'workspace' + UNION + SELECT w.id AS workspace_id, w.owner_id AS user_id + FROM "workspace" w + UNION + SELECT DISTINCT wf.workspace_id AS workspace_id, wf.user_id + FROM "workflow" wf + INNER JOIN "workspace" w + ON w.id = wf.workspace_id + WHERE wf.workspace_id IS NOT NULL +) +INSERT INTO "credential" ( + "id", + "workspace_id", + "type", + "display_name", + "provider_id", + "account_id", + "created_by", + "created_at", + "updated_at" +) +SELECT + 'cred_' || md5('oauth:' || wua.workspace_id || ':' || a.id) AS id, + wua.workspace_id, + 'oauth'::"credential_type", + COALESCE(NULLIF(a.account_id, ''), a.provider_id) AS display_name, + a.provider_id, + a.id, + a.user_id, + now(), + now() +FROM "account" a +INNER JOIN workspace_user_access wua + ON wua.user_id = a.user_id +ON CONFLICT DO NOTHING; +--> statement-breakpoint +INSERT INTO "credential" ( + "id", + "workspace_id", + "type", + "display_name", + "env_key", + "created_by", + "created_at", + "updated_at" +) +SELECT + 'cred_' || md5('env_workspace:' || we.workspace_id || ':' || env.key) AS id, + we.workspace_id, + 'env_workspace'::"credential_type", + env.key AS display_name, + env.key, + COALESCE(wf_owner.user_id, w.owner_id), + now(), + now() +FROM "workspace_environment" we +INNER JOIN "workspace" w + ON w.id = we.workspace_id +LEFT JOIN LATERAL ( + SELECT wf.user_id + FROM "workflow" wf + WHERE wf.workspace_id = we.workspace_id + ORDER BY wf.created_at ASC + LIMIT 1 +) wf_owner + ON TRUE +CROSS JOIN LATERAL jsonb_each_text(COALESCE(we.variables::jsonb, '{}'::jsonb)) AS env(key, value) +ON CONFLICT DO NOTHING; +--> statement-breakpoint +WITH workflow_workspace_owners AS ( + SELECT DISTINCT wf.workspace_id, wf.user_id + FROM "workflow" wf + INNER JOIN "workspace" w + ON w.id = wf.workspace_id + WHERE wf.workspace_id IS NOT NULL +) +INSERT INTO "credential" ( + "id", + "workspace_id", + "type", + "display_name", + "env_key", + "env_owner_user_id", + "created_by", + "created_at", + "updated_at" +) +SELECT + 'cred_' || md5('env_personal:' || wwo.workspace_id || ':' || e.user_id || ':' || env.key) AS id, + wwo.workspace_id, + 'env_personal'::"credential_type", + env.key AS display_name, + env.key, + e.user_id, + e.user_id, + now(), + now() +FROM "environment" e +INNER JOIN workflow_workspace_owners wwo + ON wwo.user_id = e.user_id +CROSS JOIN LATERAL jsonb_each_text(COALESCE(e.variables::jsonb, '{}'::jsonb)) AS env(key, value) +ON CONFLICT DO NOTHING; +--> statement-breakpoint +WITH workspace_user_access AS ( + SELECT DISTINCT w.id AS workspace_id, p.user_id + FROM "permissions" p + INNER JOIN "workspace" w + ON w.id = p.entity_id + WHERE p.entity_type = 'workspace' + UNION + SELECT w.id AS workspace_id, w.owner_id AS user_id + FROM "workspace" w + UNION + SELECT DISTINCT wf.workspace_id AS workspace_id, wf.user_id + FROM "workflow" wf + INNER JOIN "workspace" w + ON w.id = wf.workspace_id + WHERE wf.workspace_id IS NOT NULL +), +workflow_workspace_owners AS ( + SELECT DISTINCT wf.workspace_id, wf.user_id + FROM "workflow" wf + INNER JOIN "workspace" w + ON w.id = wf.workspace_id + WHERE wf.workspace_id IS NOT NULL +) +INSERT INTO "credential_member" ( + "id", + "credential_id", + "user_id", + "role", + "status", + "joined_at", + "invited_by", + "created_at", + "updated_at" +) +SELECT + 'credm_' || md5(c.id || ':' || wua.user_id) AS id, + c.id, + wua.user_id, + CASE + WHEN c.type = 'oauth'::"credential_type" AND c.created_by = wua.user_id THEN 'admin'::"credential_member_role" + WHEN c.type = 'env_workspace'::"credential_type" AND ( + EXISTS ( + SELECT 1 + FROM workflow_workspace_owners wwo + WHERE wwo.workspace_id = c.workspace_id + AND wwo.user_id = wua.user_id + ) + OR ( + NOT EXISTS ( + SELECT 1 + FROM workflow_workspace_owners wwo + WHERE wwo.workspace_id = c.workspace_id + ) + AND w.owner_id = wua.user_id + ) + ) THEN 'admin'::"credential_member_role" + ELSE 'member'::"credential_member_role" + END AS role, + 'active'::"credential_member_status", + now(), + c.created_by, + now(), + now() +FROM "credential" c +INNER JOIN "workspace" w + ON w.id = c.workspace_id +INNER JOIN workspace_user_access wua + ON wua.workspace_id = c.workspace_id +WHERE c.type IN ('oauth'::"credential_type", 'env_workspace'::"credential_type") +ON CONFLICT DO NOTHING; +--> statement-breakpoint +INSERT INTO "credential_member" ( + "id", + "credential_id", + "user_id", + "role", + "status", + "joined_at", + "invited_by", + "created_at", + "updated_at" +) +SELECT + 'credm_' || md5(c.id || ':' || c.env_owner_user_id) AS id, + c.id, + c.env_owner_user_id, + 'admin'::"credential_member_role", + 'active'::"credential_member_status", + now(), + c.created_by, + now(), + now() +FROM "credential" c +WHERE c.type = 'env_personal'::"credential_type" + AND c.env_owner_user_id IS NOT NULL +ON CONFLICT DO NOTHING; \ No newline at end of file diff --git a/packages/db/migrations/meta/0154_snapshot.json b/packages/db/migrations/meta/0154_snapshot.json new file mode 100644 index 000000000..d55f019e9 --- /dev/null +++ b/packages/db/migrations/meta/0154_snapshot.json @@ -0,0 +1,11109 @@ +{ + "id": "973c7c07-ca94-4573-bfe4-9d551c367b4a", + "prevId": "2652353e-bc06-43fe-a8c6-4d03fe4dac93", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.a2a_agent": { + "name": "a2a_agent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'1.0.0'" + }, + "capabilities": { + "name": "capabilities", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "skills": { + "name": "skills", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "authentication": { + "name": "authentication", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "signatures": { + "name": "signatures", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "is_published": { + "name": "is_published", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "published_at": { + "name": "published_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": { + "a2a_agent_workspace_id_idx": { + "name": "a2a_agent_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_workflow_id_idx": { + "name": "a2a_agent_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_created_by_idx": { + "name": "a2a_agent_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_workspace_workflow_unique": { + "name": "a2a_agent_workspace_workflow_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "a2a_agent_workspace_id_workspace_id_fk": { + "name": "a2a_agent_workspace_id_workspace_id_fk", + "tableFrom": "a2a_agent", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "a2a_agent_workflow_id_workflow_id_fk": { + "name": "a2a_agent_workflow_id_workflow_id_fk", + "tableFrom": "a2a_agent", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "a2a_agent_created_by_user_id_fk": { + "name": "a2a_agent_created_by_user_id_fk", + "tableFrom": "a2a_agent", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.a2a_push_notification_config": { + "name": "a2a_push_notification_config", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_schemes": { + "name": "auth_schemes", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "auth_credentials": { + "name": "auth_credentials", + "type": "text", + "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": { + "a2a_push_notification_config_task_id_idx": { + "name": "a2a_push_notification_config_task_id_idx", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_push_notification_config_task_unique": { + "name": "a2a_push_notification_config_task_unique", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "a2a_push_notification_config_task_id_a2a_task_id_fk": { + "name": "a2a_push_notification_config_task_id_a2a_task_id_fk", + "tableFrom": "a2a_push_notification_config", + "tableTo": "a2a_task", + "columnsFrom": ["task_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.a2a_task": { + "name": "a2a_task", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "a2a_task_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'submitted'" + }, + "messages": { + "name": "messages", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "artifacts": { + "name": "artifacts", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "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()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "a2a_task_agent_id_idx": { + "name": "a2a_task_agent_id_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_session_id_idx": { + "name": "a2a_task_session_id_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_status_idx": { + "name": "a2a_task_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_execution_id_idx": { + "name": "a2a_task_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_created_at_idx": { + "name": "a2a_task_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "a2a_task_agent_id_a2a_agent_id_fk": { + "name": "a2a_task_agent_id_a2a_agent_id_fk", + "tableFrom": "a2a_task", + "tableTo": "a2a_agent", + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "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": {} + }, + "idx_account_on_account_id_provider_id": { + "name": "idx_account_on_account_id_provider_id", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_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": { + "api_key_workspace_type_idx": { + "name": "api_key_workspace_type_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "api_key_user_type_idx": { + "name": "api_key_user_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "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.async_jobs": { + "name": "async_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "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()" + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "run_at": { + "name": "run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3 + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "output": { + "name": "output", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "async_jobs_status_started_at_idx": { + "name": "async_jobs_status_started_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "async_jobs_status_completed_at_idx": { + "name": "async_jobs_status_completed_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "completed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "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 + }, + "identifier": { + "name": "identifier", + "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": { + "identifier_idx": { + "name": "identifier_idx", + "columns": [ + { + "expression": "identifier", + "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 + }, + "plan_artifact": { + "name": "plan_artifact", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "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.credential": { + "name": "credential", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "credential_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env_key": { + "name": "env_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env_owner_user_id": { + "name": "env_owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "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": { + "credential_workspace_id_idx": { + "name": "credential_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_type_idx": { + "name": "credential_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_provider_id_idx": { + "name": "credential_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_account_id_idx": { + "name": "credential_account_id_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_env_owner_user_id_idx": { + "name": "credential_env_owner_user_id_idx", + "columns": [ + { + "expression": "env_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_account_unique": { + "name": "credential_workspace_account_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "account_id IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_env_unique": { + "name": "credential_workspace_env_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "type = 'env_workspace'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_personal_env_unique": { + "name": "credential_workspace_personal_env_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "type = 'env_personal'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_workspace_id_workspace_id_fk": { + "name": "credential_workspace_id_workspace_id_fk", + "tableFrom": "credential", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_account_id_account_id_fk": { + "name": "credential_account_id_account_id_fk", + "tableFrom": "credential", + "tableTo": "account", + "columnsFrom": ["account_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_env_owner_user_id_user_id_fk": { + "name": "credential_env_owner_user_id_user_id_fk", + "tableFrom": "credential", + "tableTo": "user", + "columnsFrom": ["env_owner_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_created_by_user_id_fk": { + "name": "credential_created_by_user_id_fk", + "tableFrom": "credential", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "credential_oauth_source_check": { + "name": "credential_oauth_source_check", + "value": "(type <> 'oauth') OR (account_id IS NOT NULL AND provider_id IS NOT NULL)" + }, + "credential_workspace_env_source_check": { + "name": "credential_workspace_env_source_check", + "value": "(type <> 'env_workspace') OR (env_key IS NOT NULL AND env_owner_user_id IS NULL)" + }, + "credential_personal_env_source_check": { + "name": "credential_personal_env_source_check", + "value": "(type <> 'env_personal') OR (env_key IS NOT NULL AND env_owner_user_id IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.credential_member": { + "name": "credential_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "credential_member_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "status": { + "name": "status", + "type": "credential_member_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "invited_by": { + "name": "invited_by", + "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": { + "credential_member_credential_id_idx": { + "name": "credential_member_credential_id_idx", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_user_id_idx": { + "name": "credential_member_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_role_idx": { + "name": "credential_member_role_idx", + "columns": [ + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_status_idx": { + "name": "credential_member_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_unique": { + "name": "credential_member_unique", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_member_credential_id_credential_id_fk": { + "name": "credential_member_credential_id_credential_id_fk", + "tableFrom": "credential_member", + "tableTo": "credential", + "columnsFrom": ["credential_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_member_user_id_user_id_fk": { + "name": "credential_member_user_id_user_id_fk", + "tableFrom": "credential_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_member_invited_by_user_id_fk": { + "name": "credential_member_invited_by_user_id_fk", + "tableFrom": "credential_member", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_set": { + "name": "credential_set", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_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 + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "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": { + "credential_set_organization_id_idx": { + "name": "credential_set_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_created_by_idx": { + "name": "credential_set_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_org_name_unique": { + "name": "credential_set_org_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_provider_id_idx": { + "name": "credential_set_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_set_organization_id_organization_id_fk": { + "name": "credential_set_organization_id_organization_id_fk", + "tableFrom": "credential_set", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_created_by_user_id_fk": { + "name": "credential_set_created_by_user_id_fk", + "tableFrom": "credential_set", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_set_invitation": { + "name": "credential_set_invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_set_id": { + "name": "credential_set_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "credential_set_invitation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "accepted_by_user_id": { + "name": "accepted_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_set_invitation_set_id_idx": { + "name": "credential_set_invitation_set_id_idx", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_invitation_token_idx": { + "name": "credential_set_invitation_token_idx", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_invitation_status_idx": { + "name": "credential_set_invitation_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_invitation_expires_at_idx": { + "name": "credential_set_invitation_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_set_invitation_credential_set_id_credential_set_id_fk": { + "name": "credential_set_invitation_credential_set_id_credential_set_id_fk", + "tableFrom": "credential_set_invitation", + "tableTo": "credential_set", + "columnsFrom": ["credential_set_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_invitation_invited_by_user_id_fk": { + "name": "credential_set_invitation_invited_by_user_id_fk", + "tableFrom": "credential_set_invitation", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_invitation_accepted_by_user_id_user_id_fk": { + "name": "credential_set_invitation_accepted_by_user_id_user_id_fk", + "tableFrom": "credential_set_invitation", + "tableTo": "user", + "columnsFrom": ["accepted_by_user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "credential_set_invitation_token_unique": { + "name": "credential_set_invitation_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_set_member": { + "name": "credential_set_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_set_id": { + "name": "credential_set_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "credential_set_member_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "invited_by": { + "name": "invited_by", + "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": { + "credential_set_member_set_id_idx": { + "name": "credential_set_member_set_id_idx", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_member_user_id_idx": { + "name": "credential_set_member_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_member_unique": { + "name": "credential_set_member_unique", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_member_status_idx": { + "name": "credential_set_member_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_set_member_credential_set_id_credential_set_id_fk": { + "name": "credential_set_member_credential_set_id_credential_set_id_fk", + "tableFrom": "credential_set_member", + "tableTo": "credential_set", + "columnsFrom": ["credential_set_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_member_user_id_user_id_fk": { + "name": "credential_set_member_user_id_user_id_fk", + "tableFrom": "credential_set_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_member_invited_by_user_id_fk": { + "name": "credential_set_member_invited_by_user_id_fk", + "tableFrom": "credential_set_member", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "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 + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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": { + "custom_tools_workspace_id_idx": { + "name": "custom_tools_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_tools_workspace_title_unique": { + "name": "custom_tools_workspace_title_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "title", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "custom_tools_workspace_id_workspace_id_fk": { + "name": "custom_tools_workspace_id_workspace_id_fk", + "tableFrom": "custom_tools", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "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": "set null", + "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 + }, + "number1": { + "name": "number1", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number2": { + "name": "number2", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number3": { + "name": "number3", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number4": { + "name": "number4", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number5": { + "name": "number5", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "date1": { + "name": "date1", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "date2": { + "name": "date2", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "boolean1": { + "name": "boolean1", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean2": { + "name": "boolean2", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean3": { + "name": "boolean3", + "type": "boolean", + "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_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": {} + }, + "doc_number1_idx": { + "name": "doc_number1_idx", + "columns": [ + { + "expression": "number1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number2_idx": { + "name": "doc_number2_idx", + "columns": [ + { + "expression": "number2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number3_idx": { + "name": "doc_number3_idx", + "columns": [ + { + "expression": "number3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number4_idx": { + "name": "doc_number4_idx", + "columns": [ + { + "expression": "number4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number5_idx": { + "name": "doc_number5_idx", + "columns": [ + { + "expression": "number5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_date1_idx": { + "name": "doc_date1_idx", + "columns": [ + { + "expression": "date1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_date2_idx": { + "name": "doc_date2_idx", + "columns": [ + { + "expression": "date2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean1_idx": { + "name": "doc_boolean1_idx", + "columns": [ + { + "expression": "boolean1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean2_idx": { + "name": "doc_boolean2_idx", + "columns": [ + { + "expression": "boolean2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean3_idx": { + "name": "doc_boolean3_idx", + "columns": [ + { + "expression": "boolean3", + "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 + }, + "number1": { + "name": "number1", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number2": { + "name": "number2", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number3": { + "name": "number3", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number4": { + "name": "number4", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number5": { + "name": "number5", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "date1": { + "name": "date1", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "date2": { + "name": "date2", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "boolean1": { + "name": "boolean1", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean2": { + "name": "boolean2", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean3": { + "name": "boolean3", + "type": "boolean", + "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_number1_idx": { + "name": "emb_number1_idx", + "columns": [ + { + "expression": "number1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number2_idx": { + "name": "emb_number2_idx", + "columns": [ + { + "expression": "number2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number3_idx": { + "name": "emb_number3_idx", + "columns": [ + { + "expression": "number3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number4_idx": { + "name": "emb_number4_idx", + "columns": [ + { + "expression": "number4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number5_idx": { + "name": "emb_number5_idx", + "columns": [ + { + "expression": "number5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_date1_idx": { + "name": "emb_date1_idx", + "columns": [ + { + "expression": "date1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_date2_idx": { + "name": "emb_date2_idx", + "columns": [ + { + "expression": "date2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean1_idx": { + "name": "emb_boolean1_idx", + "columns": [ + { + "expression": "boolean1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean2_idx": { + "name": "emb_boolean2_idx", + "columns": [ + { + "expression": "boolean2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean3_idx": { + "name": "emb_boolean3_idx", + "columns": [ + { + "expression": "boolean3", + "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.form": { + "name": "form", + "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 + }, + "identifier": { + "name": "identifier", + "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": "'[]'" + }, + "show_branding": { + "name": "show_branding", + "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": { + "form_identifier_idx": { + "name": "form_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "form_workflow_id_idx": { + "name": "form_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "form_user_id_idx": { + "name": "form_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "form_workflow_id_workflow_id_fk": { + "name": "form_workflow_id_workflow_id_fk", + "tableFrom": "form", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "form_user_id_user_id_fk": { + "name": "form_user_id_user_id_fk", + "tableFrom": "form", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.idempotency_key": { + "name": "idempotency_key", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "result": { + "name": "result", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idempotency_key_created_at_idx": { + "name": "idempotency_key_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "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.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 + }, + "status_config": { + "name": "status_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "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_unique": { + "name": "member_user_id_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "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 + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "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()" + }, + "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_workspace_idx": { + "name": "memory_workspace_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_key_idx": { + "name": "memory_workspace_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "memory_workspace_id_workspace_id_fk": { + "name": "memory_workspace_id_workspace_id_fk", + "tableFrom": "memory", + "tableTo": "workspace", + "columnsFrom": ["workspace_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 + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "departed_member_usage": { + "name": "departed_member_usage", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "credit_balance": { + "name": "credit_balance", + "type": "numeric", + "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": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.paused_executions": { + "name": "paused_executions", + "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 + }, + "execution_snapshot": { + "name": "execution_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "pause_points": { + "name": "pause_points", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "total_pause_count": { + "name": "total_pause_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "resumed_count": { + "name": "resumed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'paused'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "paused_at": { + "name": "paused_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": { + "paused_executions_workflow_id_idx": { + "name": "paused_executions_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_status_idx": { + "name": "paused_executions_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_execution_id_unique": { + "name": "paused_executions_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "paused_executions_workflow_id_workflow_id_fk": { + "name": "paused_executions_workflow_id_workflow_id_fk", + "tableFrom": "paused_executions", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_group": { + "name": "permission_group", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_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 + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_by": { + "name": "created_by", + "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()" + }, + "auto_add_new_members": { + "name": "auto_add_new_members", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "permission_group_organization_id_idx": { + "name": "permission_group_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_created_by_idx": { + "name": "permission_group_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_org_name_unique": { + "name": "permission_group_org_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_org_auto_add_unique": { + "name": "permission_group_org_auto_add_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "auto_add_new_members = true", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_organization_id_organization_id_fk": { + "name": "permission_group_organization_id_organization_id_fk", + "tableFrom": "permission_group", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_created_by_user_id_fk": { + "name": "permission_group_created_by_user_id_fk", + "tableFrom": "permission_group", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_group_member": { + "name": "permission_group_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "permission_group_id": { + "name": "permission_group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "assigned_by": { + "name": "assigned_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assigned_at": { + "name": "assigned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permission_group_member_group_id_idx": { + "name": "permission_group_member_group_id_idx", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_member_user_id_unique": { + "name": "permission_group_member_user_id_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_member_permission_group_id_permission_group_id_fk": { + "name": "permission_group_member_permission_group_id_permission_group_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "permission_group", + "columnsFrom": ["permission_group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_user_id_user_id_fk": { + "name": "permission_group_member_user_id_user_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_assigned_by_user_id_fk": { + "name": "permission_group_member_assigned_by_user_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "user", + "columnsFrom": ["assigned_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "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.rate_limit_bucket": { + "name": "rate_limit_bucket", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tokens": { + "name": "tokens", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resume_queue": { + "name": "resume_queue", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "paused_execution_id": { + "name": "paused_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_execution_id": { + "name": "parent_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "new_execution_id": { + "name": "new_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "context_id": { + "name": "context_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resume_input": { + "name": "resume_input", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "queued_at": { + "name": "queued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "resume_queue_parent_status_idx": { + "name": "resume_queue_parent_status_idx", + "columns": [ + { + "expression": "parent_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "resume_queue_new_execution_idx": { + "name": "resume_queue_new_execution_idx", + "columns": [ + { + "expression": "new_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "resume_queue_paused_execution_id_paused_executions_id_fk": { + "name": "resume_queue_paused_execution_id_paused_executions_id_fk", + "tableFrom": "resume_queue", + "tableTo": "paused_executions", + "columnsFrom": ["paused_execution_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": "'dark'" + }, + "auto_connect": { + "name": "auto_connect", + "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 + }, + "show_training_controls": { + "name": "show_training_controls", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "super_user_mode_enabled": { + "name": "super_user_mode_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "error_notifications_enabled": { + "name": "error_notifications_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "snap_to_grid_size": { + "name": "snap_to_grid_size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "show_action_bar": { + "name": "show_action_bar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "copilot_enabled_models": { + "name": "copilot_enabled_models", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "copilot_auto_allowed_tools": { + "name": "copilot_auto_allowed_tools", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "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.skill": { + "name": "skill", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "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": { + "skill_workspace_id_idx": { + "name": "skill_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "skill_workspace_name_unique": { + "name": "skill_workspace_name_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "skill_workspace_id_workspace_id_fk": { + "name": "skill_workspace_id_workspace_id_fk", + "tableFrom": "skill", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "skill_user_id_user_id_fk": { + "name": "skill_user_id_user_id_fk", + "tableFrom": "skill", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sso_provider": { + "name": "sso_provider", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oidc_config": { + "name": "oidc_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "saml_config": { + "name": "saml_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "sso_provider_provider_id_idx": { + "name": "sso_provider_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_domain_idx": { + "name": "sso_provider_domain_idx", + "columns": [ + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_user_id_idx": { + "name": "sso_provider_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_organization_id_idx": { + "name": "sso_provider_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sso_provider_user_id_user_id_fk": { + "name": "sso_provider_user_id_user_id_fk", + "tableFrom": "sso_provider", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sso_provider_organization_id_organization_id_fk": { + "name": "sso_provider_organization_id_organization_id_fk", + "tableFrom": "sso_provider", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "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_creators": { + "name": "template_creators", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "reference_type": { + "name": "reference_type", + "type": "template_creator_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "profile_image_url": { + "name": "profile_image_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "verified": { + "name": "verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_by": { + "name": "created_by", + "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": { + "template_creators_reference_idx": { + "name": "template_creators_reference_idx", + "columns": [ + { + "expression": "reference_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_creators_reference_id_idx": { + "name": "template_creators_reference_id_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_creators_created_by_idx": { + "name": "template_creators_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "template_creators_created_by_user_id_fk": { + "name": "template_creators_created_by_user_id_fk", + "tableFrom": "template_creators", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "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 + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "creator_id": { + "name": "creator_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "views": { + "name": "views", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "stars": { + "name": "stars", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "template_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "tags": { + "name": "tags", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "required_credentials": { + "name": "required_credentials", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "state": { + "name": "state", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "og_image_url": { + "name": "og_image_url", + "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": { + "templates_status_idx": { + "name": "templates_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_creator_id_idx": { + "name": "templates_creator_id_idx", + "columns": [ + { + "expression": "creator_id", + "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_status_views_idx": { + "name": "templates_status_views_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "views", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_status_stars_idx": { + "name": "templates_status_stars_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stars", + "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": "set null", + "onUpdate": "no action" + }, + "templates_creator_id_template_creators_id_fk": { + "name": "templates_creator_id_template_creators_id_fk", + "tableFrom": "templates", + "tableTo": "template_creators", + "columnsFrom": ["creator_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.usage_log": { + "name": "usage_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "usage_log_category", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "usage_log_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "cost": { + "name": "cost", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "usage_log_user_created_at_idx": { + "name": "usage_log_user_created_at_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_source_idx": { + "name": "usage_log_source_idx", + "columns": [ + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workspace_id_idx": { + "name": "usage_log_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workflow_id_idx": { + "name": "usage_log_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "usage_log_user_id_user_id_fk": { + "name": "usage_log_user_id_user_id_fk", + "tableFrom": "usage_log", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "usage_log_workspace_id_workspace_id_fk": { + "name": "usage_log_workspace_id_workspace_id_fk", + "tableFrom": "usage_log", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "usage_log_workflow_id_workflow_id_fk": { + "name": "usage_log_workflow_id_workflow_id_fk", + "tableFrom": "usage_log", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "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 + }, + "is_super_user": { + "name": "is_super_user", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "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_mcp_executions": { + "name": "total_mcp_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_a2a_executions": { + "name": "total_a2a_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": "'20'" + }, + "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'" + }, + "billed_overage_this_period": { + "name": "billed_overage_this_period", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "pro_period_cost_snapshot": { + "name": "pro_period_cost_snapshot", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "credit_balance": { + "name": "credit_balance", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "total_copilot_cost": { + "name": "total_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_period_copilot_cost": { + "name": "current_period_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "last_period_copilot_cost": { + "name": "last_period_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "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 + }, + "total_mcp_copilot_calls": { + "name": "total_mcp_copilot_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_mcp_copilot_cost": { + "name": "total_mcp_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_period_mcp_copilot_cost": { + "name": "current_period_mcp_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "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 + }, + "billing_blocked_reason": { + "name": "billing_blocked_reason", + "type": "billing_blocked_reason", + "typeSchema": "public", + "primaryKey": false, + "notNull": 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": {} + }, + "verification_expires_at_idx": { + "name": "verification_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "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 + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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 + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_failed_at": { + "name": "last_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "credential_set_id": { + "name": "credential_set_id", + "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": { + "path_deployment_unique": { + "name": "path_deployment_unique", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_webhook_on_workflow_id_block_id": { + "name": "idx_webhook_on_workflow_id_block_id", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_workflow_deployment_idx": { + "name": "webhook_workflow_deployment_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_credential_set_id_idx": { + "name": "webhook_credential_set_id_idx", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "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_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "webhook_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webhook_credential_set_id_credential_set_id_fk": { + "name": "webhook_credential_set_id_credential_set_id_fk", + "tableFrom": "webhook", + "tableTo": "credential_set", + "columnsFrom": ["credential_set_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "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 + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "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_at": { + "name": "deployed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "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": "'{}'" + } + }, + "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": {} + }, + "workflow_folder_sort_idx": { + "name": "workflow_folder_sort_idx", + "columns": [ + { + "expression": "folder_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_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" + } + }, + "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 + }, + "locked": { + "name": "locked", + "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": "'{}'" + }, + "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_type_idx": { + "name": "workflow_blocks_type_idx", + "columns": [ + { + "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 + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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_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_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": false + }, + "workspace_id": { + "name": "workspace_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 + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "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_state_snapshot_id_idx": { + "name": "workflow_execution_logs_state_snapshot_id_idx", + "columns": [ + { + "expression": "state_snapshot_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_deployment_version_id_idx": { + "name": "workflow_execution_logs_deployment_version_id_idx", + "columns": [ + { + "expression": "deployment_version_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": {} + }, + "workflow_execution_logs_workspace_started_at_idx": { + "name": "workflow_execution_logs_workspace_started_at_idx", + "columns": [ + { + "expression": "workspace_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": "set null", + "onUpdate": "no action" + }, + "workflow_execution_logs_workspace_id_workspace_id_fk": { + "name": "workflow_execution_logs_workspace_id_workspace_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workspace", + "columnsFrom": ["workspace_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" + }, + "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "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": false + }, + "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": "set null", + "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_mcp_server": { + "name": "workflow_mcp_server", + "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": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_public": { + "name": "is_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": 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_mcp_server_workspace_id_idx": { + "name": "workflow_mcp_server_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_created_by_idx": { + "name": "workflow_mcp_server_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_mcp_server_workspace_id_workspace_id_fk": { + "name": "workflow_mcp_server_workspace_id_workspace_id_fk", + "tableFrom": "workflow_mcp_server", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_mcp_server_created_by_user_id_fk": { + "name": "workflow_mcp_server_created_by_user_id_fk", + "tableFrom": "workflow_mcp_server", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_mcp_tool": { + "name": "workflow_mcp_tool", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_description": { + "name": "tool_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parameter_schema": { + "name": "parameter_schema", + "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": { + "workflow_mcp_tool_server_id_idx": { + "name": "workflow_mcp_tool_server_id_idx", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_workflow_id_idx": { + "name": "workflow_mcp_tool_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_server_workflow_unique": { + "name": "workflow_mcp_tool_server_workflow_unique", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_mcp_tool_server_id_workflow_mcp_server_id_fk": { + "name": "workflow_mcp_tool_server_id_workflow_mcp_server_id_fk", + "tableFrom": "workflow_mcp_tool", + "tableTo": "workflow_mcp_server", + "columnsFrom": ["server_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_mcp_tool_workflow_id_workflow_id_fk": { + "name": "workflow_mcp_tool_workflow_id_workflow_id_fk", + "tableFrom": "workflow_mcp_tool", + "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 + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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 + }, + "last_queued_at": { + "name": "last_queued_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_deployment_unique": { + "name": "workflow_schedule_workflow_block_deployment_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_schedule_workflow_deployment_idx": { + "name": "workflow_schedule_workflow_deployment_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "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_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_schedule_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_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 + }, + "billed_account_user_id": { + "name": "billed_account_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "allow_personal_api_keys": { + "name": "allow_personal_api_keys", + "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": {}, + "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" + }, + "workspace_billed_account_user_id_user_id_fk": { + "name": "workspace_billed_account_user_id_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": ["billed_account_user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_byok_keys": { + "name": "workspace_byok_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "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": { + "workspace_byok_provider_unique": { + "name": "workspace_byok_provider_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_byok_workspace_idx": { + "name": "workspace_byok_workspace_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_byok_keys_workspace_id_workspace_id_fk": { + "name": "workspace_byok_keys_workspace_id_workspace_id_fk", + "tableFrom": "workspace_byok_keys", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_byok_keys_created_by_user_id_fk": { + "name": "workspace_byok_keys_created_by_user_id_fk", + "tableFrom": "workspace_byok_keys", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "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_file": { + "name": "workspace_file", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_file_workspace_id_idx": { + "name": "workspace_file_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_key_idx": { + "name": "workspace_file_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_file_workspace_id_workspace_id_fk": { + "name": "workspace_file_workspace_id_workspace_id_fk", + "tableFrom": "workspace_file", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_uploaded_by_user_id_fk": { + "name": "workspace_file_uploaded_by_user_id_fk", + "tableFrom": "workspace_file", + "tableTo": "user", + "columnsFrom": ["uploaded_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_file_key_unique": { + "name": "workspace_file_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_files": { + "name": "workspace_files", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "key": { + "name": "key", + "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": false + }, + "context": { + "name": "context", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "original_name": { + "name": "original_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_files_key_idx": { + "name": "workspace_files_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_user_id_idx": { + "name": "workspace_files_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_id_idx": { + "name": "workspace_files_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_context_idx": { + "name": "workspace_files_context_idx", + "columns": [ + { + "expression": "context", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_files_user_id_user_id_fk": { + "name": "workspace_files_user_id_user_id_fk", + "tableFrom": "workspace_files", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_files_workspace_id_workspace_id_fk": { + "name": "workspace_files_workspace_id_workspace_id_fk", + "tableFrom": "workspace_files", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_files_key_unique": { + "name": "workspace_files_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "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 + }, + "public.workspace_notification_delivery": { + "name": "workspace_notification_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": "notification_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": { + "workspace_notification_delivery_subscription_id_idx": { + "name": "workspace_notification_delivery_subscription_id_idx", + "columns": [ + { + "expression": "subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_notification_delivery_execution_id_idx": { + "name": "workspace_notification_delivery_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_notification_delivery_status_idx": { + "name": "workspace_notification_delivery_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_notification_delivery_next_attempt_idx": { + "name": "workspace_notification_delivery_next_attempt_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_notification_delivery_subscription_id_workspace_notification_subscription_id_fk": { + "name": "workspace_notification_delivery_subscription_id_workspace_notification_subscription_id_fk", + "tableFrom": "workspace_notification_delivery", + "tableTo": "workspace_notification_subscription", + "columnsFrom": ["subscription_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_notification_delivery_workflow_id_workflow_id_fk": { + "name": "workspace_notification_delivery_workflow_id_workflow_id_fk", + "tableFrom": "workspace_notification_delivery", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_notification_subscription": { + "name": "workspace_notification_subscription", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "notification_type": { + "name": "notification_type", + "type": "notification_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "workflow_ids": { + "name": "workflow_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "all_workflows": { + "name": "all_workflows", + "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[]" + }, + "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 + }, + "webhook_config": { + "name": "webhook_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "email_recipients": { + "name": "email_recipients", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "slack_config": { + "name": "slack_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "alert_config": { + "name": "alert_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "last_alert_at": { + "name": "last_alert_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_by": { + "name": "created_by", + "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": { + "workspace_notification_workspace_id_idx": { + "name": "workspace_notification_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_notification_active_idx": { + "name": "workspace_notification_active_idx", + "columns": [ + { + "expression": "active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_notification_type_idx": { + "name": "workspace_notification_type_idx", + "columns": [ + { + "expression": "notification_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_notification_subscription_workspace_id_workspace_id_fk": { + "name": "workspace_notification_subscription_workspace_id_workspace_id_fk", + "tableFrom": "workspace_notification_subscription", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_notification_subscription_created_by_user_id_fk": { + "name": "workspace_notification_subscription_created_by_user_id_fk", + "tableFrom": "workspace_notification_subscription", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.a2a_task_status": { + "name": "a2a_task_status", + "schema": "public", + "values": [ + "submitted", + "working", + "input-required", + "completed", + "failed", + "canceled", + "rejected", + "auth-required", + "unknown" + ] + }, + "public.billing_blocked_reason": { + "name": "billing_blocked_reason", + "schema": "public", + "values": ["payment_failed", "dispute"] + }, + "public.credential_member_role": { + "name": "credential_member_role", + "schema": "public", + "values": ["admin", "member"] + }, + "public.credential_member_status": { + "name": "credential_member_status", + "schema": "public", + "values": ["active", "pending", "revoked"] + }, + "public.credential_set_invitation_status": { + "name": "credential_set_invitation_status", + "schema": "public", + "values": ["pending", "accepted", "expired", "cancelled"] + }, + "public.credential_set_member_status": { + "name": "credential_set_member_status", + "schema": "public", + "values": ["active", "pending", "revoked"] + }, + "public.credential_type": { + "name": "credential_type", + "schema": "public", + "values": ["oauth", "env_workspace", "env_personal"] + }, + "public.notification_delivery_status": { + "name": "notification_delivery_status", + "schema": "public", + "values": ["pending", "in_progress", "success", "failed"] + }, + "public.notification_type": { + "name": "notification_type", + "schema": "public", + "values": ["webhook", "email", "slack"] + }, + "public.permission_type": { + "name": "permission_type", + "schema": "public", + "values": ["admin", "write", "read"] + }, + "public.template_creator_type": { + "name": "template_creator_type", + "schema": "public", + "values": ["user", "organization"] + }, + "public.template_status": { + "name": "template_status", + "schema": "public", + "values": ["pending", "approved", "rejected"] + }, + "public.usage_log_category": { + "name": "usage_log_category", + "schema": "public", + "values": ["model", "fixed"] + }, + "public.usage_log_source": { + "name": "usage_log_source", + "schema": "public", + "values": ["workflow", "wand", "copilot", "mcp_copilot"] + }, + "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/packages/db/migrations/meta/_journal.json b/packages/db/migrations/meta/_journal.json index 2fa880f2a..bf534832b 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -1072,6 +1072,13 @@ "when": 1770410282842, "tag": "0153_complete_arclight", "breakpoints": true + }, + { + "idx": 154, + "version": "7", + "when": 1770840006821, + "tag": "0154_luxuriant_maria_hill", + "breakpoints": true } ] } diff --git a/packages/db/schema.ts b/packages/db/schema.ts index d145c5796..4d122b7da 100644 --- a/packages/db/schema.ts +++ b/packages/db/schema.ts @@ -89,10 +89,6 @@ export const account = pgTable( table.accountId, table.providerId ), - uniqueUserProvider: uniqueIndex('account_user_provider_unique').on( - table.userId, - table.providerId - ), }) ) @@ -2011,6 +2007,94 @@ export const usageLog = pgTable( }) ) +export const credentialTypeEnum = pgEnum('credential_type', [ + 'oauth', + 'env_workspace', + 'env_personal', +]) + +export const credential = pgTable( + 'credential', + { + id: text('id').primaryKey(), + workspaceId: text('workspace_id') + .notNull() + .references(() => workspace.id, { onDelete: 'cascade' }), + type: credentialTypeEnum('type').notNull(), + displayName: text('display_name').notNull(), + providerId: text('provider_id'), + accountId: text('account_id').references(() => account.id, { onDelete: 'cascade' }), + envKey: text('env_key'), + envOwnerUserId: text('env_owner_user_id').references(() => user.id, { onDelete: 'cascade' }), + createdBy: text('created_by') + .notNull() + .references(() => user.id, { onDelete: 'cascade' }), + createdAt: timestamp('created_at').notNull().defaultNow(), + updatedAt: timestamp('updated_at').notNull().defaultNow(), + }, + (table) => ({ + workspaceIdIdx: index('credential_workspace_id_idx').on(table.workspaceId), + typeIdx: index('credential_type_idx').on(table.type), + providerIdIdx: index('credential_provider_id_idx').on(table.providerId), + accountIdIdx: index('credential_account_id_idx').on(table.accountId), + envOwnerUserIdIdx: index('credential_env_owner_user_id_idx').on(table.envOwnerUserId), + workspaceAccountUnique: uniqueIndex('credential_workspace_account_unique') + .on(table.workspaceId, table.accountId) + .where(sql`account_id IS NOT NULL`), + workspaceEnvUnique: uniqueIndex('credential_workspace_env_unique') + .on(table.workspaceId, table.type, table.envKey) + .where(sql`type = 'env_workspace'`), + workspacePersonalEnvUnique: uniqueIndex('credential_workspace_personal_env_unique') + .on(table.workspaceId, table.type, table.envKey, table.envOwnerUserId) + .where(sql`type = 'env_personal'`), + oauthSourceConstraint: check( + 'credential_oauth_source_check', + sql`(type <> 'oauth') OR (account_id IS NOT NULL AND provider_id IS NOT NULL)` + ), + workspaceEnvSourceConstraint: check( + 'credential_workspace_env_source_check', + sql`(type <> 'env_workspace') OR (env_key IS NOT NULL AND env_owner_user_id IS NULL)` + ), + personalEnvSourceConstraint: check( + 'credential_personal_env_source_check', + sql`(type <> 'env_personal') OR (env_key IS NOT NULL AND env_owner_user_id IS NOT NULL)` + ), + }) +) + +export const credentialMemberRoleEnum = pgEnum('credential_member_role', ['admin', 'member']) +export const credentialMemberStatusEnum = pgEnum('credential_member_status', [ + 'active', + 'pending', + 'revoked', +]) + +export const credentialMember = pgTable( + 'credential_member', + { + id: text('id').primaryKey(), + credentialId: text('credential_id') + .notNull() + .references(() => credential.id, { onDelete: 'cascade' }), + userId: text('user_id') + .notNull() + .references(() => user.id, { onDelete: 'cascade' }), + role: credentialMemberRoleEnum('role').notNull().default('member'), + status: credentialMemberStatusEnum('status').notNull().default('active'), + joinedAt: timestamp('joined_at'), + invitedBy: text('invited_by').references(() => user.id, { onDelete: 'set null' }), + createdAt: timestamp('created_at').notNull().defaultNow(), + updatedAt: timestamp('updated_at').notNull().defaultNow(), + }, + (table) => ({ + credentialIdIdx: index('credential_member_credential_id_idx').on(table.credentialId), + userIdIdx: index('credential_member_user_id_idx').on(table.userId), + roleIdx: index('credential_member_role_idx').on(table.role), + statusIdx: index('credential_member_status_idx').on(table.status), + uniqueMembership: uniqueIndex('credential_member_unique').on(table.credentialId, table.userId), + }) +) + export const credentialSet = pgTable( 'credential_set', {