diff --git a/apps/sim/executor/handlers/credential-group/credential-group-handler.test.ts b/apps/sim/executor/handlers/credential-group/credential-group-handler.test.ts
index a9fb1241d5a..ea28a9e9068 100644
--- a/apps/sim/executor/handlers/credential-group/credential-group-handler.test.ts
+++ b/apps/sim/executor/handlers/credential-group/credential-group-handler.test.ts
@@ -1,339 +1,11 @@
-/**
- * @vitest-environment node
- */
-import type { WorkflowExecutionDelegatedPrincipal } from '@sim/auth/principal'
-import { beforeEach, describe, expect, it, vi } from 'vitest'
-import { BlockType } from '@/executor/constants'
-import type { ExecutionContext } from '@/executor/types'
-import type { SerializedBlock } from '@/serializer/types'
-
-const mocks = vi.hoisted(() => ({
- createPrincipal: vi.fn(),
- createInviteLink: vi.fn(),
- enforceInviteRateLimit: vi.fn(),
- listCredentials: vi.fn(),
- listMcpConnections: vi.fn(),
- listPeople: vi.fn(),
- sendInvite: vi.fn(),
-}))
-
-vi.mock('@/lib/credential-groups/application/create-invite-link', () => ({
- createCredentialGroupInviteLink: { execute: mocks.createInviteLink },
-}))
-
-vi.mock('@/lib/credential-groups/application/list-credentials', () => ({
- listCredentialGroupCredentials: { execute: mocks.listCredentials },
-}))
-
-vi.mock('@/lib/credential-groups/application/list-mcp-connections', () => ({
- listCredentialGroupMcpConnections: { execute: mocks.listMcpConnections },
-}))
-
-vi.mock('@/lib/credential-groups/application/list-people', () => ({
- CREDENTIAL_GROUP_PEOPLE_STATUSES: [
- 'invited',
- 'delivery_failed',
- 'in_progress',
- 'completed',
- 'revoked',
- ],
- listCredentialGroupPeople: { execute: mocks.listPeople },
-}))
-
-vi.mock('@/lib/credential-groups/application/send-invite', () => ({
- sendCredentialGroupInvite: { execute: mocks.sendInvite },
-}))
-
-vi.mock('@/lib/credential-groups/rate-limit', () => ({
- enforceCredentialGroupInvitationExecutionRateLimit: mocks.enforceInviteRateLimit,
-}))
-
-vi.mock('@/lib/internal/principals/executor', () => ({
- createExecutorPrincipalFromExecutionContext: mocks.createPrincipal,
-}))
-
+/** @vitest-environment node */
+import { describe, expect, it } from 'vitest'
import { CredentialGroupBlockHandler } from '@/executor/handlers/credential-group/credential-group-handler'
-const principal: WorkflowExecutionDelegatedPrincipal = {
- kind: 'delegated',
- serviceId: 'executor',
- subjectUserId: 'user-1',
- workspaceId: 'workspace-1',
- delegationId: 'delegation-1',
- audience: 'sim:credential-groups',
- issuedAt: new Date(Date.now() - 1_000),
- expiresAt: new Date(Date.now() + 60_000),
- delegationContext: {
- kind: 'workflow_execution',
- workflowId: 'workflow-1',
- principal: { kind: 'session', userId: 'user-1', sessionId: 'session-1' },
- },
-}
-
-const context = {
- workspaceId: 'workspace-1',
- workflowId: 'workflow-1',
- userId: 'user-1',
- principal: principal.delegationContext.principal,
- executorDelegationOrigin: {
- subjectUserId: 'user-1',
- workflowId: 'workflow-1',
- principal: principal.delegationContext.principal,
- },
-} as ExecutionContext
-
-const block = { metadata: { id: BlockType.CREDENTIAL_GROUP } } as SerializedBlock
-
-describe('CredentialGroupBlockHandler', () => {
- beforeEach(() => {
- vi.clearAllMocks()
- mocks.createPrincipal.mockResolvedValue(principal)
- })
-
- it('recognizes only Credential Group blocks', () => {
- const handler = new CredentialGroupBlockHandler()
-
- expect(handler.canHandle(block)).toBe(true)
- expect(handler.canHandle({ metadata: { id: BlockType.CREDENTIAL } } as SerializedBlock)).toBe(
- false
+describe('legacy Connected Accounts block', () => {
+ it('requires explicit replacement instead of changing credential scope silently', async () => {
+ await expect(new CredentialGroupBlockHandler().execute()).rejects.toThrow(
+ 'Replace this legacy Connected Accounts block with a Credential block'
)
})
-
- it('lists credentials with an optional email selector', async () => {
- mocks.listCredentials.mockResolvedValue({
- credentials: [],
- count: 0,
- hasMore: false,
- nextCursor: null,
- })
-
- const result = await new CredentialGroupBlockHandler().execute(context, block, {
- operation: 'list_credentials',
- workspaceId: 'workspace-forged',
- email: ' person@example.com ',
- credentialProviderIds: '["google-email", "google-email"]',
- limit: '25',
- cursor: ' credential-1 ',
- })
-
- expect(mocks.createPrincipal).toHaveBeenCalledWith({
- context,
- audience: 'sim:credential-groups',
- })
- expect(mocks.listCredentials).toHaveBeenCalledWith({
- principal,
- input: {
- workspaceId: 'workspace-1',
- email: 'person@example.com',
- credentialProviderIds: ['google-email'],
- limit: 25,
- cursor: 'credential-1',
- },
- })
- expect(result).toEqual({ credentials: [], count: 0, hasMore: false, nextCursor: null })
- })
-
- it('lists credentials for an actorless workflow execution', async () => {
- const executionPrincipal = {
- kind: 'system' as const,
- serviceId: 'schedule' as const,
- workspaceId: 'workspace-1',
- workflowId: 'workflow-1',
- }
- const actorlessPrincipal: WorkflowExecutionDelegatedPrincipal = {
- kind: 'delegated',
- serviceId: 'executor',
- workspaceId: 'workspace-1',
- delegationId: 'delegation-actorless',
- audience: 'sim:credential-groups',
- issuedAt: new Date(Date.now() - 1_000),
- expiresAt: new Date(Date.now() + 60_000),
- delegationContext: {
- kind: 'workflow_execution',
- workflowId: 'workflow-1',
- principal: executionPrincipal,
- currentWorkflow: {
- workflowId: 'workflow-1',
- mode: 'deployment',
- deploymentVersionId: 'deployment-version-1',
- },
- },
- }
- const actorlessContext = {
- ...context,
- userId: undefined,
- principal: executionPrincipal,
- executorDelegationOrigin: {
- workflowId: 'workflow-1',
- principal: executionPrincipal,
- currentWorkflow: actorlessPrincipal.delegationContext.currentWorkflow,
- },
- } as ExecutionContext
- mocks.createPrincipal.mockResolvedValueOnce(actorlessPrincipal)
- mocks.listCredentials.mockResolvedValue({
- credentials: [],
- count: 0,
- hasMore: false,
- nextCursor: null,
- })
-
- await new CredentialGroupBlockHandler().execute(actorlessContext, block, {
- operation: 'list_credentials',
- })
-
- expect(mocks.createPrincipal).toHaveBeenCalledWith({
- context: actorlessContext,
- audience: 'sim:credential-groups',
- })
- expect(mocks.listCredentials).toHaveBeenCalledWith({
- principal: actorlessPrincipal,
- input: {
- workspaceId: 'workspace-1',
- limit: 100,
- cursor: undefined,
- email: undefined,
- credentialProviderIds: undefined,
- },
- })
- })
-
- it('lists explicit MCP connection references for an advanced MCP tool', async () => {
- mocks.listMcpConnections.mockResolvedValue({
- mcpConnections: [],
- count: 0,
- hasMore: false,
- nextCursor: null,
- })
-
- const result = await new CredentialGroupBlockHandler().execute(context, block, {
- operation: 'list_mcp_connections',
- email: ' person@example.com ',
- mcpServerId: ' mcp-server-1 ',
- limit: '25',
- cursor: ' mcp-cg-connection-1 ',
- })
-
- expect(mocks.listMcpConnections).toHaveBeenCalledWith({
- principal,
- input: {
- workspaceId: 'workspace-1',
- email: 'person@example.com',
- mcpServerId: 'mcp-server-1',
- limit: 25,
- cursor: 'mcp-cg-connection-1',
- },
- })
- expect(result).toEqual({ mcpConnections: [], count: 0, hasMore: false, nextCursor: null })
- })
-
- it('rejects removed group discovery before delegation', async () => {
- await expect(
- new CredentialGroupBlockHandler().execute(context, block, { operation: 'list_groups' })
- ).rejects.toThrow('Unsupported Credential Group operation: list_groups')
- expect(mocks.createPrincipal).not.toHaveBeenCalled()
- })
-
- it('applies the shared workspace invitation budget before sending', async () => {
- mocks.sendInvite.mockResolvedValue({
- enrollment: {
- id: 'enrollment-1',
- email: 'person@example.com',
- status: 'invited',
- invitedAt: '2026-08-13T12:00:00.000Z',
- expiresAt: '2026-08-20T12:00:00.000Z',
- },
- })
-
- await new CredentialGroupBlockHandler().execute(context, block, {
- operation: 'send_invite',
- email: ' person@example.com ',
- })
-
- expect(mocks.enforceInviteRateLimit).toHaveBeenCalledWith('workspace-1')
- expect(mocks.enforceInviteRateLimit.mock.invocationCallOrder[0]).toBeLessThan(
- mocks.sendInvite.mock.invocationCallOrder[0]!
- )
- expect(mocks.sendInvite).toHaveBeenCalledWith({
- principal,
- input: { workspaceId: 'workspace-1', email: 'person@example.com' },
- })
- })
-
- it('issues a fresh invitation link without routing through email delivery', async () => {
- mocks.createInviteLink.mockResolvedValue({
- enrollment: {
- id: 'enrollment-1',
- email: 'person@example.com',
- status: 'invited',
- invitedAt: '2026-08-13T12:00:00.000Z',
- expiresAt: '2026-08-20T12:00:00.000Z',
- },
- invitationLink: 'https://sim.ai/credential-groups/enroll/token-1',
- })
-
- const result = await new CredentialGroupBlockHandler().execute(context, block, {
- operation: 'get_invite_link',
- email: ' person@example.com ',
- })
-
- expect(mocks.createPrincipal).toHaveBeenCalledWith({
- context,
- audience: 'sim:credential-groups',
- })
- expect(mocks.enforceInviteRateLimit).toHaveBeenCalledWith('workspace-1')
- expect(mocks.enforceInviteRateLimit.mock.invocationCallOrder[0]).toBeLessThan(
- mocks.createInviteLink.mock.invocationCallOrder[0]!
- )
- expect(mocks.createInviteLink).toHaveBeenCalledWith({
- principal,
- input: { workspaceId: 'workspace-1', email: 'person@example.com' },
- })
- expect(mocks.sendInvite).not.toHaveBeenCalled()
- expect(result).toEqual({
- enrollmentId: 'enrollment-1',
- email: 'person@example.com',
- status: 'invited',
- invitedAt: '2026-08-13T12:00:00.000Z',
- expiresAt: '2026-08-20T12:00:00.000Z',
- invitationLink: 'https://sim.ai/credential-groups/enroll/token-1',
- })
- })
-
- it('fails fast on unsupported people statuses', async () => {
- await expect(
- new CredentialGroupBlockHandler().execute(context, block, {
- operation: 'list_people',
- peopleStatuses: ['unknown'],
- })
- ).rejects.toThrow('People statuses contain an unsupported value')
- expect(mocks.listPeople).not.toHaveBeenCalled()
- })
-
- it('lists people from the execution workspace without a group input', async () => {
- const page = { people: [], count: 0, hasMore: false, nextCursor: null }
- mocks.listPeople.mockResolvedValue(page)
- await expect(
- new CredentialGroupBlockHandler().execute(context, block, {
- operation: 'list_people',
- peopleStatuses: ['completed'],
- })
- ).resolves.toEqual(page)
- expect(mocks.listPeople).toHaveBeenCalledWith({
- principal,
- input: {
- workspaceId: 'workspace-1',
- limit: 100,
- cursor: undefined,
- email: undefined,
- statuses: ['completed'],
- },
- })
- })
-
- it('rejects unsupported operations before delegation', async () => {
- await expect(
- new CredentialGroupBlockHandler().execute(context, block, { operation: 'unknown' })
- ).rejects.toThrow('Unsupported Credential Group operation: unknown')
- expect(mocks.createPrincipal).not.toHaveBeenCalled()
- })
})
diff --git a/apps/sim/executor/handlers/credential-group/credential-group-handler.ts b/apps/sim/executor/handlers/credential-group/credential-group-handler.ts
index aef1e1d1af0..9855e1ae59e 100644
--- a/apps/sim/executor/handlers/credential-group/credential-group-handler.ts
+++ b/apps/sim/executor/handlers/credential-group/credential-group-handler.ts
@@ -1,216 +1,16 @@
-import { createLogger } from '@sim/logger'
-import { CREDENTIAL_GROUP_DELEGATION_AUDIENCE } from '@/lib/credential-groups/application/authorization'
-import { createCredentialGroupInviteLink } from '@/lib/credential-groups/application/create-invite-link'
-import { listCredentialGroupCredentials } from '@/lib/credential-groups/application/list-credentials'
-import { listCredentialGroupMcpConnections } from '@/lib/credential-groups/application/list-mcp-connections'
-import {
- CREDENTIAL_GROUP_PEOPLE_STATUSES,
- listCredentialGroupPeople,
-} from '@/lib/credential-groups/application/list-people'
-import { sendCredentialGroupInvite } from '@/lib/credential-groups/application/send-invite'
-import { MAX_CREDENTIAL_GROUP_CREDENTIAL_PAGE_SIZE } from '@/lib/credential-groups/credentials'
-import type { CredentialGroupEnrollmentStatus } from '@/lib/credential-groups/enrollments'
-import { enforceCredentialGroupInvitationExecutionRateLimit } from '@/lib/credential-groups/rate-limit'
-import { createExecutorPrincipalFromExecutionContext } from '@/lib/internal/principals/executor'
import type { BlockOutput } from '@/blocks/types'
import { BlockType } from '@/executor/constants'
-import type { BlockHandler, ExecutionContext } from '@/executor/types'
+import type { BlockHandler } from '@/executor/types'
import type { SerializedBlock } from '@/serializer/types'
-const logger = createLogger('CredentialGroupBlockHandler')
-
-const CREDENTIAL_GROUP_OPERATION_IDS = [
- 'list_credentials',
- 'list_mcp_connections',
- 'send_invite',
- 'get_invite_link',
- 'list_people',
-] as const
-
-type CredentialGroupOperation = (typeof CREDENTIAL_GROUP_OPERATION_IDS)[number]
-
-function parseOperation(value: unknown): CredentialGroupOperation {
- const operation = typeof value === 'string' ? value : 'list_credentials'
- const supported = CREDENTIAL_GROUP_OPERATION_IDS.find((candidate) => candidate === operation)
- if (!supported) throw new Error(`Unsupported Credential Group operation: ${operation}`)
- return supported
-}
-
-function parseStringList(value: unknown, label: string): string[] | undefined {
- if (value === undefined || value === null || value === '') return undefined
-
- let parsed: unknown = value
- if (typeof value === 'string') {
- const trimmed = value.trim()
- if (!trimmed) return undefined
- if (!trimmed.startsWith('[')) return [trimmed]
- try {
- parsed = JSON.parse(trimmed)
- } catch {
- throw new Error(`${label} must be a valid JSON array of strings`)
- }
- }
-
- if (!Array.isArray(parsed) || !parsed.every((item) => typeof item === 'string' && item.trim())) {
- throw new Error(`${label} must be an array of non-empty strings`)
- }
-
- const values = [...new Set(parsed.map((item) => item.trim()))]
- return values.length > 0 ? values : undefined
-}
-
-function parseLimit(value: unknown): number {
- const raw = value ?? MAX_CREDENTIAL_GROUP_CREDENTIAL_PAGE_SIZE
- const limit =
- typeof raw === 'number' ? raw : typeof raw === 'string' && raw.trim() ? Number(raw) : Number.NaN
- if (!Number.isInteger(limit) || limit < 1 || limit > MAX_CREDENTIAL_GROUP_CREDENTIAL_PAGE_SIZE) {
- throw new Error(
- `Limit must be an integer between 1 and ${MAX_CREDENTIAL_GROUP_CREDENTIAL_PAGE_SIZE}`
- )
- }
- return limit
-}
-
-function parseOptionalString(value: unknown, label: string): string | undefined {
- if (value === undefined || value === null || value === '') return undefined
- if (typeof value !== 'string' || !value.trim())
- throw new Error(`${label} must be a non-empty string`)
- return value.trim()
-}
-
-function requireString(value: unknown, label: string): string {
- const parsed = parseOptionalString(value, label)
- if (!parsed) throw new Error(`${label} is required`)
- return parsed
-}
-
+/** Legacy blocks must be explicitly replaced because organization access has different scope. */
export class CredentialGroupBlockHandler implements BlockHandler {
canHandle(block: SerializedBlock): boolean {
return block.metadata?.id === BlockType.CREDENTIAL_GROUP
}
-
- async execute(
- ctx: ExecutionContext,
- _block: SerializedBlock,
- inputs: Record
- ): Promise {
- if (!ctx.workspaceId) throw new Error('workspaceId is required for Credential Group operations')
- const operation = parseOperation(inputs.operation)
- if (!ctx.executorDelegationOrigin) {
- throw new Error('Credential Group operations require an authenticated workflow execution')
- }
- const principal = await createExecutorPrincipalFromExecutionContext({
- context: ctx,
- audience: CREDENTIAL_GROUP_DELEGATION_AUDIENCE,
- })
-
- switch (operation) {
- case 'list_credentials': {
- const credentialProviderIds = parseStringList(
- inputs.credentialProviderIds,
- 'Credential provider IDs'
- )
- const result = await listCredentialGroupCredentials.execute({
- principal,
- input: {
- workspaceId: ctx.workspaceId,
- limit: parseLimit(inputs.limit),
- cursor: parseOptionalString(inputs.cursor, 'Cursor'),
- email: parseOptionalString(inputs.email, 'Email'),
- credentialProviderIds,
- },
- })
- logger.info('Listed Credential Group credentials', {
- workspaceId: ctx.workspaceId,
- count: result.count,
- hasMore: result.hasMore,
- })
- return result
- }
- case 'list_mcp_connections': {
- const result = await listCredentialGroupMcpConnections.execute({
- principal,
- input: {
- workspaceId: ctx.workspaceId,
- limit: parseLimit(inputs.limit),
- cursor: parseOptionalString(inputs.cursor, 'Cursor'),
- email: parseOptionalString(inputs.email, 'Email'),
- mcpServerId: parseOptionalString(inputs.mcpServerId, 'MCP Server ID'),
- },
- })
- logger.info('Listed Credential Group MCP connections', {
- workspaceId: ctx.workspaceId,
- count: result.count,
- hasMore: result.hasMore,
- })
- return result
- }
- case 'send_invite': {
- await enforceCredentialGroupInvitationExecutionRateLimit(principal.workspaceId)
- const result = await sendCredentialGroupInvite.execute({
- principal,
- input: {
- workspaceId: ctx.workspaceId,
- email: requireString(inputs.email, 'Email'),
- },
- })
- logger.info('Sent Credential Group invitation', {
- workspaceId: ctx.workspaceId,
- enrollmentId: result.enrollment.id,
- })
- return {
- enrollmentId: result.enrollment.id,
- email: result.enrollment.email,
- status: result.enrollment.status,
- invitedAt: result.enrollment.invitedAt,
- expiresAt: result.enrollment.expiresAt,
- }
- }
- case 'get_invite_link': {
- await enforceCredentialGroupInvitationExecutionRateLimit(principal.workspaceId)
- const result = await createCredentialGroupInviteLink.execute({
- principal,
- input: {
- workspaceId: ctx.workspaceId,
- email: requireString(inputs.email, 'Email'),
- },
- })
- logger.info('Generated Credential Group invitation link', {
- workspaceId: ctx.workspaceId,
- enrollmentId: result.enrollment.id,
- })
- return {
- enrollmentId: result.enrollment.id,
- email: result.enrollment.email,
- status: result.enrollment.status,
- invitedAt: result.enrollment.invitedAt,
- expiresAt: result.enrollment.expiresAt,
- invitationLink: result.invitationLink,
- }
- }
- case 'list_people': {
- const statuses = parseStringList(inputs.peopleStatuses, 'People statuses')
- const allowedStatuses = new Set(CREDENTIAL_GROUP_PEOPLE_STATUSES)
- if (statuses?.some((status) => !allowedStatuses.has(status))) {
- throw new Error('People statuses contain an unsupported value')
- }
- const result = await listCredentialGroupPeople.execute({
- principal,
- input: {
- workspaceId: ctx.workspaceId,
- limit: parseLimit(inputs.limit),
- cursor: parseOptionalString(inputs.cursor, 'Cursor'),
- email: parseOptionalString(inputs.email, 'Email'),
- statuses: statuses as CredentialGroupEnrollmentStatus[] | undefined,
- },
- })
- logger.info('Listed Credential Group people', {
- workspaceId: ctx.workspaceId,
- count: result.count,
- hasMore: result.hasMore,
- })
- return result
- }
- }
+ async execute(): Promise {
+ throw new Error(
+ 'Replace this legacy Connected Accounts block with a Credential block and configure its organization operation. Invitations are managed in organization settings.'
+ )
}
}
diff --git a/apps/sim/executor/handlers/credential/credential-handler.test.ts b/apps/sim/executor/handlers/credential/credential-handler.test.ts
new file mode 100644
index 00000000000..417616009af
--- /dev/null
+++ b/apps/sim/executor/handlers/credential/credential-handler.test.ts
@@ -0,0 +1,153 @@
+/** @vitest-environment node */
+import { beforeEach, describe, expect, it, vi } from 'vitest'
+import type { ExecutionContext } from '@/executor/types'
+import type { SerializedBlock } from '@/serializer/types'
+
+const mocks = vi.hoisted(() => ({
+ principal: vi.fn(),
+ oauth: vi.fn(),
+ mcp: vi.fn(),
+ workspace: vi.fn(),
+}))
+vi.mock('@/lib/internal/principals/executor', () => ({
+ createExecutorPrincipalFromExecutionContext: mocks.principal,
+}))
+vi.mock('@/lib/credential-groups/application/list-credentials', () => ({
+ listCredentialGroupCredentials: { execute: mocks.oauth },
+}))
+vi.mock('@/lib/credential-groups/application/list-mcp-connections', () => ({
+ listCredentialGroupMcpConnections: { execute: mocks.mcp },
+}))
+vi.mock('@/lib/credentials/application/resolve-workflow-credentials', () => ({
+ resolveWorkflowCredentials: { execute: mocks.workspace },
+}))
+
+import { CredentialBlockHandler } from '@/executor/handlers/credential/credential-handler'
+
+const ctx = {
+ workspaceId: 'child-workspace',
+ executorDelegationOrigin: { workflowId: 'child-workflow' },
+} as ExecutionContext
+const block = { metadata: { id: 'credential' } } as SerializedBlock
+const handler = new CredentialBlockHandler()
+const account = {
+ credentialId: 'credential-1',
+ email: 'person@example.com',
+ providerId: 'google-email',
+ displayName: 'Person',
+}
+
+describe('Credential organization operations', () => {
+ beforeEach(() => {
+ vi.clearAllMocks()
+ mocks.principal.mockResolvedValue({ delegationId: 'current-run' })
+ mocks.oauth.mockResolvedValue({
+ credentials: [account],
+ count: 1,
+ hasMore: false,
+ nextCursor: null,
+ })
+ })
+ it('uses the actual executing workspace for an exact account lookup', async () => {
+ expect(
+ await handler.execute(ctx, block, {
+ operation: 'find_organization_account',
+ email: account.email,
+ organizationProvider: 'google-email',
+ })
+ ).toEqual(account)
+ expect(mocks.oauth).toHaveBeenCalledWith({
+ principal: { delegationId: 'current-run' },
+ input: {
+ workspaceId: 'child-workspace',
+ email: account.email,
+ credentialProviderIds: ['google-email'],
+ limit: 2,
+ cursor: undefined,
+ },
+ })
+ })
+ it.each([0, 2])('rejects an ambiguous or missing account (%i matches)', async (count) => {
+ mocks.oauth.mockResolvedValue({
+ credentials: Array.from({ length: count }, () => account),
+ hasMore: false,
+ })
+ await expect(
+ handler.execute(ctx, block, {
+ operation: 'find_organization_account',
+ email: account.email,
+ organizationProvider: 'google-email',
+ })
+ ).rejects.toThrow('Expected exactly one')
+ })
+ it('requires both email and provider for find', async () => {
+ await expect(
+ handler.execute(ctx, block, { operation: 'find_organization_account', email: account.email })
+ ).rejects.toThrow('Provider is required')
+ expect(mocks.oauth).not.toHaveBeenCalled()
+ })
+ it('preserves pagination for lists', async () => {
+ await handler.execute(ctx, block, {
+ operation: 'list_organization_accounts',
+ limit: '25',
+ cursor: 'previous',
+ organizationProviders: '["google-email"]',
+ })
+ expect(mocks.oauth).toHaveBeenCalledWith(
+ expect.objectContaining({
+ input: expect.objectContaining({
+ limit: 25,
+ cursor: 'previous',
+ credentialProviderIds: ['google-email'],
+ }),
+ })
+ )
+ })
+ it('returns the person’s MCP credential separately from the shared server', async () => {
+ const connection = {
+ credentialId: 'mcp-cg-person',
+ mcpServerId: 'server',
+ toolNames: ['search'],
+ }
+ mocks.mcp.mockResolvedValue({ mcpConnections: [connection], hasMore: false })
+ expect(
+ await handler.execute(ctx, block, {
+ operation: 'find_organization_mcp_connection',
+ email: account.email,
+ mcpProvider: 'fireflies',
+ })
+ ).toEqual(connection)
+ expect(mocks.mcp).toHaveBeenCalledWith(
+ expect.objectContaining({
+ input: expect.objectContaining({
+ connectorId: 'fireflies',
+ email: account.email,
+ limit: 2,
+ }),
+ })
+ )
+ })
+ it('preserves workspace credential selection through its authorized use case', async () => {
+ mocks.workspace.mockResolvedValue([account])
+ expect(
+ await handler.execute(ctx, block, { operation: 'select', credentialId: 'credential-1' })
+ ).toEqual(account)
+ expect(mocks.oauth).not.toHaveBeenCalled()
+ })
+ it.each(['send_invite', 'get_invite_link', 'list_people'])(
+ 'does not expose the removed %s operation',
+ async (operation) => {
+ await expect(handler.execute(ctx, block, { operation })).rejects.toThrow(
+ 'Unsupported Credential operation'
+ )
+ }
+ )
+ it('requires trusted workflow execution', async () => {
+ await expect(
+ handler.execute({ workspaceId: 'workspace' } as ExecutionContext, block, {
+ operation: 'list_organization_accounts',
+ })
+ ).rejects.toThrow('authenticated workflow execution')
+ expect(mocks.principal).not.toHaveBeenCalled()
+ })
+})
diff --git a/apps/sim/executor/handlers/credential/credential-handler.ts b/apps/sim/executor/handlers/credential/credential-handler.ts
index ff01b77ff95..f570e00aa73 100644
--- a/apps/sim/executor/handlers/credential/credential-handler.ts
+++ b/apps/sim/executor/handlers/credential/credential-handler.ts
@@ -1,13 +1,61 @@
-import { db } from '@sim/db'
-import { credential } from '@sim/db/schema'
-import { createLogger } from '@sim/logger'
-import { and, asc, eq, inArray } from 'drizzle-orm'
+import { CREDENTIAL_GROUP_DELEGATION_AUDIENCE } from '@/lib/credential-groups/application/authorization'
+import { listCredentialGroupCredentials } from '@/lib/credential-groups/application/list-credentials'
+import { listCredentialGroupMcpConnections } from '@/lib/credential-groups/application/list-mcp-connections'
+import { MAX_CREDENTIAL_GROUP_CREDENTIAL_PAGE_SIZE } from '@/lib/credential-groups/credentials'
+import { resolveWorkflowCredentials } from '@/lib/credentials/application/resolve-workflow-credentials'
+import { createExecutorPrincipalFromExecutionContext } from '@/lib/internal/principals/executor'
import type { BlockOutput } from '@/blocks/types'
import { BlockType } from '@/executor/constants'
import type { BlockHandler, ExecutionContext } from '@/executor/types'
import type { SerializedBlock } from '@/serializer/types'
-const logger = createLogger('CredentialBlockHandler')
+function parseStringList(value: unknown, label: string): string[] | undefined {
+ if (value === undefined || value === null || value === '') return undefined
+
+ let parsed: unknown = value
+ if (typeof value === 'string') {
+ const trimmed = value.trim()
+ if (!trimmed) return undefined
+ if (!trimmed.startsWith('[')) return [trimmed]
+ try {
+ parsed = JSON.parse(trimmed)
+ } catch {
+ throw new Error(`${label} must be a valid JSON array of strings`)
+ }
+ }
+
+ if (!Array.isArray(parsed) || !parsed.every((item) => typeof item === 'string' && item.trim())) {
+ throw new Error(`${label} must be an array of non-empty strings`)
+ }
+
+ const values = [...new Set(parsed.map((item) => item.trim()))]
+ return values.length > 0 ? values : undefined
+}
+
+function parseLimit(value: unknown): number {
+ const raw = value ?? MAX_CREDENTIAL_GROUP_CREDENTIAL_PAGE_SIZE
+ const limit =
+ typeof raw === 'number' ? raw : typeof raw === 'string' && raw.trim() ? Number(raw) : Number.NaN
+ if (!Number.isInteger(limit) || limit < 1 || limit > MAX_CREDENTIAL_GROUP_CREDENTIAL_PAGE_SIZE) {
+ throw new Error(
+ `Limit must be an integer between 1 and ${MAX_CREDENTIAL_GROUP_CREDENTIAL_PAGE_SIZE}`
+ )
+ }
+ return limit
+}
+
+function parseOptionalString(value: unknown, label: string): string | undefined {
+ if (value === undefined || value === null || value === '') return undefined
+ if (typeof value !== 'string' || !value.trim())
+ throw new Error(`${label} must be a non-empty string`)
+ return value.trim()
+}
+
+function requireString(value: unknown, label: string): string {
+ const parsed = parseOptionalString(value, label)
+ if (!parsed) throw new Error(`${label} is required`)
+ return parsed
+}
export class CredentialBlockHandler implements BlockHandler {
canHandle(block: SerializedBlock): boolean {
@@ -19,96 +67,86 @@ export class CredentialBlockHandler implements BlockHandler {
_block: SerializedBlock,
inputs: Record
): Promise {
- if (!ctx.workspaceId) {
- throw new Error('workspaceId is required for credential resolution')
- }
-
- const operation = typeof inputs.operation === 'string' ? inputs.operation : 'select'
-
+ if (!ctx.workspaceId || !ctx.executorDelegationOrigin)
+ throw new Error('Credential operations require an authenticated workflow execution')
+ const principal = await createExecutorPrincipalFromExecutionContext({
+ context: ctx,
+ audience: CREDENTIAL_GROUP_DELEGATION_AUDIENCE,
+ })
+ const operation = parseOptionalString(inputs.operation, 'Operation') ?? 'select'
switch (operation) {
- case 'select':
- return this.selectCredential(ctx.workspaceId, inputs)
- case 'list':
- return this.listCredentials(ctx.workspaceId, inputs)
+ case 'select': {
+ const credentials = await resolveWorkflowCredentials.execute({
+ principal,
+ input: {
+ workspaceId: ctx.workspaceId,
+ credentialId: requireString(inputs.credentialId, 'Credential ID'),
+ },
+ })
+ const selected = credentials[0]
+ if (!selected) throw new Error('Credential not found')
+ return selected
+ }
+ case 'list': {
+ const credentials = await resolveWorkflowCredentials.execute({
+ principal,
+ input: {
+ workspaceId: ctx.workspaceId,
+ providerIds: parseStringList(inputs.providerFilter, 'Providers'),
+ },
+ })
+ return { credentials, count: credentials.length }
+ }
+ case 'find_organization_account':
+ case 'list_organization_accounts': {
+ const find = operation === 'find_organization_account'
+ const result = await listCredentialGroupCredentials.execute({
+ principal,
+ input: {
+ workspaceId: ctx.workspaceId,
+ email: find
+ ? requireString(inputs.email, 'Email')
+ : parseOptionalString(inputs.email, 'Email'),
+ credentialProviderIds: find
+ ? [requireString(inputs.organizationProvider, 'Provider')]
+ : parseStringList(inputs.organizationProviders, 'Providers'),
+ limit: find ? 2 : parseLimit(inputs.limit),
+ cursor: find ? undefined : parseOptionalString(inputs.cursor, 'Cursor'),
+ },
+ })
+ if (!find) return result
+ if (result.credentials.length !== 1 || result.hasMore)
+ throw new Error(
+ `Expected exactly one organization account; found ${result.credentials.length}${result.hasMore ? '+' : ''}. Check the email, provider, and connection status.`
+ )
+ return result.credentials[0]!
+ }
+ case 'find_organization_mcp_connection':
+ case 'list_organization_mcp_connections': {
+ const find = operation === 'find_organization_mcp_connection'
+ const result = await listCredentialGroupMcpConnections.execute({
+ principal,
+ input: {
+ workspaceId: ctx.workspaceId,
+ email: find
+ ? requireString(inputs.email, 'Email')
+ : parseOptionalString(inputs.email, 'Email'),
+ connectorId: find
+ ? requireString(inputs.mcpProvider, 'MCP provider')
+ : parseOptionalString(inputs.mcpProvider, 'MCP provider'),
+ limit: find ? 2 : parseLimit(inputs.limit),
+ cursor: find ? undefined : parseOptionalString(inputs.cursor, 'Cursor'),
+ },
+ })
+ if (!find) return result
+ if (result.mcpConnections.length !== 1 || result.hasMore)
+ throw new Error(
+ `Expected exactly one organization MCP connection; found ${result.mcpConnections.length}${result.hasMore ? '+' : ''}. Check the email, provider, and connection status.`
+ )
+ return result.mcpConnections[0]!
+ }
default:
throw new Error(`Unsupported Credential operation: ${operation}`)
}
}
-
- private async selectCredential(
- workspaceId: string,
- inputs: Record
- ): Promise {
- const credentialId = typeof inputs.credentialId === 'string' ? inputs.credentialId.trim() : ''
-
- if (!credentialId) {
- throw new Error('No credential selected')
- }
-
- const record = await db.query.credential.findFirst({
- where: and(
- eq(credential.id, credentialId),
- eq(credential.workspaceId, workspaceId),
- eq(credential.type, 'oauth')
- ),
- columns: {
- id: true,
- displayName: true,
- providerId: true,
- },
- })
-
- if (!record) {
- throw new Error(`Credential not found: ${credentialId}`)
- }
-
- logger.info('Credential block resolved', { credentialId: record.id })
-
- return {
- credentialId: record.id,
- displayName: record.displayName,
- providerId: record.providerId ?? '',
- }
- }
-
- private async listCredentials(
- workspaceId: string,
- inputs: Record
- ): Promise {
- const providerFilter = Array.isArray(inputs.providerFilter)
- ? (inputs.providerFilter as string[]).filter(Boolean)
- : []
-
- const conditions = [eq(credential.workspaceId, workspaceId), eq(credential.type, 'oauth')]
-
- if (providerFilter.length > 0) {
- conditions.push(inArray(credential.providerId, providerFilter))
- }
-
- const records = await db.query.credential.findMany({
- where: and(...conditions),
- columns: {
- id: true,
- displayName: true,
- providerId: true,
- },
- orderBy: [asc(credential.displayName)],
- })
-
- const credentials = records.map((r) => ({
- credentialId: r.id,
- displayName: r.displayName,
- providerId: r.providerId ?? '',
- }))
-
- logger.info('Credential block listed credentials', {
- count: credentials.length,
- providerFilter: providerFilter.length > 0 ? providerFilter : undefined,
- })
-
- return {
- credentials,
- count: credentials.length,
- }
- }
}
diff --git a/apps/sim/executor/variables/resolvers/block.test.ts b/apps/sim/executor/variables/resolvers/block.test.ts
index de4cb9da017..c972df2afc7 100644
--- a/apps/sim/executor/variables/resolvers/block.test.ts
+++ b/apps/sim/executor/variables/resolvers/block.test.ts
@@ -8,6 +8,7 @@ import { BlockResolver } from './block'
import { RESOLVED_EMPTY, type ResolutionContext } from './reference'
vi.mock('@/lib/uploads/server/metadata', () => ({
+ insertImmutableFileMetadata: vi.fn().mockResolvedValue({ id: 'execution-payload-file' }),
insertFileMetadata: vi.fn().mockResolvedValue({ id: 'execution-payload-file' }),
deleteFileMetadata: vi.fn().mockResolvedValue(undefined),
}))
diff --git a/apps/sim/executor/variables/resolvers/workflow.test.ts b/apps/sim/executor/variables/resolvers/workflow.test.ts
index 4d884b532a3..0055aaed708 100644
--- a/apps/sim/executor/variables/resolvers/workflow.test.ts
+++ b/apps/sim/executor/variables/resolvers/workflow.test.ts
@@ -16,6 +16,7 @@ vi.mock('@/lib/workflows/variables/variable-manager', () => ({
}))
vi.mock('@/lib/uploads/server/metadata', () => ({
+ insertImmutableFileMetadata: vi.fn().mockResolvedValue({ id: 'execution-payload-file' }),
insertFileMetadata: vi.fn().mockResolvedValue({ id: 'execution-payload-file' }),
deleteFileMetadata: vi.fn().mockResolvedValue(undefined),
}))
diff --git a/apps/sim/hooks/queries/credential-groups.ts b/apps/sim/hooks/queries/credential-groups.ts
index 3bcb06e41f4..949564cec16 100644
--- a/apps/sim/hooks/queries/credential-groups.ts
+++ b/apps/sim/hooks/queries/credential-groups.ts
@@ -282,15 +282,24 @@ export function useStartSlackCredentialGroupConfiguration() {
| { organizationId: string; workspaceId?: never }
)) => {
const scope = resourceScopeFromOwner({ workspaceId, organizationId })
- return scope.kind === 'organization'
- ? requestJson(startOrganizationSlackConfigurationContract, {
- params: { id: scope.organizationId, groupId: credentialGroupId },
- body,
- })
- : requestJson(startSlackCredentialGroupConfigurationContract, {
- params: { id: scope.workspaceId, groupId: credentialGroupId },
- body,
- })
+ if (scope.kind === 'organization') {
+ if (!body.appId || !body.teamId)
+ throw new Error('Slack App ID and workspace ID are required')
+ return requestJson(startOrganizationSlackConfigurationContract, {
+ params: { id: scope.organizationId, groupId: credentialGroupId },
+ body: {
+ clientId: body.clientId,
+ clientSecret: body.clientSecret,
+ appId: body.appId,
+ teamId: body.teamId,
+ requiredScopes: body.requiredScopes,
+ },
+ })
+ }
+ return requestJson(startSlackCredentialGroupConfigurationContract, {
+ params: { id: scope.workspaceId, groupId: credentialGroupId },
+ body,
+ })
},
})
}
diff --git a/apps/sim/hooks/queries/organization-accounts.ts b/apps/sim/hooks/queries/organization-accounts.ts
index 3e5ce4f3e2f..6ef7a263930 100644
--- a/apps/sim/hooks/queries/organization-accounts.ts
+++ b/apps/sim/hooks/queries/organization-accounts.ts
@@ -1,20 +1,46 @@
'use client'
-import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
+import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { requestJson } from '@/lib/api/client/request'
import {
+ type AddOrganizationAccountMcpProviderBody,
+ addOrganizationAccountMcpProviderContract,
+ type ConfigureOrganizationMcpBody,
+ configureOrganizationMcpContract,
+ disconnectPersonalOrganizationAccountContract,
type EnsureOrganizationAccountsBody,
ensureOrganizationAccountsContract,
getOrganizationAccountsContract,
+ getOrganizationAccountWorkspaceAccessContract,
+ getOrganizationDatabricksSetupContract,
+ getWorkspaceOrganizationAccountsContract,
+ type InviteOrganizationAccountPeopleBody,
+ inviteOrganizationAccountPeopleContract,
+ listOrganizationAccountPeopleContract,
+ listPersonalOrganizationAccountsContract,
+ type RemoveOrganizationAccountMcpProviderParams,
+ reconnectPersonalOrganizationAccountContract,
+ removeOrganizationAccountMcpProviderContract,
+ resendOrganizationAccountInvitationContract,
+ revokeOrganizationAccountEnrollmentContract,
startOrganizationAccountConnectionContract,
type UpdateOrganizationAccountsBody,
+ type UpdateOrganizationAccountWorkspaceAccessBody,
updateOrganizationAccountsContract,
+ updateOrganizationAccountWorkspaceAccessContract,
} from '@/lib/api/contracts/organization-accounts'
export const ORGANIZATION_ACCOUNTS_STALE_TIME = 30_000
export const organizationAccountsKeys = {
all: ['organization-accounts'] as const,
+ personal: () => [...organizationAccountsKeys.all, 'personal'] as const,
+ workspaces: () => [...organizationAccountsKeys.all, 'workspace'] as const,
+ workspace: (workspaceId?: string) =>
+ [...organizationAccountsKeys.workspaces(), workspaceId ?? ''] as const,
+ access: (id?: string) => [...organizationAccountsKeys.detail(id), 'access'] as const,
+ people: (id?: string) => [...organizationAccountsKeys.detail(id), 'people'] as const,
+ databricks: (id?: string) => [...organizationAccountsKeys.detail(id), 'databricks'] as const,
details: () => [...organizationAccountsKeys.all, 'detail'] as const,
detail: (organizationId?: string) =>
[...organizationAccountsKeys.details(), organizationId ?? ''] as const,
@@ -48,6 +74,38 @@ export function useEnsureOrganizationAccounts() {
})
}
+export function useOrganizationDatabricksSetup(organizationId: string, enabled: boolean) {
+ return useQuery({
+ queryKey: organizationAccountsKeys.databricks(organizationId),
+ enabled,
+ staleTime: ORGANIZATION_ACCOUNTS_STALE_TIME,
+ queryFn: ({ signal }) =>
+ requestJson(getOrganizationDatabricksSetupContract, {
+ params: { id: organizationId },
+ signal,
+ }),
+ })
+}
+
+export function useConfigureOrganizationMcp() {
+ const queryClient = useQueryClient()
+ return useMutation({
+ mutationFn: ({
+ organizationId,
+ ...body
+ }: { organizationId: string } & ConfigureOrganizationMcpBody) =>
+ requestJson(configureOrganizationMcpContract, { params: { id: organizationId }, body }),
+ onSuccess: (_, { organizationId }) =>
+ Promise.all([
+ queryClient.invalidateQueries({
+ queryKey: organizationAccountsKeys.detail(organizationId),
+ }),
+ queryClient.invalidateQueries({ queryKey: organizationAccountsKeys.workspaces() }),
+ queryClient.invalidateQueries({ queryKey: organizationAccountsKeys.personal() }),
+ ]),
+ })
+}
+
export function useUpdateOrganizationAccounts() {
const queryClient = useQueryClient()
return useMutation({
@@ -65,7 +123,13 @@ export function useUpdateOrganizationAccounts() {
body: update,
}),
onSuccess: (_, { organizationId }) =>
- queryClient.invalidateQueries({ queryKey: organizationAccountsKeys.detail(organizationId) }),
+ Promise.all([
+ queryClient.invalidateQueries({
+ queryKey: organizationAccountsKeys.detail(organizationId),
+ }),
+ queryClient.invalidateQueries({ queryKey: organizationAccountsKeys.workspaces() }),
+ queryClient.invalidateQueries({ queryKey: organizationAccountsKeys.personal() }),
+ ]),
})
}
@@ -78,3 +142,192 @@ export function useConnectOrganizationAccount() {
}),
})
}
+
+export function useWorkspaceOrganizationAccounts(workspaceId?: string, enabled = true) {
+ return useQuery({
+ queryKey: organizationAccountsKeys.workspace(workspaceId),
+ enabled: Boolean(workspaceId) && enabled,
+ staleTime: ORGANIZATION_ACCOUNTS_STALE_TIME,
+ queryFn: ({ signal }) => {
+ if (!workspaceId) throw new Error('Workspace is required')
+ return requestJson(getWorkspaceOrganizationAccountsContract, {
+ params: { id: workspaceId },
+ signal,
+ })
+ },
+ })
+}
+export function useOrganizationAccountWorkspaceAccess(organizationId: string) {
+ return useQuery({
+ queryKey: organizationAccountsKeys.access(organizationId),
+ staleTime: ORGANIZATION_ACCOUNTS_STALE_TIME,
+ queryFn: ({ signal }) =>
+ requestJson(getOrganizationAccountWorkspaceAccessContract, {
+ params: { id: organizationId },
+ signal,
+ }),
+ })
+}
+export function useUpdateOrganizationAccountWorkspaceAccess() {
+ const queryClient = useQueryClient()
+ return useMutation({
+ mutationFn: ({
+ organizationId,
+ ...body
+ }: { organizationId: string } & UpdateOrganizationAccountWorkspaceAccessBody) =>
+ requestJson(updateOrganizationAccountWorkspaceAccessContract, {
+ params: { id: organizationId },
+ body,
+ }),
+ onSuccess: (_, { organizationId }) =>
+ Promise.all([
+ queryClient.invalidateQueries({
+ queryKey: organizationAccountsKeys.access(organizationId),
+ }),
+ queryClient.invalidateQueries({ queryKey: organizationAccountsKeys.workspaces() }),
+ ]),
+ })
+}
+export function useOrganizationAccountPeople(organizationId: string) {
+ return useInfiniteQuery({
+ queryKey: organizationAccountsKeys.people(organizationId),
+ staleTime: ORGANIZATION_ACCOUNTS_STALE_TIME,
+ initialPageParam: undefined as string | undefined,
+ queryFn: ({ signal, pageParam }) =>
+ requestJson(listOrganizationAccountPeopleContract, {
+ params: { id: organizationId },
+ query: { limit: 50, cursor: pageParam },
+ signal,
+ }),
+ getNextPageParam: (page) => page.nextCursor ?? undefined,
+ })
+}
+export function useInviteOrganizationAccountPeople() {
+ const queryClient = useQueryClient()
+ return useMutation({
+ mutationFn: ({
+ organizationId,
+ ...body
+ }: { organizationId: string } & InviteOrganizationAccountPeopleBody) =>
+ requestJson(inviteOrganizationAccountPeopleContract, {
+ params: { id: organizationId },
+ body,
+ }),
+ onSuccess: (_, { organizationId }) =>
+ queryClient.invalidateQueries({ queryKey: organizationAccountsKeys.people(organizationId) }),
+ })
+}
+export function useResendOrganizationAccountInvitation() {
+ const queryClient = useQueryClient()
+ return useMutation({
+ mutationFn: ({
+ organizationId,
+ enrollmentId,
+ }: {
+ organizationId: string
+ enrollmentId: string
+ }) =>
+ requestJson(resendOrganizationAccountInvitationContract, {
+ params: { id: organizationId, enrollmentId },
+ }),
+ onSuccess: (_, { organizationId }) =>
+ queryClient.invalidateQueries({ queryKey: organizationAccountsKeys.people(organizationId) }),
+ })
+}
+export function useRevokeOrganizationAccountEnrollment() {
+ const queryClient = useQueryClient()
+ return useMutation({
+ mutationFn: ({
+ organizationId,
+ enrollmentId,
+ }: {
+ organizationId: string
+ enrollmentId: string
+ }) =>
+ requestJson(revokeOrganizationAccountEnrollmentContract, {
+ params: { id: organizationId, enrollmentId },
+ }),
+ onSuccess: (_, { organizationId }) =>
+ Promise.all([
+ queryClient.invalidateQueries({
+ queryKey: organizationAccountsKeys.people(organizationId),
+ }),
+ queryClient.invalidateQueries({ queryKey: organizationAccountsKeys.personal() }),
+ ]),
+ })
+}
+export function useAddOrganizationAccountMcpProvider() {
+ const queryClient = useQueryClient()
+ return useMutation({
+ mutationFn: ({
+ organizationId,
+ ...body
+ }: { organizationId: string } & AddOrganizationAccountMcpProviderBody) =>
+ requestJson(addOrganizationAccountMcpProviderContract, {
+ params: { id: organizationId },
+ body,
+ }),
+ onSuccess: (_, { organizationId }) =>
+ Promise.all([
+ queryClient.invalidateQueries({
+ queryKey: organizationAccountsKeys.detail(organizationId),
+ }),
+ queryClient.invalidateQueries({ queryKey: organizationAccountsKeys.workspaces() }),
+ ]),
+ })
+}
+export function useRemoveOrganizationAccountMcpProvider() {
+ const queryClient = useQueryClient()
+ return useMutation({
+ mutationFn: ({
+ organizationId,
+ connectorId,
+ }: { organizationId: string } & Pick<
+ RemoveOrganizationAccountMcpProviderParams,
+ 'connectorId'
+ >) =>
+ requestJson(removeOrganizationAccountMcpProviderContract, {
+ params: { id: organizationId, connectorId },
+ }),
+ onSuccess: (_, { organizationId }) =>
+ Promise.all([
+ queryClient.invalidateQueries({
+ queryKey: organizationAccountsKeys.detail(organizationId),
+ }),
+ queryClient.invalidateQueries({ queryKey: organizationAccountsKeys.workspaces() }),
+ queryClient.invalidateQueries({ queryKey: organizationAccountsKeys.personal() }),
+ ]),
+ })
+}
+
+export function usePersonalOrganizationAccounts() {
+ return useInfiniteQuery({
+ queryKey: organizationAccountsKeys.personal(),
+ staleTime: ORGANIZATION_ACCOUNTS_STALE_TIME,
+ initialPageParam: undefined as string | undefined,
+ queryFn: ({ signal, pageParam }) =>
+ requestJson(listPersonalOrganizationAccountsContract, {
+ query: { cursor: pageParam },
+ signal,
+ }),
+ getNextPageParam: (page) => page.nextCursor ?? undefined,
+ })
+}
+export function useReconnectPersonalOrganizationAccount() {
+ return useMutation({
+ mutationFn: (credentialId: string) =>
+ requestJson(reconnectPersonalOrganizationAccountContract, { params: { credentialId } }),
+ })
+}
+export function useDisconnectPersonalOrganizationAccount() {
+ const queryClient = useQueryClient()
+ return useMutation({
+ mutationFn: (credentialId: string) =>
+ requestJson(disconnectPersonalOrganizationAccountContract, { params: { credentialId } }),
+ onSuccess: () =>
+ Promise.all([
+ queryClient.invalidateQueries({ queryKey: organizationAccountsKeys.personal() }),
+ queryClient.invalidateQueries({ queryKey: organizationAccountsKeys.details() }),
+ ]),
+ })
+}
diff --git a/apps/sim/lib/api/contracts/credential-groups.test.ts b/apps/sim/lib/api/contracts/credential-groups.test.ts
index d90fd8918d7..1e339a10915 100644
--- a/apps/sim/lib/api/contracts/credential-groups.test.ts
+++ b/apps/sim/lib/api/contracts/credential-groups.test.ts
@@ -118,7 +118,7 @@ describe('credential group contracts', () => {
expect(result.success).toBe(false)
})
- it('requires a custom bot for Slack option updates', () => {
+ it('allows Slack options without a workspace bot for organization personal authorization', () => {
const missingApp = updateCredentialGroupBodySchema.safeParse({
options: [
{
@@ -139,7 +139,7 @@ describe('credential group contracts', () => {
],
})
- expect(missingApp.success).toBe(false)
+ expect(missingApp.success).toBe(true)
expect(withApp.success).toBe(true)
})
diff --git a/apps/sim/lib/api/contracts/credential-groups.ts b/apps/sim/lib/api/contracts/credential-groups.ts
index 5dec48b3c49..7fd305e57f5 100644
--- a/apps/sim/lib/api/contracts/credential-groups.ts
+++ b/apps/sim/lib/api/contracts/credential-groups.ts
@@ -49,7 +49,7 @@ const slackCredentialGroupOptionInputSchema = z
.object({
provider: z.literal('slack'),
...credentialGroupOptionFields,
- slackBotCredentialId: z.string().uuid('Select a custom Slack bot'),
+ slackBotCredentialId: z.string().uuid('Select a custom Slack bot').optional(),
})
.strict()
@@ -267,7 +267,17 @@ export type CredentialGroupOAuthCallbackQuery = z.output<
export const startSlackCredentialGroupConfigurationBodySchema = z
.object({
- slackBotCredentialId: z.string().uuid('Select a custom Slack bot'),
+ slackBotCredentialId: z.string().uuid('Select a custom Slack bot').optional(),
+ appId: z
+ .string()
+ .regex(/^A[A-Z0-9]+$/, 'Enter the Slack App ID')
+ .max(64)
+ .optional(),
+ teamId: z
+ .string()
+ .regex(/^T[A-Z0-9]+$/, 'Enter the Slack workspace ID')
+ .max(64)
+ .optional(),
clientId: z.string().trim().min(1, 'Slack Client ID is required').max(256),
clientSecret: z.string().trim().min(1, 'Slack Client Secret is required').max(512),
requiredScopes: z.array(z.string().trim().min(1).max(255)).min(1).max(100).optional(),
diff --git a/apps/sim/lib/api/contracts/organization-accounts.test.ts b/apps/sim/lib/api/contracts/organization-accounts.test.ts
new file mode 100644
index 00000000000..0a964866eb1
--- /dev/null
+++ b/apps/sim/lib/api/contracts/organization-accounts.test.ts
@@ -0,0 +1,39 @@
+/** @vitest-environment node */
+import { describe, expect, it } from 'vitest'
+import { addOrganizationAccountMcpProviderContract } from '@/lib/api/contracts/organization-accounts'
+
+describe('organization MCP provider creation contract', () => {
+ const schema = addOrganizationAccountMcpProviderContract.body
+ if (!schema) throw new Error('MCP provider creation requires a body contract')
+ const databricks = {
+ connectorId: 'databricks',
+ name: 'Databricks',
+ url: 'https://tenant.cloud.databricks.com/api/2.0/mcp/sql',
+ oauthClientId: 'client-1',
+ }
+
+ it.each(['fireflies', 'granola'])(
+ 'allows adding %s without tenant configuration',
+ (connectorId) => {
+ expect(schema.parse({ connectorId })).toEqual({ connectorId })
+ }
+ )
+
+ it('requires configuration when adding Databricks', () => {
+ expect(schema.safeParse({ connectorId: 'databricks' }).success).toBe(false)
+ expect(schema.parse(databricks)).toEqual(databricks)
+ })
+
+ it.each(['name', 'url', 'oauthClientId'])('rejects missing or blank %s', (field) => {
+ expect(schema.safeParse({ ...databricks, [field]: undefined }).success).toBe(false)
+ expect(schema.safeParse({ ...databricks, [field]: ' ' }).success).toBe(false)
+ })
+
+ it('accepts an optional client secret while rejecting a caller-supplied workspace scope', () => {
+ expect(schema.parse({ ...databricks, oauthClientSecret: 'secret-1' })).toHaveProperty(
+ 'oauthClientSecret',
+ 'secret-1'
+ )
+ expect(schema.safeParse({ ...databricks, workspaceId: 'workspace-1' }).success).toBe(false)
+ })
+})
diff --git a/apps/sim/lib/api/contracts/organization-accounts.ts b/apps/sim/lib/api/contracts/organization-accounts.ts
index 2afed8334b2..5b03d3ca021 100644
--- a/apps/sim/lib/api/contracts/organization-accounts.ts
+++ b/apps/sim/lib/api/contracts/organization-accounts.ts
@@ -1,14 +1,25 @@
import { z } from 'zod'
import {
+ createCredentialGroupMcpConnectorBodySchema,
+ credentialGroupEnrollmentDetailSchema,
+ credentialGroupEnrollmentSchema,
+ credentialGroupMcpServerSchema,
credentialGroupOptionInputSchema,
credentialGroupProviderSchema,
credentialGroupSchema,
+ inviteCredentialGroupEnrollmentsBodySchema,
+ inviteCredentialGroupEnrollmentsContract,
+ managedMcpConnectorIdSchema,
startSlackCredentialGroupConfigurationBodySchema,
startSlackCredentialGroupConfigurationContract,
updateCredentialGroupBodySchema,
} from '@/lib/api/contracts/credential-groups'
-import { organizationIdSchema } from '@/lib/api/contracts/primitives'
+import { organizationIdSchema, workspaceIdSchema } from '@/lib/api/contracts/primitives'
import { defineRouteContract } from '@/lib/api/contracts/types'
+import {
+ ORGANIZATION_ACCOUNT_INDEXING_SOURCE_LIMIT,
+ ORGANIZATION_ACCOUNT_WORKSPACE_LIMIT,
+} from '@/lib/credential-groups/limits'
const organizationAccountsParamsSchema = z.object({ id: organizationIdSchema })
const organizationCredentialGroupSchema = credentialGroupSchema.extend({
@@ -29,6 +40,7 @@ export const getOrganizationAccountsContract = defineRouteContract({
credentialGroup: organizationCredentialGroupSchema.nullable(),
availableProviders: z.array(credentialGroupProviderSchema),
canManage: z.boolean(),
+ indexingAvailable: z.boolean(),
}),
},
})
@@ -58,11 +70,245 @@ export const startOrganizationSlackConfigurationContract = defineRouteContract({
method: 'POST',
path: '/api/organizations/[id]/connected-accounts/[groupId]/slack-managed-users',
params: organizationAccountsParamsSchema.extend({ groupId: z.string().min(1).max(128) }),
- body: startSlackCredentialGroupConfigurationBodySchema,
+ body: startSlackCredentialGroupConfigurationBodySchema
+ .omit({ slackBotCredentialId: true })
+ .required({ appId: true, teamId: true }),
response: startSlackCredentialGroupConfigurationContract.response,
})
export type OrganizationAccountsSettings = z.output<
typeof getOrganizationAccountsContract.response.schema
>
-export type EnsureOrganizationAccountsBody = z.input
+
+export const updateOrganizationAccountIndexingContract = defineRouteContract({
+ method: 'PUT',
+ path: '/api/organizations/[id]/connected-accounts/indexing',
+ params: organizationAccountsParamsSchema,
+ body: z
+ .object({
+ optionId: z.string().min(1, 'Provider option is required').max(128),
+ enabled: z.boolean(),
+ })
+ .strict(),
+ response: {
+ mode: 'json',
+ schema: z.object({
+ enabled: z.boolean(),
+ knowledgeBaseIds: z
+ .array(z.string().min(1).max(128))
+ .max(ORGANIZATION_ACCOUNT_INDEXING_SOURCE_LIMIT),
+ }),
+ },
+})
+export type UpdateOrganizationAccountIndexingBody = z.input<
+ NonNullable
+>
+export type EnsureOrganizationAccountsBody = z.input<
+ NonNullable
+>
export type UpdateOrganizationAccountsBody = z.input
+
+export const organizationAccountWorkspaceAccessSchema = z.object({
+ revision: z.number().int().positive(),
+ workspaceIds: z
+ .array(workspaceIdSchema)
+ .max(ORGANIZATION_ACCOUNT_WORKSPACE_LIMIT)
+ .refine((ids) => new Set(ids).size === ids.length, 'Workspace IDs must be unique'),
+})
+export const getOrganizationAccountWorkspaceAccessContract = defineRouteContract({
+ method: 'GET',
+ path: '/api/organizations/[id]/connected-accounts/workspace-access',
+ params: organizationAccountsParamsSchema,
+ response: {
+ mode: 'json',
+ schema: organizationAccountWorkspaceAccessSchema.extend({
+ workspaces: z
+ .array(z.object({ id: workspaceIdSchema, name: z.string().max(256) }))
+ .max(ORGANIZATION_ACCOUNT_WORKSPACE_LIMIT),
+ }),
+ },
+})
+export const updateOrganizationAccountWorkspaceAccessContract = defineRouteContract({
+ method: 'PUT',
+ path: '/api/organizations/[id]/connected-accounts/workspace-access',
+ params: organizationAccountsParamsSchema,
+ body: organizationAccountWorkspaceAccessSchema.strict(),
+ response: { mode: 'json', schema: organizationAccountWorkspaceAccessSchema },
+})
+export type OrganizationAccountWorkspaceAccess = z.output<
+ typeof getOrganizationAccountWorkspaceAccessContract.response.schema
+>
+export type UpdateOrganizationAccountWorkspaceAccessBody = z.input<
+ NonNullable
+>
+
+export const listOrganizationAccountPeopleContract = defineRouteContract({
+ method: 'GET',
+ path: '/api/organizations/[id]/connected-accounts/people',
+ params: organizationAccountsParamsSchema,
+ query: z.object({
+ limit: z.coerce.number().int().min(1).max(100).default(50),
+ cursor: z.string().min(1).max(512).optional(),
+ email: z.string().trim().max(320).optional(),
+ }),
+ response: {
+ mode: 'json',
+ schema: z.object({
+ enrollments: z.array(credentialGroupEnrollmentDetailSchema).max(100),
+ nextCursor: z.string().nullable(),
+ }),
+ },
+})
+export const inviteOrganizationAccountPeopleContract = defineRouteContract({
+ method: 'POST',
+ path: '/api/organizations/[id]/connected-accounts/people',
+ params: organizationAccountsParamsSchema,
+ body: inviteCredentialGroupEnrollmentsBodySchema,
+ response: inviteCredentialGroupEnrollmentsContract.response,
+})
+const organizationAccountEnrollmentParamsSchema = organizationAccountsParamsSchema.extend({
+ enrollmentId: z.string().min(1, 'Enrollment ID is required').max(128),
+})
+export const resendOrganizationAccountInvitationContract = defineRouteContract({
+ method: 'POST',
+ path: '/api/organizations/[id]/connected-accounts/people/[enrollmentId]/resend',
+ params: organizationAccountEnrollmentParamsSchema,
+ response: {
+ mode: 'json',
+ schema: z.object({ credentialGroupEnrollment: credentialGroupEnrollmentSchema }),
+ },
+})
+export const revokeOrganizationAccountEnrollmentContract = defineRouteContract({
+ method: 'DELETE',
+ path: '/api/organizations/[id]/connected-accounts/people/[enrollmentId]',
+ params: organizationAccountEnrollmentParamsSchema,
+ response: {
+ mode: 'json',
+ schema: z.object({ credentialGroupEnrollment: credentialGroupEnrollmentSchema }),
+ },
+})
+export const addOrganizationAccountMcpProviderContract = defineRouteContract({
+ method: 'POST',
+ path: '/api/organizations/[id]/connected-accounts/mcp-providers',
+ params: organizationAccountsParamsSchema,
+ body: createCredentialGroupMcpConnectorBodySchema,
+ response: { mode: 'json', schema: z.object({ mcpServer: credentialGroupMcpServerSchema }) },
+})
+export const removeOrganizationAccountMcpProviderContract = defineRouteContract({
+ method: 'DELETE',
+ path: '/api/organizations/[id]/connected-accounts/mcp-providers/[connectorId]',
+ params: organizationAccountsParamsSchema.extend({ connectorId: managedMcpConnectorIdSchema }),
+ response: { mode: 'json', schema: z.object({ success: z.literal(true) }) },
+})
+export type OrganizationAccountPeoplePage = z.output<
+ typeof listOrganizationAccountPeopleContract.response.schema
+>
+export type OrganizationAccountPeopleQuery = z.input<
+ NonNullable
+>
+export type InviteOrganizationAccountPeopleBody = z.input<
+ NonNullable
+>
+export type AddOrganizationAccountMcpProviderBody = z.input<
+ NonNullable
+>
+export type RemoveOrganizationAccountMcpProviderParams = z.input<
+ NonNullable
+>
+
+export const configureOrganizationMcpContract = defineRouteContract({
+ method: 'PUT',
+ path: '/api/organizations/[id]/connected-accounts/databricks',
+ params: organizationAccountsParamsSchema,
+ body: z
+ .object({
+ url: z.string().trim().min(1, 'Databricks MCP endpoint is required').max(2048).url(),
+ oauthClientId: z.string().trim().min(1, 'OAuth client ID is required').max(2048),
+ oauthClientSecret: z.string().min(1).max(8192).nullable().optional(),
+ name: z.string().trim().min(1).max(256).optional(),
+ })
+ .strict(),
+ response: { mode: 'json', schema: z.object({ mcpServer: credentialGroupMcpServerSchema }) },
+})
+export type ConfigureOrganizationMcpBody = z.input<
+ NonNullable
+>
+
+export const getOrganizationDatabricksSetupContract = defineRouteContract({
+ method: 'GET',
+ path: '/api/organizations/[id]/connected-accounts/databricks',
+ params: organizationAccountsParamsSchema,
+ response: {
+ mode: 'json',
+ schema: z.object({
+ server: z.object({
+ id: z.string().min(1).max(128),
+ name: z.string().min(1).max(256),
+ url: z.string().max(2048).nullable(),
+ oauthClientId: z.string().max(2048).nullable(),
+ hasOauthClientSecret: z.boolean(),
+ enabled: z.boolean(),
+ }),
+ }),
+ },
+})
+export type OrganizationDatabricksSetup = z.output<
+ typeof getOrganizationDatabricksSetupContract.response.schema
+>
+
+export const getWorkspaceOrganizationAccountsContract = defineRouteContract({
+ method: 'GET',
+ path: '/api/workspaces/[id]/organization-accounts',
+ params: z.object({ id: workspaceIdSchema }),
+ response: {
+ mode: 'json',
+ schema: z.object({
+ organizationId: organizationIdSchema.nullable(),
+ organizationName: z.string().nullable(),
+ available: z.boolean(),
+ allowed: z.boolean(),
+ canManage: z.boolean(),
+ providers: z.array(z.object({ id: z.string(), label: z.string() })),
+ mcpProviders: z.array(z.object({ id: z.string(), label: z.string() })),
+ }),
+ },
+})
+export type WorkspaceOrganizationAccounts = z.output<
+ typeof getWorkspaceOrganizationAccountsContract.response.schema
+>
+
+export const personalOrganizationAccountSchema = z.object({
+ credentialId: z.string().min(1).max(128),
+ displayName: z.string(),
+ providerId: z.string().min(1),
+ kind: z.enum(['oauth', 'mcp']),
+ status: z.enum(['active', 'needs_reauth', 'revoked']),
+ organizationId: organizationIdSchema,
+ organizationName: z.string(),
+ enrollmentStatus: z.enum(['invited', 'delivery_failed', 'in_progress', 'completed', 'revoked']),
+ canReconnect: z.boolean(),
+})
+export const listPersonalOrganizationAccountsContract = defineRouteContract({
+ method: 'GET',
+ path: '/api/users/me/organization-accounts',
+ query: z.object({ cursor: z.string().min(1).max(128).optional() }),
+ response: {
+ mode: 'json',
+ schema: z.object({
+ accounts: z.array(personalOrganizationAccountSchema).max(50),
+ nextCursor: z.string().nullable(),
+ }),
+ },
+})
+export const reconnectPersonalOrganizationAccountContract = defineRouteContract({
+ method: 'POST',
+ path: '/api/users/me/organization-accounts/[credentialId]/reconnect',
+ params: z.object({ credentialId: z.string().min(1).max(128) }),
+ response: { mode: 'json', schema: z.object({ invitationLink: z.string().url() }) },
+})
+export const disconnectPersonalOrganizationAccountContract = defineRouteContract({
+ method: 'DELETE',
+ path: '/api/users/me/organization-accounts/[credentialId]',
+ params: z.object({ credentialId: z.string().min(1).max(128) }),
+ response: { mode: 'json', schema: z.object({ success: z.literal(true) }) },
+})
+export type PersonalOrganizationAccount = z.output
diff --git a/apps/sim/lib/copilot/generated/docs-manifest.ts b/apps/sim/lib/copilot/generated/docs-manifest.ts
index ee11d415c70..805780ac4e0 100644
--- a/apps/sim/lib/copilot/generated/docs-manifest.ts
+++ b/apps/sim/lib/copilot/generated/docs-manifest.ts
@@ -366,6 +366,7 @@ export const DOCS_MANIFEST: readonly string[] = [
'logs-debugging.mdx',
'logs-debugging/alerts.mdx',
'logs-debugging/logging.mdx',
+ 'platform/connected-accounts.mdx',
'platform/costs.mdx',
'platform/credentials.mdx',
'platform/enterprise.mdx',
diff --git a/apps/sim/lib/core/config/feature-flags.test.ts b/apps/sim/lib/core/config/feature-flags.test.ts
index 4aa4597245e..16dc6902a84 100644
--- a/apps/sim/lib/core/config/feature-flags.test.ts
+++ b/apps/sim/lib/core/config/feature-flags.test.ts
@@ -202,12 +202,17 @@ describe('isFeatureEnabled', () => {
expect(await isFeatureEnabled('credential-groups')).toBe(true)
})
- it('opens for an allowlisted workspace only', async () => {
- withAppConfig({ 'credential-groups': { workspaceIds: ['ws-1'] } })
- expect(await isFeatureEnabled('credential-groups', { workspaceId: 'ws-1' })).toBe(true)
- expect(await isFeatureEnabled('credential-groups', { workspaceId: 'ws-2' })).toBe(false)
+ it('opens for an allowlisted organization only', async () => {
+ withAppConfig({ 'credential-groups': { orgIds: ['org-1'] } })
+ expect(await isFeatureEnabled('credential-groups', { orgId: 'org-1' })).toBe(true)
+ expect(await isFeatureEnabled('credential-groups', { orgId: 'org-2' })).toBe(false)
expect(await isFeatureEnabled('credential-groups')).toBe(false)
})
+
+ it('a legacy workspace allowlist does not enable the organization gate', async () => {
+ withAppConfig({ 'credential-groups': { workspaceIds: ['ws-1'] } })
+ expect(await isFeatureEnabled('credential-groups', { orgId: 'org-1' })).toBe(false)
+ })
})
it('matches the workspaceIds clause', async () => {
diff --git a/apps/sim/lib/core/config/feature-flags.ts b/apps/sim/lib/core/config/feature-flags.ts
index fb9ce87bb17..4ef2852cbe3 100644
--- a/apps/sim/lib/core/config/feature-flags.ts
+++ b/apps/sim/lib/core/config/feature-flags.ts
@@ -76,20 +76,20 @@ const FEATURE_FLAGS = {
},
'credential-groups': {
description:
- 'Workspace-owned collections that gather managed OAuth credentials from external users. ' +
- 'Gated by workspaceId via AppConfig (or globally); hosted workspaces must also have an ' +
- 'Enterprise subscription. Off-AppConfig falls back to CREDENTIAL_GROUPS.',
+ 'Managed connected accounts, including organization account pools and their settings UI. ' +
+ 'Uses orgId targeting only; workspace callers resolve their canonical organization. Hosted ' +
+ 'owners also require an active Enterprise subscription. Organization Search additionally ' +
+ 'requires knowledge-member-access. Off-AppConfig falls back to CREDENTIAL_GROUPS.',
fallback: 'CREDENTIAL_GROUPS',
},
'knowledge-member-access': {
description:
- 'Permission-aware knowledge bases: lets a workspace admin sync a connector once per ' +
- 'Credential Group member so each person sees only what their own account can read, and ' +
- 'makes hybrid retrieval with a source-recency boost the default for searches in that ' +
- 'workspace. Gated by workspaceId via AppConfig for members mode, which is judged by the ' +
- 'workspace alone; the adminEnabled clause additionally opens the retrieval default to a ' +
- 'platform admin anywhere. Off-AppConfig falls back to KNOWLEDGE_MEMBER_ACCESS. Requires ' +
- 'the credential-groups flag for the connector side to do anything.',
+ 'Permission-aware indexing and retrieval. Organization Search UI, MCP, and search APIs ' +
+ 'require this flag and credential-groups for the canonical orgId; user/admin/workspace ' +
+ 'targeting cannot enable another organization. Workspace member sync uses workspaceId; ' +
+ 'workspace retrieval defaults may additionally use user/admin targeting. Source ACL ' +
+ 'mirroring remains independent of managed identities. Off-AppConfig falls back to ' +
+ 'KNOWLEDGE_MEMBER_ACCESS.',
fallback: 'KNOWLEDGE_MEMBER_ACCESS',
},
} satisfies Record
diff --git a/apps/sim/lib/core/resource-scope.server.ts b/apps/sim/lib/core/resource-scope.server.ts
index 292ab1dc502..aa7423d045d 100644
--- a/apps/sim/lib/core/resource-scope.server.ts
+++ b/apps/sim/lib/core/resource-scope.server.ts
@@ -1,4 +1,4 @@
-import { and, eq, isNull } from 'drizzle-orm'
+import { and, eq, isNull, or } from 'drizzle-orm'
import type { PgColumn } from 'drizzle-orm/pg-core'
import type { ResourceScope } from '@/lib/core/resource-scope'
@@ -11,3 +11,22 @@ export function resourceScopeCondition(
? and(eq(table.workspaceId, scope.workspaceId), isNull(table.organizationId))!
: and(eq(table.organizationId, scope.organizationId), isNull(table.workspaceId))!
}
+
+/** Joins resources only when both have the same unambiguous owner. */
+export function sameResourceScopeCondition(
+ left: { workspaceId: PgColumn; organizationId: PgColumn },
+ right: { workspaceId: PgColumn; organizationId: PgColumn }
+) {
+ return or(
+ and(
+ eq(left.workspaceId, right.workspaceId),
+ isNull(left.organizationId),
+ isNull(right.organizationId)
+ ),
+ and(
+ eq(left.organizationId, right.organizationId),
+ isNull(left.workspaceId),
+ isNull(right.workspaceId)
+ )
+ )!
+}
diff --git a/apps/sim/lib/credential-groups/README.md b/apps/sim/lib/credential-groups/README.md
new file mode 100644
index 00000000000..69e29c08454
--- /dev/null
+++ b/apps/sim/lib/credential-groups/README.md
@@ -0,0 +1,99 @@
+# Organization connected accounts
+
+An organization has at most one account pool. Owners and admins manage it in **Organization settings → Connected accounts**, with Providers, People, and Workspace access tabs. The initial workspace allowlist is empty. Personal account settings show the signed-in person’s own contributions, including contributions to organizations they have not joined.
+
+Organization settings selects one setup page: **Connected accounts** when `credential-groups` is available and `knowledge-member-access` is off, or the Search **Integrations** page when both are available. The two pages remain separate; the inactive page is hidden from navigation and returns not found on direct access. This switches the settings UI without changing credential-group API access or stored workspace policies.
+
+Both pages include **People → Request connections**, with the existing manual email invitation, Resend, and Revoke actions. Search-enabled orgs open **Settings → Integrations → People**; Providers remains the default tab. If no org credential group exists, People directs the admin to provider setup first. Search approval alone does not configure a provider for account invitations.
+
+## Permissions
+
+An allowed workspace grants every normally authorized manual and deployed workflow access to every active contribution in this pool. There is no per-workflow resource-policy grant and no per-person filtering for workflow execution. Keep ordinary workspace/workflow authorization and deployment authority: an allowlist entry alone cannot authorize running a workflow. Nested workflows use their actual execution workspace. A workspace move, revocation, inactive enrollment, removed provider, disabled group, or unavailable org entitlement blocks subsequent use.
+
+Standalone Chat uses the signed-in person’s own connections. Invited contributors do not need organization membership. Redemption requires a verified matching Sim email; the enrollment is then bound permanently to that user ID. An email change cannot transfer an enrollment. OAuth callbacks require the same verified signed-in user who started authorization. Search requires current organization membership and applies document permissions using the viewer's own verified provider identities; workspace access to the shared credential pool does not grant access to other people's indexed documents.
+
+Disconnect revokes the local grant and invalidates pending invitation-based authorization. Administrators can revoke an enrollment; the person cannot restore it themselves. Removing workspace access stops future authorized calls, but cannot recall a provider request already in flight or erase data already returned to a workflow. Full-pool sharing includes public, scheduled, and webhook deployments that otherwise pass workflow authorization.
+
+## Credential block
+
+| Operation | Inputs | Output |
+| --- | --- | --- |
+| Select Credential | Workspace OAuth credential | Existing credential reference |
+| List Credentials | Optional workspace provider filter | Existing reference list |
+| Find Organization Account | Email and OAuth provider | Exactly one reference, otherwise an error |
+| List Organization Accounts | Optional email/providers, limit/cursor | Bounded reference page |
+| Find Organization MCP Connection | Email and MCP provider | Exactly one personal MCP reference, otherwise an error |
+| List Organization MCP Connections | Optional email/provider, limit/cursor | Bounded MCP reference page |
+
+MCP `credentialId` identifies the person’s connection (`mcp-cg-…`) and is used to select the connection in the MCP block. `mcpServerId` identifies shared configuration and does not grant access to a person’s token. The block never returns secrets or invitation links. Org operations are hidden in ineligible workspaces; saved invalid configurations fail during execution.
+
+Credential trigger mode supports `credential_added`, `credential_reconnected`, and `form_submitted`. Events go to opted-in, currently deployed Credential triggers in allowed workspaces. Legacy Credential Group blocks are hidden and fail with an explicit replacement instruction; they are not automatically rebound.
+
+## Providers
+
+The Providers tab lists only added providers. **Add provider** opens a searchable catalog with the remaining providers. Required configuration is completed before adding a provider; **Configure** reopens Slack or Databricks settings directly. Providers without required setup are added directly. Providers have **Remove** in their actions menu. Individual account connections remain in People. Connected accounts does not display indexing status, load Search sources, or open indexing setup.
+
+- Fireflies and Granola: an admin adds the provider; Sim supplies the fixed hosted MCP endpoint and dynamic client registration. Each person completes their own OAuth authorization.
+- Databricks: **Add** collects and validates the organization's tenant MCP URL and registered OAuth client before creating an enabled provider in one transaction. Cancelling leaves nothing added. Organization owners and admins create it through `POST /api/organizations/[id]/connected-accounts/mcp-providers` and read or edit settings through `GET` / `PUT /api/organizations/[id]/connected-accounts/databricks`; no workspace configuration is used. Unfinished entries from the earlier flow appear in the Add catalog until their configuration is saved. The form never reads stored client secrets, and leaving the secret blank when editing preserves it. Endpoint/client identity changes invalidate affected grants and pending attempts, requiring people to reconnect.
+- Slack personal OAuth: the org admin supplies App ID, Slack workspace ID, client ID, and client secret, then verifies authorization. The org has one configured app/workspace. Existing workspace bots and their triggers remain separate.
+
+Search approval and source setup are managed through **Organization settings → Integrations**. Approval alone does not create a credential or start indexing. Support for indexing connected accounts comes from the existing Search connector registry's permission-scoped OAuth ingestion capabilities: Gmail, Google Drive, Google Calendar, GitHub repositories, Jira, Confluence, and Slack. Source setup collects any required repository, domain, space, or other settings. Reconnecting an OAuth account queues the existing active sources for that option; it never enables paused indexing.
+
+Both `CREDENTIAL_GROUPS` and `KNOWLEDGE_MEMBER_ACCESS` must be enabled locally. Hosted deployments additionally enforce the routed org's feature rules and Enterprise availability. Owners and admins manage indexing; existing Knowledge permission-group rules still apply. Managed MCP account connections remain available for live tool calls only and have no indexing switch. Separate API-key KB connectors for Fireflies, Granola, and Databricks do not consume these managed MCP connections.
+
+### Feature gates
+
+| Surface or behavior | Required organization flags |
+| --- | --- |
+| Connected accounts settings page | `credential-groups` enabled, `knowledge-member-access` disabled |
+| Provider setup APIs, personal contributions, workspace access to the pool | `credential-groups` |
+| Indexing On, member sync, organization Home/Assistant and chat pages, Integrations source setup, Search MCP settings | `credential-groups` and `knowledge-member-access` |
+| Organization Search MCP endpoint and organization knowledge search through internal/public APIs or trusted tools | `credential-groups` and `knowledge-member-access`, checked after current authorization |
+
+The Search gate uses the persisted knowledge base owner or the authenticated route's target organization. User, platform-admin, and workspace targeting cannot opt a different organization into Search. Disabled organizations receive `403 Search is not enabled for this organization` before index lookup or model execution; hiding navigation is not the authorization boundary. Organization Home, Search, and chat URLs open full settings in the viewer's most recent accessible workspace when Search is disabled. Default app entry uses that same destination, and Home, Integrations, chat history, and Assistant loading UI are hidden. Connected accounts settings and Workspaces remain available. Settings and source-setup URLs also enforce their gates. Ordinary workspace knowledge search keeps its existing behavior. Pausing configured indexing remains available to authorized org admins when the Search flag is off.
+
+For a targeted hosted rollout, configure both existing flags in AppConfig's `feature-flags` document:
+
+```json
+{
+ "credential-groups": { "enabled": false, "orgIds": ["org-to-enable"] },
+ "knowledge-member-access": { "enabled": false, "orgIds": ["org-to-enable"] }
+}
+```
+
+`enabled: true` enables a flag globally; it is not needed alongside an org allowlist. Off AppConfig, `CREDENTIAL_GROUPS=true` and `KNOWLEDGE_MEMBER_ACCESS=true` are deployment-wide switches and cannot target individual organizations. Both flag checks still apply the organization's hosted Enterprise/billing requirements and normal membership, permission-group, and document access checks. These examples document configuration only; this change does not update a deployed AppConfig document.
+
+Credential-groups rollout never evaluates `workspaceIds`. Existing workspace-scoped callers resolve their owning organization and use its `orgId`; personal workspaces cannot enable connected accounts. This flag rollout is separate from the organization's workspace access allowlist, which still controls which workflows may use the pool. Normal settings no longer prefetch the legacy workspace-owned account container.
+
+Current bounds: 100 entries per discovery page, 1,000 workspace allowlist entries, and 1,000 deployed event subscriptions per organization. Event delivery is synchronous after enrollment commits; a delivery failure surfaces as an error and does not roll back the saved connection. An outbox/retry mechanism is not included.
+
+## Rollout
+
+OAuth attempt state changes at this release boundary (OAuth v5 and managed MCP v3). Older attempts lack a verified Sim user binding; older MCP attempts also lack the configuration version. They are deliberately rejected before token exchange, with an explicit instruction to reopen the invitation and connect again. Existing saved credentials are not invalidated by the state version change. Mixed application versions cannot complete each other's in-flight attempts: pause enrollment starts, allow the ten-minute state lifetime to drain, replace the application instances together, and only then reopen enrollment and enable the org rollout. Do not run enrollment OAuth across mixed versions or roll back with active attempts.
+
+1. Apply `0327_organization_connected_accounts.sql` before deploying code that reads the new columns. It expands ownership columns and checks, adds stable enrollment identity and MCP configuration versions, and builds indexes concurrently. No grants, enrollments, or Search data are moved or deleted. Constraints are added `NOT VALID` to avoid scanning existing tables while holding the DDL lock; validate them separately after auditing existing rows.
+2. Inventory existing groups and their Search dependencies before enabling the feature. The queries below read IDs/counts only. Review archived/deleted sources too because a reset must account for retained documents and cleanup work.
+3. Existing org groups without the new v2 workspace policy stop with a migration-review error. Do not insert a v2 policy over legacy contributions. Resolve Search dependencies explicitly, retire the old group through an audited maintenance procedure, create a fresh org pool, and invite people to reconnect. No reset command is supplied or run by this change.
+4. Enable the existing `credential-groups` feature flag for the target org (`orgIds`), then set up providers and allow specific same-org workspaces. A previous workspace-only feature-flag allowlist does not enable the org surface. Sim Cloud also requires an active Enterprise entitlement.
+5. Replace legacy workflow blocks, reconfigure credential references, and redeploy event subscribers. Verify one manual run, one deployed run, and one revocation before widening the workspace allowlist.
+
+```sql
+SELECT cg.id, cg.organization_id, cg.workspace_id,
+ (SELECT count(*) FROM credential_group_enrollment e
+ WHERE e.credential_group_id = cg.id) AS enrollments,
+ (SELECT count(*) FROM credential c
+ JOIN credential_group_enrollment e ON e.id = c.credential_group_enrollment_id
+ WHERE e.credential_group_id = cg.id) AS credentials,
+ (SELECT count(*) FROM knowledge_connector kc
+ WHERE kc.credential_group_id = cg.id) AS search_dependencies
+FROM credential_group cg
+ORDER BY cg.id;
+
+SELECT kc.id AS connector_id, kc.credential_group_id, kc.knowledge_base_id,
+ kc.access_mode, kc.deleted_at
+FROM knowledge_connector kc
+WHERE kc.credential_group_id IS NOT NULL
+ORDER BY kc.credential_group_id, kc.id;
+```
+
+The migration is rerunnable after partial completion. If a concurrent index build leaves an invalid index, the final check fails with its name; repair that index explicitly before retrying. Keep the flag off during rollback. Earlier app versions do not understand the org sharing policy or stable enrollment identity; rollback does not transfer new org grants back to workspaces.
diff --git a/apps/sim/lib/credential-groups/application/authorization.test.ts b/apps/sim/lib/credential-groups/application/authorization.test.ts
index 5522e1402b9..25b2d806c86 100644
--- a/apps/sim/lib/credential-groups/application/authorization.test.ts
+++ b/apps/sim/lib/credential-groups/application/authorization.test.ts
@@ -4,13 +4,14 @@
import type { DelegatedPrincipal, WorkflowExecutionDelegatedPrincipal } from '@sim/auth/principal'
import { beforeEach, describe, expect, it, vi } from 'vitest'
-import { compileCredentialGroupWorkflowAccessPolicy } from '@/lib/credential-groups/application/workflow-access-policy'
+import { buildOrganizationAccountAccessPolicy } from '@/lib/credential-groups/application/workspace-access-policy'
import { credentialOperations } from '@/lib/credentials/application/operations'
const mocks = vi.hoisted(() => ({
loadEnrollmentAccess: vi.fn(),
loadBinding: vi.fn(),
requirePolicy: vi.fn(),
+ isAvailable: vi.fn(),
}))
vi.mock('@/lib/credential-groups/credentials', () => ({
@@ -28,6 +29,10 @@ vi.mock('@/lib/credential-groups/credentials', () => ({
binding.optionStatus === 'active',
}))
+vi.mock('@/lib/credential-groups/scoped-availability', () => ({
+ isScopedCredentialGroupsAvailable: mocks.isAvailable,
+}))
+
vi.mock('@/lib/resource-policies/repository', () => ({
requireResourcePolicy: mocks.requirePolicy,
}))
@@ -39,7 +44,8 @@ import {
const context = {
workspaceId: 'workspace-1',
- workspaceOrganizationId: null,
+ workspaceOrganizationId: 'org-1',
+ organizationId: 'org-1',
allowPersonalApiKeys: true,
credentialId: 'credential-1',
credentialGroupId: 'group-1',
@@ -58,17 +64,12 @@ const liveBinding = {
optionStatus: 'active',
}
-function storedPolicy(allowedWorkflowIds: string[] = []) {
+function storedPolicy(workspaceIds: string[] = ['workspace-1']) {
return {
id: 'policy-1',
- workspaceId: 'workspace-1',
+ organizationId: 'org-1',
revision: 1,
- document: compileCredentialGroupWorkflowAccessPolicy({
- credentialGroupId: 'group-1',
- allowedWorkflowIds,
- }),
- createdAt: new Date('2026-08-20T00:00:00.000Z'),
- updatedAt: new Date('2026-08-20T00:00:00.000Z'),
+ document: buildOrganizationAccountAccessPolicy('group-1', workspaceIds),
}
}
@@ -132,6 +133,7 @@ function requireAccess(principal: DelegatedPrincipal, accessContext = context):
describe('requireCredentialGroupCredentialAccess', () => {
beforeEach(() => {
vi.clearAllMocks()
+ mocks.isAvailable.mockResolvedValue(true)
mocks.requirePolicy.mockResolvedValue(storedPolicy())
mocks.loadEnrollmentAccess.mockResolvedValue({
enrollmentId: 'enrollment-1',
@@ -172,8 +174,8 @@ describe('requireCredentialGroupCredentialAccess', () => {
).rejects.toMatchObject({ code: 'forbidden' })
})
- it('denies a Chat turn whose user holds no live enrollment, even for an allowlisted workflow', async () => {
- mocks.requirePolicy.mockResolvedValue(storedPolicy(['workflow-1']))
+ it('denies a Chat turn whose user holds no live enrollment, even in an allowlisted workspace', async () => {
+ mocks.requirePolicy.mockResolvedValue(storedPolicy())
mocks.loadEnrollmentAccess.mockResolvedValue(null)
await expect(requireAccess(copilotPrincipal())).rejects.toMatchObject({ code: 'forbidden' })
@@ -187,32 +189,20 @@ describe('requireCredentialGroupCredentialAccess', () => {
expect(mocks.loadEnrollmentAccess).not.toHaveBeenCalled()
})
- it('allows an external actor to use only their own enrollment', async () => {
- const principal = executorPrincipal()
-
- await expect(requireAccess(principal)).resolves.toBeUndefined()
- expect(mocks.requirePolicy).toHaveBeenCalledWith({
- workspaceId: 'workspace-1',
- resourceType: 'credential_group',
- resourceId: 'group-1',
- codec: expect.objectContaining({ resourceType: 'credential_group' }),
- })
- expect(mocks.loadEnrollmentAccess).toHaveBeenCalledWith('group-1', {
- kind: 'external_user',
- provider: 'slack',
- tenantId: 'T123',
- subjectId: 'U123',
- })
-
+ it('allows an external actor to use any live contributed credential in an allowed workspace', async () => {
await expect(
- requireAccess(principal, {
+ requireAccess(executorPrincipal(), {
...context,
- credentialGroupEnrollmentId: 'enrollment-2',
+ credentialGroupEnrollmentId: 'someone-else',
})
- ).rejects.toMatchObject({ code: 'forbidden' })
+ ).resolves.toBeUndefined()
+ expect(mocks.loadEnrollmentAccess).not.toHaveBeenCalled()
+ expect(mocks.requirePolicy).toHaveBeenCalledWith(
+ expect.objectContaining({ organizationId: 'org-1', resourceId: 'group-1' })
+ )
})
- it('allows a Sim actor to use their own enrollment', async () => {
+ it('allows a Sim actor without a personal enrollment', async () => {
const principal = executorPrincipal()
principal.subjectUserId = 'user-1'
principal.delegationContext!.principal = {
@@ -220,15 +210,12 @@ describe('requireCredentialGroupCredentialAccess', () => {
userId: 'user-1',
sessionId: 'session-1',
}
-
+ mocks.loadEnrollmentAccess.mockResolvedValue(null)
await expect(requireAccess(principal)).resolves.toBeUndefined()
- expect(mocks.loadEnrollmentAccess).toHaveBeenCalledWith('group-1', {
- kind: 'sim_user',
- userId: 'user-1',
- })
+ expect(mocks.loadEnrollmentAccess).not.toHaveBeenCalled()
})
- it('allows an actorless deployed workflow only when its current workflow is allowlisted', async () => {
+ it('allows an actorless deployed workflow without a per-workflow allowlist', async () => {
const principal = executorPrincipal()
principal.delegationContext!.principal = {
kind: 'system',
@@ -236,35 +223,36 @@ describe('requireCredentialGroupCredentialAccess', () => {
workspaceId: 'workspace-1',
workflowId: 'root-workflow',
}
- mocks.requirePolicy.mockResolvedValue(storedPolicy(['workflow-1']))
-
await expect(requireAccess(principal)).resolves.toBeUndefined()
expect(mocks.loadEnrollmentAccess).not.toHaveBeenCalled()
-
- principal.delegationContext!.currentWorkflow = { workflowId: 'workflow-1', mode: 'draft' }
- await expect(requireAccess(principal)).rejects.toMatchObject({
- code: 'forbidden',
- })
+ mocks.requirePolicy.mockResolvedValue(storedPolicy([]))
+ await expect(requireAccess(principal)).rejects.toMatchObject({ code: 'forbidden' })
})
- it('uses the current child workflow rather than the root workflow grant', async () => {
+ it('denies a child workflow in a different or non-allowlisted workspace', async () => {
const principal = executorPrincipal()
- principal.delegationContext!.principal = {
- kind: 'system',
- serviceId: 'schedule',
- workspaceId: 'workspace-1',
- workflowId: 'root-workflow',
- }
principal.delegationContext!.currentWorkflow = {
workflowId: 'child-workflow',
mode: 'deployment',
deploymentVersionId: 'child-version',
}
- mocks.requirePolicy.mockResolvedValue(storedPolicy(['root-workflow']))
+ await expect(
+ requireAccess(principal, { ...context, workspaceId: 'child-workspace' })
+ ).rejects.toMatchObject({ code: 'forbidden' })
+ await expect(
+ requireAccess(principal, { ...context, workspaceOrganizationId: 'other-org' })
+ ).rejects.toMatchObject({ code: 'forbidden' })
+ })
- await expect(requireAccess(principal)).rejects.toMatchObject({
- code: 'forbidden',
- })
+ it('rejects workspace-owned legacy workflow credentials with a reconnect instruction', async () => {
+ await expect(
+ requireAccess(executorPrincipal(), { ...context, organizationId: undefined })
+ ).rejects.toThrow('Reconnect this account')
+ })
+
+ it('rechecks the org feature flag before credential use', async () => {
+ mocks.isAvailable.mockResolvedValue(false)
+ await expect(requireAccess(executorPrincipal())).rejects.toMatchObject({ code: 'not_found' })
})
it('rejects inconsistent Sim and external subject assertions before loading policy', async () => {
diff --git a/apps/sim/lib/credential-groups/application/authorization.ts b/apps/sim/lib/credential-groups/application/authorization.ts
index 33df6dd3e72..13a51696fd6 100644
--- a/apps/sim/lib/credential-groups/application/authorization.ts
+++ b/apps/sim/lib/credential-groups/application/authorization.ts
@@ -10,10 +10,10 @@ import type {
WorkspaceDelegationPolicy,
} from '@/lib/core/application'
import { OrchestrationError } from '@/lib/core/orchestration/types'
+import { requireOrganizationAccountsWorkspaceAccess } from '@/lib/credential-groups/application/organization-workspace-access'
import {
credentialGroupWorkflowAccessPolicyCodec,
evaluateCredentialGroupActorCredentialAccess,
- evaluateCredentialGroupWorkflowAccess,
} from '@/lib/credential-groups/application/workflow-access-policy'
import type {
CredentialGroupCredentialListContext,
@@ -41,6 +41,7 @@ export const workspaceAccountsSettingsDelegationPolicy = {
export interface CredentialGroupAuthorizationContext extends WorkspaceAuthorizationContext {
credentialGroupId: string
+ organizationId?: string
}
export interface CredentialGroupApplicationContext
@@ -98,6 +99,7 @@ function requireConsistentWorkflowSubject(
* user simply records none.
*/
export function requireCredentialGroupWorkflowActor(principal: Principal): PrincipalSubject | null {
+ requireCurrentWorkflow(principal)
return requireConsistentWorkflowSubject(principal, requireWorkflowExecutionPrincipal(principal))
}
@@ -122,6 +124,19 @@ async function requireCredentialGroupActorCredentialAccess(
if (!binding) {
throw new OrchestrationError('forbidden', 'Credential Group credential access denied')
}
+ if (context.organizationId) {
+ const actorAccess = await loadCredentialGroupEnrollmentAccessForSubject(
+ context.credentialGroupId,
+ subject
+ )
+ if (actorAccess?.enrollmentId !== context.credentialGroupEnrollmentId) {
+ throw new OrchestrationError(
+ 'forbidden',
+ 'Only your own organization connections can be used in Chat'
+ )
+ }
+ return
+ }
const [policy, actorAccess] = await Promise.all([
requireResourcePolicy({
workspaceId: context.workspaceId,
@@ -167,29 +182,18 @@ export async function requireCredentialGroupCredentialAccess(
if (principal.kind === 'delegated' && principal.serviceId === 'copilot') {
return requireCredentialGroupActorCredentialAccess(principal, context, binding, resourcePolicy)
}
- const executionPrincipal = requireWorkflowExecutionPrincipal(principal)
- const currentWorkflow = requireCurrentWorkflow(principal)
- const subject = requireConsistentWorkflowSubject(principal, executionPrincipal)
- const policy = await requireResourcePolicy({
- workspaceId: context.workspaceId,
- resourceType: 'credential_group',
- resourceId: context.credentialGroupId,
- codec: credentialGroupWorkflowAccessPolicyCodec,
- })
- const actorAccess = subject
- ? await loadCredentialGroupEnrollmentAccessForSubject(context.credentialGroupId, subject)
- : null
- const decision = evaluateCredentialGroupWorkflowAccess({
- document: policy.document,
- credentialGroupId: context.credentialGroupId,
- selectedEnrollmentId: context.credentialGroupEnrollmentId,
- ...(actorAccess ? { actorEnrollmentId: actorAccess.enrollmentId } : {}),
- currentWorkflow,
- resourcePolicy,
- })
- if (decision.decision !== 'allow') {
- throw new OrchestrationError('forbidden', 'Credential Group credential access denied')
+ requireCredentialGroupWorkflowActor(principal)
+ requireCurrentWorkflow(principal)
+ if (!context.organizationId) {
+ throw new OrchestrationError(
+ 'forbidden',
+ 'Reconnect this account in organization settings and replace the legacy Connected Accounts block'
+ )
}
+ await requireOrganizationAccountsWorkspaceAccess({
+ ...context,
+ organizationId: context.organizationId,
+ })
}
export const credentialGroupDelegationPolicy = {
diff --git a/apps/sim/lib/credential-groups/application/configure-organization-mcp.test.ts b/apps/sim/lib/credential-groups/application/configure-organization-mcp.test.ts
new file mode 100644
index 00000000000..5dd78c484fc
--- /dev/null
+++ b/apps/sim/lib/credential-groups/application/configure-organization-mcp.test.ts
@@ -0,0 +1,241 @@
+/** @vitest-environment node */
+import * as audit from '@sim/audit'
+import type { SessionPrincipal } from '@sim/auth/principal'
+import { queueTableRows, resetDbChainMock, schemaMock } from '@sim/testing'
+import { eq } from 'drizzle-orm'
+import { beforeEach, describe, expect, it, vi } from 'vitest'
+
+const mocks = vi.hoisted(() => ({
+ available: vi.fn(),
+ group: vi.fn(),
+ setup: vi.fn(),
+ read: vi.fn(),
+ create: vi.fn(),
+ update: vi.fn(),
+ clear: vi.fn(),
+ evict: vi.fn(),
+}))
+vi.mock('@/lib/credential-groups/scoped-availability', () => ({
+ isScopedCredentialGroupsAvailable: mocks.available,
+}))
+vi.mock('@/lib/credential-groups/credentials', () => ({
+ loadScopedAccountsCredentialListContext: mocks.group,
+}))
+vi.mock('@/lib/credential-groups/organization-setup', () => ({
+ requireOrganizationAccountsSetup: mocks.setup,
+}))
+vi.mock('@/lib/permission-groups/resolve.server', () => ({
+ getUserPermissionConfigForOrganization: vi.fn().mockResolvedValue(null),
+}))
+vi.mock('@/lib/credential-groups/service', () => ({
+ ensureWorkspaceAccountsGroup: vi.fn(),
+ getOrganizationAccountsGroup: vi.fn(),
+ updateCredentialGroup: vi.fn(),
+}))
+vi.mock('@/lib/credential-groups/provider-availability', () => ({
+ listConfiguredCredentialGroupProviders: vi.fn(),
+}))
+vi.mock('@/lib/credential-groups/self-enrollment', () => ({
+ createViewerCredentialGroupEnrollment: vi.fn(),
+}))
+vi.mock('@/lib/credential-groups/managed-mcp-service', () => ({
+ loadOrganizationDatabricksSetup: mocks.read,
+ createManagedMcpConnector: mocks.create,
+ updateManagedMcpConnector: mocks.update,
+ ManagedMcpConnectorError: class extends Error {
+ constructor(
+ message: string,
+ public code: string
+ ) {
+ super(message)
+ }
+ },
+}))
+vi.mock('@/lib/credential-groups/mcp-oauth-state', () => ({
+ clearCredentialGroupMcpOAuthAttempts: mocks.clear,
+}))
+vi.mock('@/lib/mcp/connection-pool', () => ({ evictMcpServerConnections: mocks.evict }))
+
+import { configureOrganizationMcp } from '@/lib/credential-groups/application/configure-organization-mcp'
+import { addOrganizationAccountMcpProvider } from '@/lib/credential-groups/application/organization-account-management'
+import { getOrganizationDatabricksSetup } from '@/lib/credential-groups/application/organization-databricks-setup'
+import { ManagedMcpConnectorError } from '@/lib/credential-groups/managed-mcp-service'
+
+const principal: SessionPrincipal = {
+ kind: 'session',
+ userId: 'admin-user',
+ sessionId: 'session-1',
+}
+const input = {
+ organizationId: 'customer-org',
+ connectorId: 'databricks' as const,
+ name: 'Databricks',
+ url: 'https://tenant.cloud.databricks.com/api/2.0/mcp/sql',
+ oauthClientId: 'registered-client',
+}
+const server = {
+ id: 'server-new',
+ name: 'Databricks',
+ url: input.url,
+ oauthClientId: input.oauthClientId,
+ hasOauthClientSecret: true,
+ enabled: true,
+}
+
+describe('organization Databricks setup', () => {
+ beforeEach(() => {
+ vi.clearAllMocks()
+ vi.spyOn(audit, 'recordAudit').mockImplementation(() => {})
+ resetDbChainMock()
+ mocks.available.mockResolvedValue(true)
+ mocks.group.mockResolvedValue({ credentialGroupId: 'customer-group' })
+ mocks.setup.mockResolvedValue(undefined)
+ mocks.read.mockResolvedValue(server)
+ mocks.create.mockResolvedValue({
+ mcpServer: { id: 'server-new', name: 'Databricks', enabled: true },
+ resetMcpServerIds: [],
+ retiredMcpConnectionIds: [],
+ })
+ mocks.update.mockResolvedValue({
+ mcpServer: { id: 'server-new', name: 'Databricks' },
+ resetMcpServerIds: ['server-old'],
+ retiredMcpConnectionIds: ['mcp-cg-old'],
+ })
+ })
+
+ describe.each([
+ ['read', getOrganizationDatabricksSetup],
+ ['configure', configureOrganizationMcp],
+ ['add', addOrganizationAccountMcpProvider],
+ ] as const)('%s authorization', (_name, useCase) => {
+ it.each(['member', null])('refuses a %s before reading configuration', async (role) => {
+ queueTableRows(schemaMock.member, role ? [{ role }] : [])
+ await expect(useCase.execute({ principal, input })).rejects.toThrow()
+ expect(mocks.group).not.toHaveBeenCalled()
+ expect(mocks.read).not.toHaveBeenCalled()
+ expect(mocks.update).not.toHaveBeenCalled()
+ expect(mocks.create).not.toHaveBeenCalled()
+ })
+
+ it.each(['admin', 'owner'])(
+ 'allows an organization %s using the routed organization',
+ async (role) => {
+ queueTableRows(schemaMock.member, [{ role }])
+ await useCase.execute({ principal, input })
+ expect(eq).toHaveBeenCalledWith(schemaMock.member.userId, 'admin-user')
+ expect(eq).toHaveBeenCalledWith(schemaMock.member.organizationId, 'customer-org')
+ expect(mocks.group).toHaveBeenCalledWith({
+ kind: 'organization',
+ organizationId: 'customer-org',
+ })
+ }
+ )
+
+ it('refuses setup when the feature is unavailable', async () => {
+ queueTableRows(schemaMock.member, [{ role: 'admin' }])
+ mocks.available.mockResolvedValue(false)
+ await expect(useCase.execute({ principal, input })).rejects.toMatchObject({
+ code: 'not_found',
+ })
+ expect(mocks.read).not.toHaveBeenCalled()
+ expect(mocks.update).not.toHaveBeenCalled()
+ expect(mocks.create).not.toHaveBeenCalled()
+ })
+
+ it('requires the organization connected accounts group', async () => {
+ queueTableRows(schemaMock.member, [{ role: 'admin' }])
+ mocks.group.mockResolvedValue(null)
+ await expect(useCase.execute({ principal, input })).rejects.toMatchObject({
+ code: 'not_found',
+ })
+ expect(mocks.read).not.toHaveBeenCalled()
+ expect(mocks.update).not.toHaveBeenCalled()
+ expect(mocks.create).not.toHaveBeenCalled()
+ })
+ })
+
+ it('loads setup metadata for the organization without returning a client secret', async () => {
+ queueTableRows(schemaMock.member, [{ role: 'admin' }])
+ const result = await getOrganizationDatabricksSetup.execute({ principal, input })
+ expect(mocks.read).toHaveBeenCalledWith('customer-org', 'customer-group')
+ expect(result).toEqual({ server })
+ expect(result.server).not.toHaveProperty('oauthClientSecret')
+ })
+
+ it('updates only the organization provider and invalidates affected connections', async () => {
+ queueTableRows(schemaMock.member, [{ role: 'admin' }])
+ await configureOrganizationMcp.execute({ principal, input })
+ expect(mocks.update).toHaveBeenCalledWith({
+ organizationId: 'customer-org',
+ credentialGroupId: 'customer-group',
+ connectorId: 'databricks',
+ input: {
+ url: input.url,
+ oauthClientId: input.oauthClientId,
+ oauthClientSecret: undefined,
+ name: 'Databricks',
+ },
+ })
+ expect(audit.recordAudit).toHaveBeenCalledWith(
+ expect.objectContaining({
+ actorId: 'admin-user',
+ resourceId: 'customer-group',
+ metadata: { organizationId: 'customer-org' },
+ })
+ )
+ expect(mocks.clear).toHaveBeenCalledWith(['server-old'])
+ expect(mocks.evict).toHaveBeenCalledWith('server-old', expect.any(String))
+ expect(mocks.evict).toHaveBeenCalledWith('mcp-cg-old', expect.any(String))
+ })
+
+ it('surfaces configuration failures without auditing success or evicting connections', async () => {
+ queueTableRows(schemaMock.member, [{ role: 'admin' }])
+ mocks.update.mockRejectedValue(
+ new ManagedMcpConnectorError('Databricks has not been added', 'not_found')
+ )
+ await expect(configureOrganizationMcp.execute({ principal, input })).rejects.toMatchObject({
+ code: 'not_found',
+ })
+ expect(audit.recordAudit).not.toHaveBeenCalled()
+ expect(mocks.clear).not.toHaveBeenCalled()
+ expect(mocks.evict).not.toHaveBeenCalled()
+ })
+
+ it('creates the fully configured provider with canonical organization scope in one mutation', async () => {
+ queueTableRows(schemaMock.member, [{ role: 'admin' }])
+ const result = await addOrganizationAccountMcpProvider.execute({ principal, input })
+ expect(mocks.create).toHaveBeenCalledExactlyOnceWith({
+ organizationId: 'customer-org',
+ credentialGroupId: 'customer-group',
+ userId: 'admin-user',
+ input: {
+ connectorId: 'databricks',
+ name: 'Databricks',
+ url: input.url,
+ oauthClientId: input.oauthClientId,
+ },
+ })
+ expect(result.mcpServer.enabled).toBe(true)
+ expect(mocks.update).not.toHaveBeenCalled()
+ expect(audit.recordAudit).toHaveBeenCalledWith(
+ expect.objectContaining({
+ actorId: 'admin-user',
+ resourceId: 'customer-group',
+ metadata: { organizationId: 'customer-org' },
+ })
+ )
+ })
+
+ it('rejects invalid Databricks creation without auditing success or trying a second mutation', async () => {
+ queueTableRows(schemaMock.member, [{ role: 'admin' }])
+ mocks.create.mockRejectedValue(new ManagedMcpConnectorError('Invalid MCP URL', 'validation'))
+ await expect(
+ addOrganizationAccountMcpProvider.execute({ principal, input })
+ ).rejects.toMatchObject({
+ code: 'validation',
+ message: 'Invalid MCP URL',
+ })
+ expect(audit.recordAudit).not.toHaveBeenCalled()
+ expect(mocks.update).not.toHaveBeenCalled()
+ })
+})
diff --git a/apps/sim/lib/credential-groups/application/configure-organization-mcp.ts b/apps/sim/lib/credential-groups/application/configure-organization-mcp.ts
new file mode 100644
index 00000000000..5251a0e8f6b
--- /dev/null
+++ b/apps/sim/lib/credential-groups/application/configure-organization-mcp.ts
@@ -0,0 +1,69 @@
+import type { OrganizationMembershipContext } from '@/lib/core/application/organization-authorization'
+import { defineOrganizationOperation } from '@/lib/core/application/organization-operation'
+import { OrchestrationError } from '@/lib/core/orchestration/types'
+import { defineOrganizationAccountsUseCase } from '@/lib/credential-groups/application/organization-accounts'
+import { loadScopedAccountsCredentialListContext } from '@/lib/credential-groups/credentials'
+import { updateManagedMcpConnector } from '@/lib/credential-groups/managed-mcp-service'
+import { clearCredentialGroupMcpOAuthAttempts } from '@/lib/credential-groups/mcp-oauth-state'
+import { evictMcpServerConnections } from '@/lib/mcp/connection-pool'
+
+export const configureOrganizationMcpOperation = defineOrganizationOperation({
+ id: 'organization_accounts.mcp.configure',
+ minimumRole: 'admin',
+ principalKinds: ['session'],
+ capability: 'integrations.manage',
+})
+
+export interface ConfigureOrganizationMcpInput {
+ organizationId: string
+ url: string
+ oauthClientId: string
+ oauthClientSecret?: string | null
+ name?: string
+}
+
+export const configureOrganizationMcp = defineOrganizationAccountsUseCase({
+ operation: configureOrganizationMcpOperation,
+ async execute({
+ input,
+ context,
+ }: {
+ input: ConfigureOrganizationMcpInput
+ context: OrganizationMembershipContext
+ }) {
+ const group = await loadScopedAccountsCredentialListContext({
+ kind: 'organization',
+ organizationId: context.organizationId,
+ })
+ if (!group)
+ throw new OrchestrationError(
+ 'not_found',
+ 'Organization connected accounts are not configured'
+ )
+ const result = await updateManagedMcpConnector({
+ organizationId: context.organizationId,
+ credentialGroupId: group.credentialGroupId,
+ connectorId: 'databricks',
+ input: {
+ url: input.url,
+ oauthClientId: input.oauthClientId,
+ oauthClientSecret: input.oauthClientSecret,
+ name: input.name,
+ },
+ })
+ return { ...result, credentialGroupId: group.credentialGroupId }
+ },
+ projectAudit: ({ credentialGroupId }) => ({
+ resourceId: credentialGroupId,
+ resourceName: 'Connected accounts',
+ description: 'Configured the organization Databricks provider',
+ }),
+ async afterSuccess({ result }) {
+ await clearCredentialGroupMcpOAuthAttempts(result.resetMcpServerIds)
+ await Promise.all(
+ [...result.resetMcpServerIds, ...result.retiredMcpConnectionIds].map((id) =>
+ evictMcpServerConnections(id, 'organization MCP configuration changed')
+ )
+ )
+ },
+})
diff --git a/apps/sim/lib/credential-groups/application/context.ts b/apps/sim/lib/credential-groups/application/context.ts
index cd0092050f1..a5f8a97a262 100644
--- a/apps/sim/lib/credential-groups/application/context.ts
+++ b/apps/sim/lib/credential-groups/application/context.ts
@@ -13,7 +13,10 @@ import { loadActiveWorkspaceApplicationContext } from '@/lib/workspaces/applicat
export async function requireCredentialGroupsAvailable(workspaceId: string): Promise {
const ownerBilling = await getWorkspaceOwnerSubscriptionAccess(workspaceId)
- const availability = await resolveCredentialGroupsAvailability({ workspaceId, ownerBilling })
+ const availability = await resolveCredentialGroupsAvailability({
+ organizationId: ownerBilling.organizationId,
+ ownerBilling,
+ })
if (!availability.available) {
const message =
availability.reason === 'enterprise_plan_required'
@@ -25,7 +28,12 @@ export async function requireCredentialGroupsAvailable(workspaceId: string): Pro
export async function requireCredentialGroupSettingsAvailable(workspaceId: string): Promise {
const ownerBilling = await getWorkspaceOwnerSubscriptionAccess(workspaceId)
- if (!(await isCredentialGroupsAvailable({ workspaceId, ownerBilling }))) {
+ if (
+ !(await isCredentialGroupsAvailable({
+ organizationId: ownerBilling.organizationId,
+ ownerBilling,
+ }))
+ ) {
throw new OrchestrationError('not_found', 'Credential Groups are not available')
}
}
diff --git a/apps/sim/lib/credential-groups/application/enrollment-auth.test.ts b/apps/sim/lib/credential-groups/application/enrollment-auth.test.ts
index 79db9327bbb..0ef34067003 100644
--- a/apps/sim/lib/credential-groups/application/enrollment-auth.test.ts
+++ b/apps/sim/lib/credential-groups/application/enrollment-auth.test.ts
@@ -1,8 +1,9 @@
/** @vitest-environment node */
import { sha256Hex } from '@sim/security/hash'
-import { describe, expect, it, vi } from 'vitest'
+import { beforeEach, describe, expect, it, vi } from 'vitest'
-const mocks = vi.hoisted(() => ({ authenticate: vi.fn() }))
+const mocks = vi.hoisted(() => ({ authenticate: vi.fn(), getSession: vi.fn() }))
+vi.mock('@/lib/auth', () => ({ getSession: mocks.getSession }))
vi.mock('@/lib/credential-groups/enrollments', () => ({
authenticatePublicCredentialGroupEnrollment: mocks.authenticate,
}))
@@ -14,17 +15,23 @@ import {
import type { CredentialGroupOAuthAttempt } from '@/lib/credential-groups/oauth-state'
describe('consumed OAuth attempt identity', () => {
+ beforeEach(() => {
+ vi.clearAllMocks()
+ mocks.getSession.mockResolvedValue({ user: { id: 'user-1', emailVerified: true } })
+ })
it('retains the old invitation identity without reauthenticating a rotated bearer', async () => {
const attempt = {
+ userId: 'user-1',
workspaceId: 'workspace',
credentialGroupId: 'group',
enrollmentId: 'enrollment',
email: 'person@example.com',
invitationToken: 'old-invitation',
} as CredentialGroupOAuthAttempt
- const principal = credentialGroupOAuthAttemptPrincipal(attempt)
+ const principal = await credentialGroupOAuthAttemptPrincipal(attempt)
expect(principal).toEqual({
kind: 'credential_group_enrollment',
+ userId: 'user-1',
workspaceId: 'workspace',
credentialGroupId: 'group',
enrollmentId: 'enrollment',
@@ -37,4 +44,21 @@ describe('consumed OAuth attempt identity', () => {
expect(await authenticateCredentialGroupEnrollment('old-invitation')).toBeNull()
expect(mocks.authenticate).toHaveBeenCalledWith('old-invitation')
})
+ it('rejects completion from a different signed-in user', async () => {
+ await expect(
+ credentialGroupOAuthAttemptPrincipal({
+ userId: 'other-user',
+ organizationId: 'org-1',
+ credentialGroupId: 'group',
+ enrollmentId: 'enrollment',
+ email: 'person@example.com',
+ invitationToken: 'token',
+ })
+ ).rejects.toMatchObject({ code: 'forbidden' })
+ })
+ it('requires a verified signed-in user before reading an invitation', async () => {
+ mocks.getSession.mockResolvedValue(null)
+ expect(await authenticateCredentialGroupEnrollment('token')).toBeNull()
+ expect(mocks.authenticate).not.toHaveBeenCalled()
+ })
})
diff --git a/apps/sim/lib/credential-groups/application/enrollment-auth.ts b/apps/sim/lib/credential-groups/application/enrollment-auth.ts
index d4a8af1b0f5..b39ac84ea1e 100644
--- a/apps/sim/lib/credential-groups/application/enrollment-auth.ts
+++ b/apps/sim/lib/credential-groups/application/enrollment-auth.ts
@@ -1,5 +1,7 @@
import type { CredentialGroupEnrollmentPrincipal } from '@sim/auth/principal'
import { sha256Hex } from '@sim/security/hash'
+import { getSession } from '@/lib/auth'
+import { OrchestrationError } from '@/lib/core/orchestration/types'
import { resourceScopeFields, resourceScopeFromOwner } from '@/lib/core/resource-scope'
import { authenticatePublicCredentialGroupEnrollment } from '@/lib/credential-groups/enrollments'
import type { CredentialGroupOAuthAttempt } from '@/lib/credential-groups/oauth-state'
@@ -9,10 +11,14 @@ export async function authenticateCredentialGroupEnrollment(
invitationToken: string
): Promise {
if (!invitationToken.trim() || invitationToken.length > 128) return null
+ const session = await getSession()
+ if (!session?.user?.id || !session.user.emailVerified) return null
const identity = await authenticatePublicCredentialGroupEnrollment(invitationToken)
if (!identity) return null
+ if (identity.userId && identity.userId !== session.user.id) return null
return Object.freeze({
kind: 'credential_group_enrollment' as const,
+ userId: session.user.id,
...resourceScopeFields(resourceScopeFromOwner(identity)),
credentialGroupId: identity.credentialGroupId,
enrollmentId: identity.enrollmentId,
@@ -22,7 +28,7 @@ export async function authenticateCredentialGroupEnrollment(
}
/** A consumed one-time attempt retains only its original enrollment authority, never a rotated invitation. */
-export function credentialGroupOAuthAttemptPrincipal(
+export async function credentialGroupOAuthAttemptPrincipal(
attempt: Pick<
CredentialGroupOAuthAttempt,
| 'workspaceId'
@@ -31,10 +37,19 @@ export function credentialGroupOAuthAttemptPrincipal(
| 'enrollmentId'
| 'email'
| 'invitationToken'
+ | 'userId'
>
-): CredentialGroupEnrollmentPrincipal {
+): Promise {
+ const session = await getSession()
+ if (!attempt.userId || !session?.user?.emailVerified || session.user.id !== attempt.userId) {
+ throw new OrchestrationError(
+ 'forbidden',
+ 'Complete authorization using the same signed-in account that started it'
+ )
+ }
return Object.freeze({
kind: 'credential_group_enrollment',
+ userId: session.user.id,
...resourceScopeFields(resourceScopeFromOwner(attempt)),
credentialGroupId: attempt.credentialGroupId,
enrollmentId: attempt.enrollmentId,
diff --git a/apps/sim/lib/credential-groups/application/list-credentials.test.ts b/apps/sim/lib/credential-groups/application/list-credentials.test.ts
index 58a5dd13087..b940d915701 100644
--- a/apps/sim/lib/credential-groups/application/list-credentials.test.ts
+++ b/apps/sim/lib/credential-groups/application/list-credentials.test.ts
@@ -3,9 +3,11 @@
*/
import type { SessionPrincipal, WorkflowExecutionDelegatedPrincipal } from '@sim/auth/principal'
import { beforeEach, describe, expect, it, vi } from 'vitest'
+import { buildOrganizationAccountAccessPolicy } from '@/lib/credential-groups/application/workspace-access-policy'
const mocks = vi.hoisted(() => ({
getWorkspaceOwnerSubscriptionAccess: vi.fn(),
+ requirePolicy: vi.fn(),
listCredentials: vi.fn(),
loadEnrollmentAccess: vi.fn(),
loadGroup: vi.fn(),
@@ -18,8 +20,12 @@ vi.mock('@/lib/billing/core/workspace-access', () => ({
getWorkspaceOwnerSubscriptionAccess: mocks.getWorkspaceOwnerSubscriptionAccess,
}))
-vi.mock('@/lib/credential-groups/availability', () => ({
- resolveCredentialGroupsAvailability: mocks.resolveCredentialGroupsAvailability,
+vi.mock('@/lib/credential-groups/scoped-availability', () => ({
+ isScopedCredentialGroupsAvailable: async () =>
+ (await mocks.resolveCredentialGroupsAvailability()).available,
+}))
+vi.mock('@/lib/resource-policies/repository', () => ({
+ requireResourcePolicy: mocks.requirePolicy,
}))
vi.mock('@/lib/credential-groups/credentials', () => ({
@@ -31,7 +37,7 @@ vi.mock('@/lib/credential-groups/credentials', () => ({
},
listCredentialGroupCredentialReferences: mocks.listCredentials,
loadCredentialGroupEnrollmentAccessForSubject: mocks.loadEnrollmentAccess,
- loadWorkspaceAccountsCredentialListContext: mocks.loadGroup,
+ loadScopedAccountsCredentialListContext: mocks.loadGroup,
MAX_CREDENTIAL_GROUP_CREDENTIAL_PAGE_SIZE: 100,
}))
@@ -78,7 +84,7 @@ const groupContext = {
}
const workspaceContext = {
workspaceId: 'workspace-1',
- workspaceOrganizationId: null,
+ workspaceOrganizationId: 'org-1',
allowPersonalApiKeys: true,
billedAccountUserId: 'billing-owner-1',
}
@@ -110,6 +116,9 @@ function executorPrincipal(workspaceId = 'workspace-1'): WorkflowExecutionDelega
describe('listCredentialGroupCredentials', () => {
beforeEach(() => {
vi.clearAllMocks()
+ mocks.requirePolicy.mockResolvedValue({
+ document: buildOrganizationAccountAccessPolicy('group-1', ['workspace-1']),
+ })
mocks.loadGroup.mockResolvedValue(groupContext)
mocks.loadWorkspace.mockResolvedValue(workspaceContext)
mocks.resolvePermission.mockResolvedValue('read')
@@ -152,7 +161,7 @@ describe('listCredentialGroupCredentials', () => {
await expect(
listCredentialGroupCredentials.execute({ principal: executorPrincipal(), input })
).rejects.toMatchObject({ code: 'not_found' })
- expect(mocks.loadGroup).toHaveBeenCalledWith('workspace-1')
+ expect(mocks.loadGroup).toHaveBeenCalledWith({ kind: 'organization', organizationId: 'org-1' })
expect(mocks.listCredentials).not.toHaveBeenCalled()
})
@@ -192,14 +201,25 @@ describe('listCredentialGroupCredentials', () => {
expect(mocks.listCredentials).toHaveBeenCalled()
})
- it('does not use the executor subject to filter credential references', async () => {
+ it('rejects inconsistent execution attribution', async () => {
const principal = executorPrincipal()
principal.subjectUserId = 'different-user'
+ await expect(
+ listCredentialGroupCredentials.execute({ principal, input })
+ ).rejects.toMatchObject({ code: 'forbidden' })
+ expect(mocks.listCredentials).not.toHaveBeenCalled()
+ })
- await listCredentialGroupCredentials.execute({ principal, input })
-
- expect(mocks.loadEnrollmentAccess).not.toHaveBeenCalled()
- expect(mocks.listCredentials).toHaveBeenCalled()
+ it('rechecks workspace revocation for the next discovery', async () => {
+ await listCredentialGroupCredentials.execute({ principal: executorPrincipal(), input })
+ mocks.listCredentials.mockClear()
+ mocks.requirePolicy.mockResolvedValue({
+ document: buildOrganizationAccountAccessPolicy('group-1', []),
+ })
+ await expect(
+ listCredentialGroupCredentials.execute({ principal: executorPrincipal(), input })
+ ).rejects.toMatchObject({ code: 'forbidden' })
+ expect(mocks.listCredentials).not.toHaveBeenCalled()
})
it('returns a bounded page after current workspace and entitlement checks', async () => {
@@ -208,11 +228,17 @@ describe('listCredentialGroupCredentials', () => {
input,
})
- expect(mocks.resolvePermission).toHaveBeenCalledWith('user-1', 'workspace-1', null, undefined, {
- forUpdate: undefined,
- })
+ expect(mocks.resolvePermission).toHaveBeenCalledWith(
+ 'user-1',
+ 'workspace-1',
+ 'org-1',
+ undefined,
+ {
+ forUpdate: undefined,
+ }
+ )
expect(mocks.listCredentials).toHaveBeenCalledWith({
- workspaceId: 'workspace-1',
+ organizationId: 'org-1',
credentialGroupId: 'group-1',
limit: 50,
cursor: undefined,
@@ -299,8 +325,8 @@ describe('listCredentialGroupCredentials', () => {
await expect(
listCredentialGroupCredentials.execute({ principal: executorPrincipal(), input })
).rejects.toMatchObject({
- code: 'forbidden',
- message: 'Credential Groups are not available',
+ code: 'not_found',
+ message: 'Organization connected accounts are not available',
})
expect(mocks.listCredentials).not.toHaveBeenCalled()
})
@@ -315,8 +341,8 @@ describe('listCredentialGroupCredentials', () => {
await expect(
listCredentialGroupCredentials.execute({ principal: executorPrincipal(), input })
).rejects.toMatchObject({
- code: 'forbidden',
- message: 'Credential Groups are not available. Enterprise plan required.',
+ code: 'not_found',
+ message: 'Organization connected accounts are not available',
})
expect(mocks.listCredentials).not.toHaveBeenCalled()
})
diff --git a/apps/sim/lib/credential-groups/application/list-credentials.ts b/apps/sim/lib/credential-groups/application/list-credentials.ts
index 8b54994d73b..a81af811f5c 100644
--- a/apps/sim/lib/credential-groups/application/list-credentials.ts
+++ b/apps/sim/lib/credential-groups/application/list-credentials.ts
@@ -1,12 +1,15 @@
import { isValidEmailSyntax, normalizeEmail } from '@sim/utils/string'
import { defineAuthorizedWorkspaceUseCase } from '@/lib/core/application'
import { OrchestrationError } from '@/lib/core/orchestration/types'
-import { credentialGroupDelegationPolicy } from '@/lib/credential-groups/application/authorization'
import {
- requireCredentialGroupsAvailable,
- resolveWorkspaceAccountsContext,
-} from '@/lib/credential-groups/application/context'
+ credentialGroupDelegationPolicy,
+ requireCredentialGroupWorkflowActor,
+} from '@/lib/credential-groups/application/authorization'
import { credentialGroupOperations } from '@/lib/credential-groups/application/operations'
+import {
+ requireOrganizationAccountsWorkspaceAccess,
+ resolveOrganizationAccountsWorkspaceContext,
+} from '@/lib/credential-groups/application/organization-workspace-access'
import {
CredentialGroupCredentialCursorNotFoundError,
type CredentialGroupCredentialReference,
@@ -36,8 +39,12 @@ export interface ListCredentialGroupCredentialsResult {
export const listCredentialGroupCredentials = defineAuthorizedWorkspaceUseCase({
operation: credentialGroupOperations.listCredentials,
resolveContext: ({ input }: { input: ListCredentialGroupCredentialsInput }) =>
- resolveWorkspaceAccountsContext(input.workspaceId),
+ resolveOrganizationAccountsWorkspaceContext(input.workspaceId),
authorizationOptions: { delegation: credentialGroupDelegationPolicy },
+ async authorizeResource({ principal, context }) {
+ requireCredentialGroupWorkflowActor(principal)
+ await requireOrganizationAccountsWorkspaceAccess(context)
+ },
execute: async ({ input, context }): Promise => {
if (
!Number.isInteger(input.limit) ||
@@ -81,12 +88,10 @@ export const listCredentialGroupCredentials = defineAuthorizedWorkspaceUseCase({
)
}
- await requireCredentialGroupsAvailable(context.workspaceId)
-
let page
try {
page = await listCredentialGroupCredentialReferences({
- workspaceId: context.workspaceId,
+ organizationId: context.organizationId,
credentialGroupId: context.credentialGroupId,
credentialGroupOptionIds: activeOptions.map((option) => option.id),
limit: input.limit,
diff --git a/apps/sim/lib/credential-groups/application/list-mcp-connections.test.ts b/apps/sim/lib/credential-groups/application/list-mcp-connections.test.ts
index 8a1a4d775b6..25487113610 100644
--- a/apps/sim/lib/credential-groups/application/list-mcp-connections.test.ts
+++ b/apps/sim/lib/credential-groups/application/list-mcp-connections.test.ts
@@ -3,9 +3,11 @@
*/
import type { SessionPrincipal, WorkflowExecutionDelegatedPrincipal } from '@sim/auth/principal'
import { beforeEach, describe, expect, it, vi } from 'vitest'
+import { buildOrganizationAccountAccessPolicy } from '@/lib/credential-groups/application/workspace-access-policy'
const mocks = vi.hoisted(() => ({
getWorkspaceOwnerSubscriptionAccess: vi.fn(),
+ requirePolicy: vi.fn(),
listMcpConnections: vi.fn(),
loadGroup: vi.fn(),
loadWorkspace: vi.fn(),
@@ -17,12 +19,16 @@ vi.mock('@/lib/billing/core/workspace-access', () => ({
getWorkspaceOwnerSubscriptionAccess: mocks.getWorkspaceOwnerSubscriptionAccess,
}))
-vi.mock('@/lib/credential-groups/availability', () => ({
- resolveCredentialGroupsAvailability: mocks.resolveCredentialGroupsAvailability,
+vi.mock('@/lib/credential-groups/scoped-availability', () => ({
+ isScopedCredentialGroupsAvailable: async () =>
+ (await mocks.resolveCredentialGroupsAvailability()).available,
+}))
+vi.mock('@/lib/resource-policies/repository', () => ({
+ requireResourcePolicy: mocks.requirePolicy,
}))
vi.mock('@/lib/credential-groups/credentials', () => ({
- loadWorkspaceAccountsCredentialListContext: mocks.loadGroup,
+ loadScopedAccountsCredentialListContext: mocks.loadGroup,
}))
vi.mock('@/lib/credential-groups/mcp-connections', () => ({
@@ -58,7 +64,7 @@ const groupContext = {
}
const workspaceContext = {
workspaceId: 'workspace-1',
- workspaceOrganizationId: null,
+ workspaceOrganizationId: 'org-1',
allowPersonalApiKeys: true,
billedAccountUserId: 'billing-owner-1',
}
@@ -90,6 +96,9 @@ function executorPrincipal(workspaceId = 'workspace-1'): WorkflowExecutionDelega
describe('listCredentialGroupMcpConnections', () => {
beforeEach(() => {
vi.clearAllMocks()
+ mocks.requirePolicy.mockResolvedValue({
+ document: buildOrganizationAccountAccessPolicy('group-1', ['workspace-1']),
+ })
mocks.loadGroup.mockResolvedValue(groupContext)
mocks.loadWorkspace.mockResolvedValue(workspaceContext)
mocks.resolvePermission.mockResolvedValue('read')
@@ -144,12 +153,13 @@ describe('listCredentialGroupMcpConnections', () => {
})
expect(mocks.listMcpConnections).toHaveBeenCalledWith({
- workspaceId: 'workspace-1',
+ organizationId: 'org-1',
credentialGroupId: 'group-1',
limit: 50,
cursor: undefined,
email: 'person@example.com',
mcpServerId: 'mcp-server-1',
+ connectorId: undefined,
})
expect(result).toEqual({
mcpConnections: [
diff --git a/apps/sim/lib/credential-groups/application/list-mcp-connections.ts b/apps/sim/lib/credential-groups/application/list-mcp-connections.ts
index f3af6657ecf..576b86d27dd 100644
--- a/apps/sim/lib/credential-groups/application/list-mcp-connections.ts
+++ b/apps/sim/lib/credential-groups/application/list-mcp-connections.ts
@@ -1,12 +1,16 @@
import { isValidEmailSyntax, normalizeEmail } from '@sim/utils/string'
import { defineAuthorizedWorkspaceUseCase } from '@/lib/core/application'
import { OrchestrationError } from '@/lib/core/orchestration/types'
-import { credentialGroupDelegationPolicy } from '@/lib/credential-groups/application/authorization'
import {
- requireCredentialGroupsAvailable,
- resolveWorkspaceAccountsContext,
-} from '@/lib/credential-groups/application/context'
+ credentialGroupDelegationPolicy,
+ requireCredentialGroupWorkflowActor,
+} from '@/lib/credential-groups/application/authorization'
import { credentialGroupOperations } from '@/lib/credential-groups/application/operations'
+import {
+ requireOrganizationAccountsWorkspaceAccess,
+ resolveOrganizationAccountsWorkspaceContext,
+} from '@/lib/credential-groups/application/organization-workspace-access'
+import { getManagedMcpConnector } from '@/lib/credential-groups/managed-mcp-connectors'
import {
CredentialGroupMcpConnectionCursorNotFoundError,
type CredentialGroupMcpConnectionReference,
@@ -20,6 +24,7 @@ export interface ListCredentialGroupMcpConnectionsInput {
cursor?: string
email?: string
mcpServerId?: string
+ connectorId?: string
}
export interface ListCredentialGroupMcpConnectionsResult {
@@ -32,8 +37,12 @@ export interface ListCredentialGroupMcpConnectionsResult {
export const listCredentialGroupMcpConnections = defineAuthorizedWorkspaceUseCase({
operation: credentialGroupOperations.listMcpConnections,
resolveContext: ({ input }: { input: ListCredentialGroupMcpConnectionsInput }) =>
- resolveWorkspaceAccountsContext(input.workspaceId),
+ resolveOrganizationAccountsWorkspaceContext(input.workspaceId),
authorizationOptions: { delegation: credentialGroupDelegationPolicy },
+ async authorizeResource({ principal, context }) {
+ requireCredentialGroupWorkflowActor(principal)
+ await requireOrganizationAccountsWorkspaceAccess(context)
+ },
execute: async ({ input, context }): Promise => {
if (
!Number.isInteger(input.limit) ||
@@ -54,21 +63,21 @@ export const listCredentialGroupMcpConnections = defineAuthorizedWorkspaceUseCas
throw new OrchestrationError('validation', 'Email must be a valid address')
}
const mcpServerId = input.mcpServerId?.trim()
+ if (input.connectorId !== undefined) getManagedMcpConnector(input.connectorId)
if (input.mcpServerId !== undefined && !mcpServerId) {
throw new OrchestrationError('validation', 'MCP server ID must not be empty')
}
- await requireCredentialGroupsAvailable(context.workspaceId)
-
let page
try {
page = await listCredentialGroupMcpConnectionReferences({
- workspaceId: context.workspaceId,
+ organizationId: context.organizationId,
credentialGroupId: context.credentialGroupId,
limit: input.limit,
cursor: input.cursor,
email,
mcpServerId,
+ connectorId: input.connectorId,
})
} catch (error) {
if (error instanceof CredentialGroupMcpConnectionCursorNotFoundError) {
diff --git a/apps/sim/lib/credential-groups/application/organization-access.test.ts b/apps/sim/lib/credential-groups/application/organization-access.test.ts
new file mode 100644
index 00000000000..f86ea9f7790
--- /dev/null
+++ b/apps/sim/lib/credential-groups/application/organization-access.test.ts
@@ -0,0 +1,153 @@
+/** @vitest-environment node */
+import type { SessionPrincipal } from '@sim/auth/principal'
+import { dbChainMockFns, queueTableRows, resetDbChainMock, schemaMock } from '@sim/testing'
+import { eq } from 'drizzle-orm'
+import { beforeEach, describe, expect, it, vi } from 'vitest'
+
+const mocks = vi.hoisted(() => ({
+ available: vi.fn(),
+ group: vi.fn(),
+ setup: vi.fn(),
+ write: vi.fn(),
+ policy: vi.fn(),
+}))
+vi.mock('@/lib/credential-groups/scoped-availability', () => ({
+ isScopedCredentialGroupsAvailable: mocks.available,
+}))
+vi.mock('@/lib/credential-groups/credentials', () => ({
+ loadScopedAccountsCredentialListContext: mocks.group,
+}))
+vi.mock('@/lib/credential-groups/organization-setup', () => ({
+ requireOrganizationAccountsSetup: mocks.setup,
+}))
+vi.mock('@/lib/permission-groups/resolve.server', () => ({
+ getUserPermissionConfigForOrganization: vi.fn().mockResolvedValue(null),
+}))
+vi.mock('@/lib/credential-groups/service', () => ({
+ ensureWorkspaceAccountsGroup: vi.fn(),
+ getOrganizationAccountsGroup: vi.fn(),
+ updateCredentialGroup: vi.fn(),
+}))
+vi.mock('@/lib/credential-groups/provider-availability', () => ({
+ listConfiguredCredentialGroupProviders: vi.fn(),
+}))
+vi.mock('@/lib/credential-groups/self-enrollment', () => ({
+ createViewerCredentialGroupEnrollment: vi.fn(),
+}))
+vi.mock('@/lib/resource-policies/repository', () => ({
+ requireResourcePolicy: mocks.policy,
+ writeResourcePolicy: mocks.write,
+ ResourcePolicyRevisionConflictError: class extends Error {},
+}))
+
+import {
+ getOrganizationAccountWorkspaceAccess,
+ updateOrganizationAccountWorkspaceAccess,
+} from '@/lib/credential-groups/application/organization-access'
+import { buildOrganizationAccountAccessPolicy } from '@/lib/credential-groups/application/workspace-access-policy'
+import { ResourcePolicyRevisionConflictError } from '@/lib/resource-policies/repository'
+
+const principal: SessionPrincipal = {
+ kind: 'session',
+ userId: 'admin-user',
+ sessionId: 'session-1',
+}
+const input = { organizationId: 'org-1', revision: 3, workspaceIds: ['workspace-1'] }
+
+describe('organization workspace sharing administration', () => {
+ beforeEach(() => {
+ vi.clearAllMocks()
+ resetDbChainMock()
+ mocks.available.mockResolvedValue(true)
+ mocks.group.mockResolvedValue({
+ credentialGroupId: 'group-1',
+ name: 'Accounts',
+ status: 'active',
+ })
+ mocks.setup.mockResolvedValue(undefined)
+ mocks.policy.mockResolvedValue({
+ revision: 3,
+ document: buildOrganizationAccountAccessPolicy('group-1', []),
+ })
+ mocks.write.mockImplementation(async ({ document }) => ({ revision: 4, document }))
+ })
+
+ it.each(['member', null])(
+ 'denies management by a %s before loading account data',
+ async (role) => {
+ queueTableRows(schemaMock.member, role ? [{ role }] : [])
+ await expect(
+ updateOrganizationAccountWorkspaceAccess.execute({ principal, input })
+ ).rejects.toThrow()
+ expect(mocks.group).not.toHaveBeenCalled()
+ expect(mocks.write).not.toHaveBeenCalled()
+ }
+ )
+
+ it('uses the routed org and checks every workspace before granting access', async () => {
+ queueTableRows(schemaMock.member, [{ role: 'admin' }])
+ queueTableRows(schemaMock.workspace, [{ id: 'workspace-1' }])
+ await expect(
+ updateOrganizationAccountWorkspaceAccess.execute({ principal, input })
+ ).resolves.toMatchObject({ revision: 4, workspaceIds: ['workspace-1'] })
+ expect(eq).toHaveBeenCalledWith(schemaMock.member.userId, 'admin-user')
+ expect(eq).toHaveBeenCalledWith(schemaMock.member.organizationId, 'org-1')
+ expect(eq).toHaveBeenCalledWith(schemaMock.workspace.organizationId, 'org-1')
+ expect(mocks.write).toHaveBeenCalledWith(
+ expect.objectContaining({
+ organizationId: 'org-1',
+ actorUserId: 'admin-user',
+ expectedRevision: 3,
+ })
+ )
+ })
+
+ it('refuses foreign or archived workspaces without writing a policy', async () => {
+ queueTableRows(schemaMock.member, [{ role: 'owner' }])
+ queueTableRows(schemaMock.workspace, [])
+ await expect(
+ updateOrganizationAccountWorkspaceAccess.execute({ principal, input })
+ ).rejects.toThrow('Every allowed workspace')
+ expect(mocks.write).not.toHaveBeenCalled()
+ })
+
+ it('supports revoking every workspace without a replacement workflow grant', async () => {
+ queueTableRows(schemaMock.member, [{ role: 'admin' }])
+ await expect(
+ updateOrganizationAccountWorkspaceAccess.execute({
+ principal,
+ input: { ...input, workspaceIds: [] },
+ })
+ ).resolves.toMatchObject({ workspaceIds: [] })
+ expect(mocks.write).toHaveBeenCalledWith(
+ expect.objectContaining({ document: buildOrganizationAccountAccessPolicy('group-1', []) })
+ )
+ })
+
+ it('rejects a stale revision rather than overwriting another admin', async () => {
+ queueTableRows(schemaMock.member, [{ role: 'admin' }])
+ mocks.write.mockRejectedValue(new ResourcePolicyRevisionConflictError())
+ await expect(
+ updateOrganizationAccountWorkspaceAccess.execute({
+ principal,
+ input: { ...input, workspaceIds: [] },
+ })
+ ).rejects.toMatchObject({ code: 'conflict' })
+ })
+
+ it('fails closed when the flag is disabled or legacy setup is unresolved', async () => {
+ queueTableRows(schemaMock.member, [{ role: 'admin' }])
+ mocks.available.mockResolvedValue(false)
+ await expect(
+ getOrganizationAccountWorkspaceAccess.execute({ principal, input })
+ ).rejects.toMatchObject({ code: 'not_found' })
+ expect(mocks.group).not.toHaveBeenCalled()
+ queueTableRows(schemaMock.member, [{ role: 'admin' }])
+ mocks.available.mockResolvedValue(true)
+ mocks.setup.mockRejectedValue(new Error('Migration review required'))
+ await expect(
+ getOrganizationAccountWorkspaceAccess.execute({ principal, input })
+ ).rejects.toThrow('Migration review required')
+ expect(dbChainMockFns.from).not.toHaveBeenCalledWith(schemaMock.workspace)
+ })
+})
diff --git a/apps/sim/lib/credential-groups/application/organization-access.ts b/apps/sim/lib/credential-groups/application/organization-access.ts
new file mode 100644
index 00000000000..7300e2bbfb8
--- /dev/null
+++ b/apps/sim/lib/credential-groups/application/organization-access.ts
@@ -0,0 +1,143 @@
+import { db } from '@sim/db'
+import { workspace } from '@sim/db/schema'
+import { and, asc, eq, inArray, isNull } from 'drizzle-orm'
+import type { OrganizationMembershipContext } from '@/lib/core/application/organization-authorization'
+import { defineOrganizationOperation } from '@/lib/core/application/organization-operation'
+import { OrchestrationError } from '@/lib/core/orchestration/types'
+import { defineOrganizationAccountsUseCase } from '@/lib/credential-groups/application/organization-accounts'
+import {
+ buildOrganizationAccountAccessPolicy,
+ listOrganizationAccountWorkspaceIds,
+ organizationAccountAccessPolicyCodec,
+ organizationAccountWorkspaceIdsSchema,
+} from '@/lib/credential-groups/application/workspace-access-policy'
+import { loadScopedAccountsCredentialListContext } from '@/lib/credential-groups/credentials'
+import { ORGANIZATION_ACCOUNT_WORKSPACE_LIMIT } from '@/lib/credential-groups/limits'
+import {
+ ResourcePolicyRevisionConflictError,
+ requireResourcePolicy,
+ writeResourcePolicy,
+} from '@/lib/resource-policies/repository'
+
+export const organizationAccountAccessOperations = {
+ read: defineOrganizationOperation({
+ id: 'organization_accounts.workspace_access.read',
+ minimumRole: 'admin',
+ principalKinds: ['session'],
+ capability: 'integrations.manage',
+ }),
+ update: defineOrganizationOperation({
+ id: 'organization_accounts.workspace_access.update',
+ minimumRole: 'admin',
+ principalKinds: ['session'],
+ capability: 'integrations.manage',
+ }),
+} as const
+
+async function requireGroup(organizationId: string) {
+ const group = await loadScopedAccountsCredentialListContext({
+ kind: 'organization',
+ organizationId,
+ })
+ if (!group)
+ throw new OrchestrationError('not_found', 'Organization connected accounts are not configured')
+ return group
+}
+
+export const getOrganizationAccountWorkspaceAccess = defineOrganizationAccountsUseCase({
+ operation: organizationAccountAccessOperations.read,
+ async execute({ context }) {
+ const group = await requireGroup(context.organizationId)
+ const [policy, workspaces] = await Promise.all([
+ requireResourcePolicy({
+ organizationId: context.organizationId,
+ resourceType: 'credential_group',
+ resourceId: group.credentialGroupId,
+ codec: organizationAccountAccessPolicyCodec,
+ }),
+ db
+ .select({ id: workspace.id, name: workspace.name })
+ .from(workspace)
+ .where(
+ and(eq(workspace.organizationId, context.organizationId), isNull(workspace.archivedAt))
+ )
+ .orderBy(asc(workspace.name), asc(workspace.id))
+ .limit(ORGANIZATION_ACCOUNT_WORKSPACE_LIMIT + 1),
+ ])
+ if (workspaces.length > ORGANIZATION_ACCOUNT_WORKSPACE_LIMIT)
+ throw new OrchestrationError(
+ 'validation',
+ `Workspace access supports at most ${ORGANIZATION_ACCOUNT_WORKSPACE_LIMIT} workspaces`
+ )
+ return {
+ revision: policy.revision,
+ workspaceIds: listOrganizationAccountWorkspaceIds(policy.document),
+ workspaces,
+ }
+ },
+})
+
+export const updateOrganizationAccountWorkspaceAccess = defineOrganizationAccountsUseCase({
+ operation: organizationAccountAccessOperations.update,
+ async execute({
+ input,
+ context,
+ }: {
+ input: { organizationId: string; revision: number; workspaceIds: string[] }
+ context: OrganizationMembershipContext
+ }) {
+ const parsed = organizationAccountWorkspaceIdsSchema.safeParse(input.workspaceIds)
+ if (!parsed.success)
+ throw new OrchestrationError(
+ 'validation',
+ 'Workspace IDs must be unique, valid identifiers within the supported limit'
+ )
+ const group = await requireGroup(context.organizationId)
+ if (parsed.data.length) {
+ const rows = await db
+ .select({ id: workspace.id })
+ .from(workspace)
+ .where(
+ and(
+ eq(workspace.organizationId, context.organizationId),
+ inArray(workspace.id, parsed.data),
+ isNull(workspace.archivedAt)
+ )
+ )
+ if (rows.length !== parsed.data.length)
+ throw new OrchestrationError(
+ 'validation',
+ 'Every allowed workspace must be active and belong to this organization'
+ )
+ }
+ try {
+ const policy = await writeResourcePolicy({
+ organizationId: context.organizationId,
+ resourceType: 'credential_group',
+ resourceId: group.credentialGroupId,
+ codec: organizationAccountAccessPolicyCodec,
+ expectedRevision: input.revision,
+ document: buildOrganizationAccountAccessPolicy(group.credentialGroupId, parsed.data),
+ actorUserId: context.userId,
+ })
+ return {
+ credentialGroupId: group.credentialGroupId,
+ name: group.name,
+ revision: policy.revision,
+ workspaceIds: listOrganizationAccountWorkspaceIds(policy.document),
+ }
+ } catch (error) {
+ if (error instanceof ResourcePolicyRevisionConflictError)
+ throw new OrchestrationError(
+ 'conflict',
+ 'Workspace access changed. Reload before saving again.'
+ )
+ throw error
+ }
+ },
+ projectAudit: (result) => ({
+ resourceId: result.credentialGroupId,
+ resourceName: result.name,
+ description: `Allowed ${result.workspaceIds.length} workspaces to use organization connected accounts`,
+ }),
+})
diff --git a/apps/sim/lib/credential-groups/application/organization-account-indexing.test.ts b/apps/sim/lib/credential-groups/application/organization-account-indexing.test.ts
new file mode 100644
index 00000000000..0917a7d64ae
--- /dev/null
+++ b/apps/sim/lib/credential-groups/application/organization-account-indexing.test.ts
@@ -0,0 +1,136 @@
+/** @vitest-environment node */
+import { auditMock, auditMockFns, queueTableRows, resetDbChainMock, schemaMock } from '@sim/testing'
+import { beforeEach, describe, expect, it, vi } from 'vitest'
+
+const mocks = vi.hoisted(() => ({
+ available: vi.fn(),
+ group: vi.fn(),
+ readiness: vi.fn(),
+ feature: vi.fn(),
+ setIndexing: vi.fn(),
+ dispatch: vi.fn(),
+}))
+vi.mock('@sim/audit', () => auditMock)
+vi.mock('@/lib/credential-groups/scoped-availability', () => ({
+ isScopedCredentialGroupsAvailable: mocks.available,
+}))
+vi.mock('@/lib/credential-groups/credentials', () => ({
+ loadScopedAccountsCredentialListContext: mocks.group,
+}))
+vi.mock('@/lib/credential-groups/organization-setup', () => ({
+ requireOrganizationAccountsSetup: mocks.readiness,
+}))
+vi.mock('@/lib/permission-groups/resolve.server', () => ({
+ getUserPermissionConfigForOrganization: vi.fn().mockResolvedValue(null),
+}))
+vi.mock('@/lib/credential-groups/service', () => ({
+ ensureWorkspaceAccountsGroup: vi.fn(),
+ getOrganizationAccountsGroup: vi.fn(),
+ updateCredentialGroup: vi.fn(),
+}))
+vi.mock('@/lib/credential-groups/provider-availability', () => ({
+ listConfiguredCredentialGroupProviders: vi.fn(),
+}))
+vi.mock('@/lib/credential-groups/self-enrollment', () => ({
+ createViewerCredentialGroupEnrollment: vi.fn(),
+}))
+vi.mock('@/lib/credential-groups/managed-mcp-service', () => ({
+ ManagedMcpConnectorError: class extends Error {},
+}))
+vi.mock('@/lib/knowledge/access/availability', () => ({
+ requireKnowledgeMemberAccessAvailable: mocks.feature,
+ isKnowledgeMemberAccessAvailable: vi.fn(),
+}))
+vi.mock('@/lib/knowledge/connectors/organization-account-indexing', () => ({
+ setOrganizationAccountIndexing: mocks.setIndexing,
+}))
+vi.mock('@/lib/knowledge/connectors/member-queue', () => ({
+ dispatchMemberSyncsForCredentialOption: mocks.dispatch,
+}))
+
+import { updateOrganizationAccountIndexing } from '@/lib/credential-groups/application/organization-account-indexing'
+
+const principal = { kind: 'session' as const, userId: 'admin-1', sessionId: 'session-1' }
+const input = { organizationId: 'org-1', optionId: 'option-1', enabled: true }
+
+describe('organization account indexing authorization', () => {
+ beforeEach(() => {
+ vi.clearAllMocks()
+ resetDbChainMock()
+ mocks.available.mockResolvedValue(true)
+ mocks.group.mockResolvedValue({ credentialGroupId: 'group-1' })
+ mocks.feature.mockResolvedValue(undefined)
+ mocks.setIndexing.mockResolvedValue({
+ enabled: true,
+ changed: true,
+ providerName: 'Gmail',
+ knowledgeBaseIds: ['kb-1'],
+ })
+ })
+ it.each(['member', null])('denies a %s before reading account data', async (role) => {
+ queueTableRows(schemaMock.member, role ? [{ role }] : [])
+ await expect(updateOrganizationAccountIndexing.execute({ principal, input })).rejects.toThrow()
+ expect(mocks.group).not.toHaveBeenCalled()
+ expect(mocks.setIndexing).not.toHaveBeenCalled()
+ })
+ it.each(['owner', 'admin'])(
+ 'allows an org %s and dispatches only that organization option',
+ async (role) => {
+ queueTableRows(schemaMock.member, [{ role }])
+ await updateOrganizationAccountIndexing.execute({ principal, input })
+ expect(mocks.setIndexing).toHaveBeenCalledWith({ ...input, credentialGroupId: 'group-1' })
+ expect(mocks.dispatch).toHaveBeenCalledWith({
+ organizationId: 'org-1',
+ credentialGroupOptionId: 'option-1',
+ })
+ expect(auditMockFns.mockRecordAudit).toHaveBeenCalledWith(
+ expect.objectContaining({ actorId: 'admin-1', metadata: { organizationId: 'org-1' } })
+ )
+ }
+ )
+ it('requires indexing availability to enable a source', async () => {
+ queueTableRows(schemaMock.member, [{ role: 'admin' }])
+ mocks.feature.mockRejectedValue(new Error('Search is not enabled'))
+ await expect(updateOrganizationAccountIndexing.execute({ principal, input })).rejects.toThrow(
+ 'Search is not enabled'
+ )
+ expect(mocks.setIndexing).not.toHaveBeenCalled()
+ expect(mocks.dispatch).not.toHaveBeenCalled()
+ })
+ it('allows pausing when the Search feature is off and does not dispatch', async () => {
+ queueTableRows(schemaMock.member, [{ role: 'admin' }])
+ mocks.setIndexing.mockResolvedValue({
+ enabled: false,
+ changed: true,
+ providerName: 'Gmail',
+ knowledgeBaseIds: ['kb-1'],
+ })
+ await updateOrganizationAccountIndexing.execute({
+ principal,
+ input: { ...input, enabled: false },
+ })
+ expect(mocks.feature).not.toHaveBeenCalled()
+ expect(mocks.dispatch).not.toHaveBeenCalled()
+ })
+ it('does not audit or redispatch an unchanged setting', async () => {
+ queueTableRows(schemaMock.member, [{ role: 'admin' }])
+ mocks.setIndexing.mockResolvedValue({
+ enabled: true,
+ changed: false,
+ providerName: 'Gmail',
+ knowledgeBaseIds: ['kb-1'],
+ })
+ await updateOrganizationAccountIndexing.execute({ principal, input })
+ expect(auditMockFns.mockRecordAudit).not.toHaveBeenCalled()
+ expect(mocks.dispatch).not.toHaveBeenCalled()
+ })
+ it('does not audit or dispatch a failed mutation', async () => {
+ queueTableRows(schemaMock.member, [{ role: 'admin' }])
+ mocks.setIndexing.mockRejectedValue(new Error('Sync already in progress'))
+ await expect(updateOrganizationAccountIndexing.execute({ principal, input })).rejects.toThrow(
+ 'Sync already in progress'
+ )
+ expect(auditMockFns.mockRecordAudit).not.toHaveBeenCalled()
+ expect(mocks.dispatch).not.toHaveBeenCalled()
+ })
+})
diff --git a/apps/sim/lib/credential-groups/application/organization-account-indexing.ts b/apps/sim/lib/credential-groups/application/organization-account-indexing.ts
new file mode 100644
index 00000000000..1c6791ea61e
--- /dev/null
+++ b/apps/sim/lib/credential-groups/application/organization-account-indexing.ts
@@ -0,0 +1,60 @@
+import type { OrganizationMembershipContext } from '@/lib/core/application/organization-authorization'
+import { defineOrganizationOperation } from '@/lib/core/application/organization-operation'
+import { OrchestrationError } from '@/lib/core/orchestration/types'
+import { defineOrganizationAccountsUseCase } from '@/lib/credential-groups/application/organization-accounts'
+import { loadScopedAccountsCredentialListContext } from '@/lib/credential-groups/credentials'
+import { requireKnowledgeMemberAccessAvailable } from '@/lib/knowledge/access/availability'
+import { dispatchMemberSyncsForCredentialOption } from '@/lib/knowledge/connectors/member-queue'
+import { setOrganizationAccountIndexing } from '@/lib/knowledge/connectors/organization-account-indexing'
+
+export const updateOrganizationAccountIndexingOperation = defineOrganizationOperation({
+ id: 'organization_accounts.indexing.update',
+ minimumRole: 'admin',
+ principalKinds: ['session'],
+ capability: 'knowledge.use',
+})
+
+export const updateOrganizationAccountIndexing = defineOrganizationAccountsUseCase({
+ operation: updateOrganizationAccountIndexingOperation,
+ async execute({
+ input,
+ context,
+ }: {
+ input: { organizationId: string; optionId: string; enabled: boolean }
+ context: OrganizationMembershipContext
+ }) {
+ if (input.enabled)
+ await requireKnowledgeMemberAccessAvailable({ organizationId: context.organizationId })
+ const group = await loadScopedAccountsCredentialListContext({
+ kind: 'organization',
+ organizationId: context.organizationId,
+ })
+ if (!group)
+ throw new OrchestrationError(
+ 'not_found',
+ 'Organization connected accounts are not configured'
+ )
+ const result = await setOrganizationAccountIndexing({
+ organizationId: context.organizationId,
+ credentialGroupId: group.credentialGroupId,
+ optionId: input.optionId,
+ enabled: input.enabled,
+ })
+ return { ...result, credentialGroupId: group.credentialGroupId, optionId: input.optionId }
+ },
+ projectAudit: (result) =>
+ result.changed
+ ? {
+ resourceId: result.credentialGroupId,
+ resourceName: 'Connected accounts',
+ description: `${result.enabled ? 'Enabled' : 'Paused'} ${result.providerName} indexing`,
+ }
+ : null,
+ async afterSuccess({ context, result }) {
+ if (result.enabled && result.changed)
+ await dispatchMemberSyncsForCredentialOption({
+ organizationId: context.organizationId,
+ credentialGroupOptionId: result.optionId,
+ })
+ },
+})
diff --git a/apps/sim/lib/credential-groups/application/organization-account-management.ts b/apps/sim/lib/credential-groups/application/organization-account-management.ts
new file mode 100644
index 00000000000..905feb0831b
--- /dev/null
+++ b/apps/sim/lib/credential-groups/application/organization-account-management.ts
@@ -0,0 +1,235 @@
+import type { OrganizationMembershipContext } from '@/lib/core/application/organization-authorization'
+import { defineOrganizationOperation } from '@/lib/core/application/organization-operation'
+import { OrchestrationError } from '@/lib/core/orchestration/types'
+import { defineOrganizationAccountsUseCase } from '@/lib/credential-groups/application/organization-accounts'
+import { validateCredentialGroupInvitationEmails } from '@/lib/credential-groups/application/validation'
+import { loadScopedAccountsCredentialListContext } from '@/lib/credential-groups/credentials'
+import {
+ inviteCredentialGroupEnrollments,
+ listCredentialGroupEnrollments,
+ loadCredentialGroupInviterIdentity,
+ resendCredentialGroupEnrollment,
+ revokeCredentialGroupEnrollment,
+} from '@/lib/credential-groups/enrollments'
+import type { ManagedMcpConnectorId } from '@/lib/credential-groups/managed-mcp-connectors'
+import {
+ type CreateManagedMcpConnectorInput,
+ createManagedMcpConnector,
+ deleteManagedMcpConnector,
+} from '@/lib/credential-groups/managed-mcp-service'
+import { clearCredentialGroupMcpOAuthAttempts } from '@/lib/credential-groups/mcp-oauth-state'
+import { evictMcpServerConnections } from '@/lib/mcp/connection-pool'
+
+export const organizationAccountManagementOperations = {
+ people: defineOrganizationOperation({
+ id: 'organization_accounts.people.list',
+ minimumRole: 'admin',
+ principalKinds: ['session'],
+ capability: 'integrations.manage',
+ }),
+ invite: defineOrganizationOperation({
+ id: 'organization_accounts.people.invite',
+ minimumRole: 'admin',
+ principalKinds: ['session'],
+ capability: 'integrations.manage',
+ }),
+ resend: defineOrganizationOperation({
+ id: 'organization_accounts.people.resend',
+ minimumRole: 'admin',
+ principalKinds: ['session'],
+ capability: 'integrations.manage',
+ }),
+ revoke: defineOrganizationOperation({
+ id: 'organization_accounts.people.revoke',
+ minimumRole: 'admin',
+ principalKinds: ['session'],
+ capability: 'integrations.manage',
+ }),
+ addMcp: defineOrganizationOperation({
+ id: 'organization_accounts.mcp.add',
+ minimumRole: 'admin',
+ principalKinds: ['session'],
+ capability: 'integrations.manage',
+ }),
+ removeMcp: defineOrganizationOperation({
+ id: 'organization_accounts.mcp.remove',
+ minimumRole: 'admin',
+ principalKinds: ['session'],
+ capability: 'integrations.manage',
+ }),
+} as const
+
+interface OrganizationInput {
+ organizationId: string
+}
+interface OrganizationExecution {
+ input: I
+ context: OrganizationMembershipContext
+}
+
+async function requireGroup(organizationId: string) {
+ const scope = { kind: 'organization' as const, organizationId }
+ const group = await loadScopedAccountsCredentialListContext(scope)
+ if (!group)
+ throw new OrchestrationError('not_found', 'Organization connected accounts are not configured')
+ return { scope, group }
+}
+
+async function requireInviterName(userId: string) {
+ const inviter = await loadCredentialGroupInviterIdentity(userId)
+ const name = inviter?.name?.trim() || inviter?.email
+ if (!name) throw new OrchestrationError('conflict', 'Inviting user has no display identity')
+ return name
+}
+
+function groupAudit(result: { credentialGroupId: string; description: string }) {
+ return {
+ resourceId: result.credentialGroupId,
+ resourceName: 'Connected accounts',
+ description: result.description,
+ }
+}
+
+async function evictConnections(connectionIds: string[]) {
+ await Promise.all(
+ connectionIds.map((id) =>
+ evictMcpServerConnections(id, 'organization connected accounts changed')
+ )
+ )
+}
+
+export const listOrganizationAccountPeople = defineOrganizationAccountsUseCase({
+ operation: organizationAccountManagementOperations.people,
+ async execute({
+ input,
+ context,
+ }: OrganizationExecution<
+ OrganizationInput & { limit: number; cursor?: string; email?: string }
+ >) {
+ const { scope, group } = await requireGroup(context.organizationId)
+ return listCredentialGroupEnrollments(
+ scope,
+ group.credentialGroupId,
+ input.limit,
+ input.cursor,
+ { email: input.email }
+ )
+ },
+})
+
+export const inviteOrganizationAccountPeople = defineOrganizationAccountsUseCase({
+ operation: organizationAccountManagementOperations.invite,
+ async execute({
+ input,
+ context,
+ }: OrganizationExecution) {
+ const emails = validateCredentialGroupInvitationEmails(input.emails)
+ const { scope, group } = await requireGroup(context.organizationId)
+ const result = await inviteCredentialGroupEnrollments(
+ scope,
+ group.credentialGroupId,
+ context.userId,
+ await requireInviterName(context.userId),
+ { emails }
+ )
+ return {
+ ...result,
+ credentialGroupId: group.credentialGroupId,
+ description: `Sent ${result.sentCount} connected account invitations`,
+ }
+ },
+ projectAudit: groupAudit,
+})
+
+export const resendOrganizationAccountInvitation = defineOrganizationAccountsUseCase({
+ operation: organizationAccountManagementOperations.resend,
+ async execute({
+ input,
+ context,
+ }: OrganizationExecution) {
+ const { scope, group } = await requireGroup(context.organizationId)
+ const enrollment = await resendCredentialGroupEnrollment(
+ scope,
+ group.credentialGroupId,
+ input.enrollmentId,
+ context.userId,
+ await requireInviterName(context.userId)
+ )
+ return {
+ credentialGroupEnrollment: enrollment,
+ credentialGroupId: group.credentialGroupId,
+ description: 'Resent a connected account invitation',
+ }
+ },
+ projectAudit: groupAudit,
+})
+
+export const revokeOrganizationAccountEnrollment = defineOrganizationAccountsUseCase({
+ operation: organizationAccountManagementOperations.revoke,
+ async execute({
+ input,
+ context,
+ }: OrganizationExecution) {
+ const { scope, group } = await requireGroup(context.organizationId)
+ const result = await revokeCredentialGroupEnrollment(
+ scope,
+ group.credentialGroupId,
+ input.enrollmentId
+ )
+ return {
+ ...result,
+ credentialGroupId: group.credentialGroupId,
+ description: 'Revoked a person’s connected accounts',
+ }
+ },
+ projectAudit: groupAudit,
+ afterSuccess: ({ result }) => evictConnections(result.retiredMcpConnectionIds),
+})
+
+export const addOrganizationAccountMcpProvider = defineOrganizationAccountsUseCase({
+ operation: organizationAccountManagementOperations.addMcp,
+ async execute({
+ input,
+ context,
+ }: OrganizationExecution) {
+ const { group } = await requireGroup(context.organizationId)
+ const { organizationId: _organizationId, ...connectorInput } = input
+ const result = await createManagedMcpConnector({
+ organizationId: context.organizationId,
+ credentialGroupId: group.credentialGroupId,
+ userId: context.userId,
+ input: connectorInput,
+ })
+ return {
+ ...result,
+ credentialGroupId: group.credentialGroupId,
+ description: `Added ${result.mcpServer.name} to connected accounts`,
+ }
+ },
+ projectAudit: groupAudit,
+})
+
+export const removeOrganizationAccountMcpProvider = defineOrganizationAccountsUseCase({
+ operation: organizationAccountManagementOperations.removeMcp,
+ async execute({
+ input,
+ context,
+ }: OrganizationExecution) {
+ const { group } = await requireGroup(context.organizationId)
+ const result = await deleteManagedMcpConnector({
+ organizationId: context.organizationId,
+ credentialGroupId: group.credentialGroupId,
+ connectorId: input.connectorId,
+ })
+ return {
+ ...result,
+ credentialGroupId: group.credentialGroupId,
+ description: `Removed ${result.mcpServer.name} from connected accounts`,
+ }
+ },
+ projectAudit: groupAudit,
+ async afterSuccess({ result }) {
+ await clearCredentialGroupMcpOAuthAttempts(result.serverIds)
+ await evictConnections([...result.serverIds, ...result.retiredMcpConnectionIds])
+ },
+})
diff --git a/apps/sim/lib/credential-groups/application/organization-accounts.ts b/apps/sim/lib/credential-groups/application/organization-accounts.ts
index b95265fce6d..ccf1782f170 100644
--- a/apps/sim/lib/credential-groups/application/organization-accounts.ts
+++ b/apps/sim/lib/credential-groups/application/organization-accounts.ts
@@ -10,6 +10,10 @@ import {
} from '@/lib/core/application/organization-operation'
import { OrchestrationError } from '@/lib/core/orchestration/types'
import { validateUpdateCredentialGroupInput } from '@/lib/credential-groups/application/validation'
+import { loadScopedAccountsCredentialListContext } from '@/lib/credential-groups/credentials'
+import { CredentialGroupEnrollmentError } from '@/lib/credential-groups/enrollments'
+import { ManagedMcpConnectorError } from '@/lib/credential-groups/managed-mcp-service'
+import { requireOrganizationAccountsSetup } from '@/lib/credential-groups/organization-setup'
import { listConfiguredCredentialGroupProviders } from '@/lib/credential-groups/provider-availability'
import { isScopedCredentialGroupsAvailable } from '@/lib/credential-groups/scoped-availability'
import { createViewerCredentialGroupEnrollment } from '@/lib/credential-groups/self-enrollment'
@@ -22,6 +26,7 @@ import type {
CredentialGroupOptionInput,
UpdateCredentialGroupInput,
} from '@/lib/credential-groups/types'
+import { isKnowledgeMemberAccessAvailable } from '@/lib/knowledge/access/availability'
export const organizationAccountOperations = {
read: defineOrganizationOperation({
@@ -54,14 +59,17 @@ interface OrganizationAccountsInput {
organizationId: string
}
-function defineOrganizationAccountsUseCase<
+export function defineOrganizationAccountsUseCase<
const O extends OrganizationOperation,
I extends OrganizationAccountsInput,
R,
>(definition: {
operation: O
execute(args: { input: I; context: OrganizationMembershipContext }): Promise
- projectAudit?(result: R): { resourceId: string; resourceName: string; description: string } | null
+ projectAudit?(
+ result: NoInfer
+ ): { resourceId: string; resourceName: string; description: string } | null
+ afterSuccess?(args: { result: NoInfer; context: OrganizationMembershipContext }): Promise
}): OperationUseCase {
return {
operation: definition.operation,
@@ -75,7 +83,33 @@ function defineOrganizationAccountsUseCase<
) {
throw new OrchestrationError('not_found', 'Connected accounts are not available')
}
- const result = await definition.execute({ input, context })
+ if (definition.operation.id !== organizationAccountOperations.ensure.id) {
+ const group = await loadScopedAccountsCredentialListContext({
+ kind: 'organization',
+ organizationId: context.organizationId,
+ })
+ if (group)
+ await requireOrganizationAccountsSetup(context.organizationId, group.credentialGroupId)
+ }
+ const result = await definition.execute({ input, context }).catch((error: unknown) => {
+ if (error instanceof ManagedMcpConnectorError)
+ throw new OrchestrationError(
+ error.code === 'bad_gateway' ? 'internal' : error.code,
+ error.message
+ )
+ if (error instanceof CredentialGroupEnrollmentError)
+ throw new OrchestrationError(
+ error.status === 404
+ ? 'not_found'
+ : error.status === 409
+ ? 'conflict'
+ : error.status === 400
+ ? 'validation'
+ : 'internal',
+ error.message
+ )
+ throw error
+ })
const audit = definition.projectAudit?.(result)
if (audit)
recordAudit({
@@ -86,6 +120,7 @@ function defineOrganizationAccountsUseCase<
metadata: { organizationId: context.organizationId },
request,
})
+ await definition.afterSuccess?.({ result, context })
return result
},
}
@@ -98,6 +133,9 @@ export const getOrganizationAccountsSettings = defineOrganizationAccountsUseCase
credentialGroup: await getOrganizationAccountsGroup(context.organizationId),
availableProviders: listConfiguredCredentialGroupProviders(),
canManage: context.role === 'owner' || context.role === 'admin',
+ indexingAvailable: await isKnowledgeMemberAccessAvailable({
+ organizationId: context.organizationId,
+ }),
}
},
})
diff --git a/apps/sim/lib/credential-groups/application/organization-databricks-setup.ts b/apps/sim/lib/credential-groups/application/organization-databricks-setup.ts
new file mode 100644
index 00000000000..9fafe3301fd
--- /dev/null
+++ b/apps/sim/lib/credential-groups/application/organization-databricks-setup.ts
@@ -0,0 +1,32 @@
+import { defineOrganizationOperation } from '@/lib/core/application/organization-operation'
+import { OrchestrationError } from '@/lib/core/orchestration/types'
+import { defineOrganizationAccountsUseCase } from '@/lib/credential-groups/application/organization-accounts'
+import { loadScopedAccountsCredentialListContext } from '@/lib/credential-groups/credentials'
+import { loadOrganizationDatabricksSetup } from '@/lib/credential-groups/managed-mcp-service'
+
+export const getOrganizationDatabricksSetupOperation = defineOrganizationOperation({
+ id: 'organization_accounts.mcp.setup.read',
+ minimumRole: 'admin',
+ principalKinds: ['session'],
+ capability: 'integrations.manage',
+})
+
+export const getOrganizationDatabricksSetup = defineOrganizationAccountsUseCase({
+ operation: getOrganizationDatabricksSetupOperation,
+ async execute({ context }) {
+ const group = await loadScopedAccountsCredentialListContext({
+ kind: 'organization',
+ organizationId: context.organizationId,
+ })
+ if (!group)
+ throw new OrchestrationError(
+ 'not_found',
+ 'Organization connected accounts are not configured'
+ )
+ const server = await loadOrganizationDatabricksSetup(
+ context.organizationId,
+ group.credentialGroupId
+ )
+ return { server }
+ },
+})
diff --git a/apps/sim/lib/credential-groups/application/organization-workspace-access.ts b/apps/sim/lib/credential-groups/application/organization-workspace-access.ts
new file mode 100644
index 00000000000..76ed97340cf
--- /dev/null
+++ b/apps/sim/lib/credential-groups/application/organization-workspace-access.ts
@@ -0,0 +1,63 @@
+import { OrchestrationError } from '@/lib/core/orchestration/types'
+import type { CredentialGroupApplicationContext } from '@/lib/credential-groups/application/authorization'
+import { resolveCredentialGroupWorkspaceContext } from '@/lib/credential-groups/application/context'
+import {
+ organizationAccountAccessPolicyCodec,
+ organizationAccountPolicyAllowsWorkspace,
+} from '@/lib/credential-groups/application/workspace-access-policy'
+import { loadScopedAccountsCredentialListContext } from '@/lib/credential-groups/credentials'
+import { isScopedCredentialGroupsAvailable } from '@/lib/credential-groups/scoped-availability'
+import { requireResourcePolicy } from '@/lib/resource-policies/repository'
+
+export interface OrganizationAccountsWorkspaceContext extends CredentialGroupApplicationContext {
+ organizationId: string
+}
+
+/** Resolves the singleton using the executing workspace's current organization. */
+export async function resolveOrganizationAccountsWorkspaceContext(
+ workspaceId: string
+): Promise {
+ const workspace = await resolveCredentialGroupWorkspaceContext(workspaceId)
+ if (!workspace.workspaceOrganizationId) {
+ throw new OrchestrationError('forbidden', 'This workspace does not belong to an organization')
+ }
+ const group = await loadScopedAccountsCredentialListContext({
+ kind: 'organization',
+ organizationId: workspace.workspaceOrganizationId,
+ })
+ if (!group)
+ throw new OrchestrationError('not_found', 'Organization connected accounts are not configured')
+ return { ...group, ...workspace, organizationId: workspace.workspaceOrganizationId }
+}
+
+/** Uses live policy and ownership; cached selections and deployment snapshots never grant access. */
+export async function requireOrganizationAccountsWorkspaceAccess(context: {
+ workspaceId: string
+ workspaceOrganizationId: string | null
+ organizationId: string
+ credentialGroupId: string
+}): Promise {
+ if (context.organizationId !== context.workspaceOrganizationId) {
+ throw new OrchestrationError('forbidden', 'Connected accounts belong to another organization')
+ }
+ if (
+ !(await isScopedCredentialGroupsAvailable({
+ kind: 'organization',
+ organizationId: context.organizationId,
+ }))
+ ) {
+ throw new OrchestrationError('not_found', 'Organization connected accounts are not available')
+ }
+ const policy = await requireResourcePolicy({
+ organizationId: context.organizationId,
+ resourceType: 'credential_group',
+ resourceId: context.credentialGroupId,
+ codec: organizationAccountAccessPolicyCodec,
+ })
+ if (!organizationAccountPolicyAllowsWorkspace(policy.document, context.workspaceId)) {
+ throw new OrchestrationError(
+ 'forbidden',
+ 'An organization admin must allow this workspace to use connected accounts'
+ )
+ }
+}
diff --git a/apps/sim/lib/credential-groups/application/personal-organization-accounts.test.ts b/apps/sim/lib/credential-groups/application/personal-organization-accounts.test.ts
new file mode 100644
index 00000000000..e4708a87ab4
--- /dev/null
+++ b/apps/sim/lib/credential-groups/application/personal-organization-accounts.test.ts
@@ -0,0 +1,114 @@
+/** @vitest-environment node */
+import type { SessionPrincipal } from '@sim/auth/principal'
+import { dbChainMockFns, queueTableRows, resetDbChainMock, schemaMock } from '@sim/testing'
+import { eq } from 'drizzle-orm'
+import { beforeEach, describe, expect, it, vi } from 'vitest'
+
+const mocks = vi.hoisted(() => ({
+ lock: vi.fn(),
+ evict: vi.fn(),
+ invite: vi.fn(),
+ available: vi.fn(),
+}))
+vi.mock('@/lib/credential-groups/enrollments', () => ({
+ lockCredentialGroupEnrollmentLifecycle: mocks.lock,
+}))
+vi.mock('@/lib/mcp/connection-pool', () => ({ evictMcpServerConnections: mocks.evict }))
+vi.mock('@/lib/credential-groups/self-enrollment', () => ({
+ createViewerCredentialGroupEnrollment: mocks.invite,
+}))
+vi.mock('@/lib/credential-groups/scoped-availability', () => ({
+ isScopedCredentialGroupsAvailable: mocks.available,
+}))
+
+import {
+ disconnectPersonalOrganizationAccount,
+ listPersonalOrganizationAccounts,
+ reconnectPersonalOrganizationAccount,
+} from '@/lib/credential-groups/application/personal-organization-accounts'
+
+const principal: SessionPrincipal = {
+ kind: 'session',
+ userId: 'contributor',
+ sessionId: 'session-1',
+}
+const input = { credentialId: 'mcp-cg-person' }
+const row = {
+ ...input,
+ displayName: 'Fireflies',
+ providerId: null,
+ type: 'managed_mcp',
+ status: 'active',
+ organizationId: 'org-1',
+ organizationName: 'Acme',
+ groupId: 'group-1',
+ groupStatus: 'active',
+ enrollmentId: 'enrollment-1',
+ enrollmentStatus: 'completed',
+ optionId: null,
+ mcpProvider: 'fireflies',
+}
+
+describe('personal organization contributions', () => {
+ beforeEach(() => {
+ vi.clearAllMocks()
+ resetDbChainMock()
+ mocks.available.mockResolvedValue(true)
+ mocks.invite.mockResolvedValue({ invitationLink: 'https://sim.test/enroll/token' })
+ })
+
+ it('lists only the stable signed-in identity without requiring org membership', async () => {
+ queueTableRows(schemaMock.credential, [row])
+ const result = await listPersonalOrganizationAccounts.execute({ principal, input: {} })
+ expect(result.accounts).toEqual([
+ expect.objectContaining({
+ credentialId: 'mcp-cg-person',
+ providerId: 'fireflies',
+ canReconnect: true,
+ }),
+ ])
+ expect(eq).toHaveBeenCalledWith(schemaMock.credentialGroupEnrollment.userId, 'contributor')
+ expect(dbChainMockFns.from).not.toHaveBeenCalledWith(schemaMock.member)
+ })
+
+ it('refuses disconnect of another contributor’s account', async () => {
+ queueTableRows(schemaMock.credential, [])
+ await expect(
+ disconnectPersonalOrganizationAccount.execute({ principal, input })
+ ).rejects.toMatchObject({ code: 'not_found' })
+ expect(dbChainMockFns.update).not.toHaveBeenCalled()
+ })
+
+ it('rechecks ownership under the lifecycle lock before revoking', async () => {
+ queueTableRows(schemaMock.credential, [row])
+ queueTableRows(schemaMock.credential, [])
+ await expect(
+ disconnectPersonalOrganizationAccount.execute({ principal, input })
+ ).rejects.toMatchObject({ code: 'not_found' })
+ expect(mocks.lock).toHaveBeenCalledWith(expect.any(Object), 'enrollment-1')
+ expect(dbChainMockFns.update).not.toHaveBeenCalled()
+ })
+
+ it('revokes locally and invalidates pending attempts even with the flag off', async () => {
+ queueTableRows(schemaMock.credential, [row])
+ queueTableRows(schemaMock.credential, [row])
+ mocks.available.mockResolvedValue(false)
+ await disconnectPersonalOrganizationAccount.execute({ principal, input })
+ expect(dbChainMockFns.set).toHaveBeenCalledWith(
+ expect.objectContaining({ managedOauthStatus: 'revoked', revokedAt: expect.any(Date) })
+ )
+ expect(dbChainMockFns.set).toHaveBeenCalledWith(
+ expect.objectContaining({ invitationTokenHash: expect.any(String) })
+ )
+ expect(mocks.evict).toHaveBeenCalledWith('mcp-cg-person', expect.any(String))
+ expect(mocks.available).not.toHaveBeenCalled()
+ })
+
+ it('does not restore an enrollment revoked by its administrator', async () => {
+ queueTableRows(schemaMock.credential, [{ ...row, enrollmentStatus: 'revoked' }])
+ await expect(
+ reconnectPersonalOrganizationAccount.execute({ principal, input })
+ ).rejects.toMatchObject({ code: 'forbidden' })
+ expect(mocks.invite).not.toHaveBeenCalled()
+ })
+})
diff --git a/apps/sim/lib/credential-groups/application/personal-organization-accounts.ts b/apps/sim/lib/credential-groups/application/personal-organization-accounts.ts
new file mode 100644
index 00000000000..243643c45d6
--- /dev/null
+++ b/apps/sim/lib/credential-groups/application/personal-organization-accounts.ts
@@ -0,0 +1,203 @@
+import { AuditAction, AuditResourceType } from '@sim/audit'
+import type { SessionPrincipal } from '@sim/auth/principal'
+import { db } from '@sim/db'
+import {
+ credential,
+ credentialGroup,
+ credentialGroupEnrollment,
+ mcpServers,
+ organization,
+} from '@sim/db/schema'
+import { sha256Hex } from '@sim/security/hash'
+import { generateId } from '@sim/utils/id'
+import { and, asc, eq, gt, inArray, isNotNull } from 'drizzle-orm'
+import { defineOperation } from '@/lib/core/application'
+import { OrchestrationError } from '@/lib/core/orchestration/types'
+import { sameResourceScopeCondition } from '@/lib/core/resource-scope.server'
+import { lockCredentialGroupEnrollmentLifecycle } from '@/lib/credential-groups/enrollments'
+import { isScopedCredentialGroupsAvailable } from '@/lib/credential-groups/scoped-availability'
+import { createViewerCredentialGroupEnrollment } from '@/lib/credential-groups/self-enrollment'
+import { defineAuthorizedCredentialUserUseCase } from '@/lib/credentials/application/authorized-user-use-case'
+import type { DbOrTx } from '@/lib/db/types'
+import { evictMcpServerConnections } from '@/lib/mcp/connection-pool'
+
+/** Contributors retain control of their own grants even without organization or workspace membership. */
+export const personalOrganizationAccountOperations = {
+ /**
+ * permission-group-exempt: Contributors retain control of their own grants after organization membership ends.
+ */
+ list: defineOperation({
+ id: 'organization_accounts.personal.list',
+ principalKinds: ['session'],
+ capability: 'none',
+ }),
+ /**
+ * permission-group-exempt: Contributors retain control of their own grants after organization membership ends.
+ */
+ reconnect: defineOperation({
+ id: 'organization_accounts.personal.reconnect',
+ principalKinds: ['session'],
+ capability: 'none',
+ }),
+ /**
+ * permission-group-exempt: Contributors retain control of their own grants after organization membership ends.
+ */
+ disconnect: defineOperation({
+ id: 'organization_accounts.personal.disconnect',
+ principalKinds: ['session'],
+ capability: 'none',
+ }),
+} as const
+
+function ownAccounts(
+ userId: string,
+ input: { credentialId?: string; cursor?: string },
+ executor: DbOrTx = db
+) {
+ return executor
+ .select({
+ credentialId: credential.id,
+ displayName: credential.displayName,
+ providerId: credential.providerId,
+ type: credential.type,
+ status: credential.managedOauthStatus,
+ organizationId: organization.id,
+ organizationName: organization.name,
+ groupId: credentialGroup.id,
+ groupStatus: credentialGroup.status,
+ enrollmentId: credentialGroupEnrollment.id,
+ enrollmentStatus: credentialGroupEnrollment.status,
+ optionId: credential.credentialGroupOptionId,
+ mcpProvider: mcpServers.managedConnectorId,
+ })
+ .from(credential)
+ .innerJoin(
+ credentialGroupEnrollment,
+ eq(credentialGroupEnrollment.id, credential.credentialGroupEnrollmentId)
+ )
+ .innerJoin(credentialGroup, eq(credentialGroup.id, credentialGroupEnrollment.credentialGroupId))
+ .innerJoin(organization, eq(organization.id, credentialGroup.organizationId))
+ .leftJoin(mcpServers, eq(mcpServers.id, credential.mcpServerId))
+ .where(
+ and(
+ eq(credentialGroupEnrollment.userId, userId),
+ sameResourceScopeCondition(credential, credentialGroup),
+ isNotNull(credential.organizationId),
+ inArray(credential.type, ['managed_oauth', 'managed_mcp']),
+ input.credentialId ? eq(credential.id, input.credentialId) : undefined,
+ input.cursor ? gt(credential.id, input.cursor) : undefined
+ )
+ )
+ .orderBy(asc(credential.id))
+ .limit(input.credentialId ? 1 : 51)
+}
+
+export const listPersonalOrganizationAccounts = defineAuthorizedCredentialUserUseCase({
+ operation: personalOrganizationAccountOperations.list,
+ async execute({ principal, input }: { principal: SessionPrincipal; input: { cursor?: string } }) {
+ const rows = await ownAccounts(principal.userId, input)
+ const page = rows.slice(0, 50)
+ return {
+ accounts: page.map((row) => {
+ const providerId = row.type === 'managed_mcp' ? row.mcpProvider : row.providerId
+ if (!providerId || !row.status)
+ throw new Error('Organization account identity is incomplete')
+ return {
+ credentialId: row.credentialId,
+ displayName: row.displayName,
+ providerId,
+ kind: row.type === 'managed_mcp' ? ('mcp' as const) : ('oauth' as const),
+ status: row.status,
+ organizationId: row.organizationId,
+ organizationName: row.organizationName,
+ enrollmentStatus: row.enrollmentStatus,
+ canReconnect: row.groupStatus === 'active' && row.enrollmentStatus !== 'revoked',
+ }
+ }),
+ nextCursor: rows.length > 50 ? page.at(-1)!.credentialId : null,
+ }
+ },
+})
+
+export const reconnectPersonalOrganizationAccount = defineAuthorizedCredentialUserUseCase({
+ operation: personalOrganizationAccountOperations.reconnect,
+ async execute({
+ principal,
+ input,
+ }: {
+ principal: SessionPrincipal
+ input: { credentialId: string }
+ }) {
+ const [account] = await ownAccounts(principal.userId, input)
+ if (!account) throw new OrchestrationError('not_found', 'Connected account not found')
+ if (account.groupStatus !== 'active' || account.enrollmentStatus === 'revoked')
+ throw new OrchestrationError(
+ 'forbidden',
+ 'An organization admin must restore your access before you reconnect'
+ )
+ if (
+ !(await isScopedCredentialGroupsAvailable({
+ kind: 'organization',
+ organizationId: account.organizationId,
+ }))
+ )
+ throw new OrchestrationError('forbidden', 'Organization connected accounts are unavailable')
+ const { invitationLink } = await createViewerCredentialGroupEnrollment({
+ organizationId: account.organizationId,
+ credentialGroupId: account.groupId,
+ userId: principal.userId,
+ })
+ const url = new URL(invitationLink)
+ if (account.optionId) url.searchParams.set('optionId', account.optionId)
+ return { invitationLink: url.toString() }
+ },
+})
+
+export const disconnectPersonalOrganizationAccount = defineAuthorizedCredentialUserUseCase({
+ operation: personalOrganizationAccountOperations.disconnect,
+ async execute({
+ principal,
+ input,
+ }: {
+ principal: SessionPrincipal
+ input: { credentialId: string }
+ }) {
+ const [account] = await ownAccounts(principal.userId, input)
+ if (!account) throw new OrchestrationError('not_found', 'Connected account not found')
+ await db.transaction(async (tx) => {
+ await lockCredentialGroupEnrollmentLifecycle(tx, account.enrollmentId)
+ const [current] = await ownAccounts(principal.userId, input, tx)
+ if (!current || current.enrollmentId !== account.enrollmentId)
+ throw new OrchestrationError('not_found', 'Connected account not found')
+ await tx
+ .update(credential)
+ .set({ managedOauthStatus: 'revoked', revokedAt: new Date(), updatedAt: new Date() })
+ .where(eq(credential.id, current.credentialId))
+ await tx
+ .update(credentialGroupEnrollment)
+ .set({ invitationTokenHash: sha256Hex(generateId()), updatedAt: new Date() })
+ .where(eq(credentialGroupEnrollment.id, current.enrollmentId))
+ })
+ return {
+ success: true as const,
+ credentialId: account.credentialId,
+ organizationId: account.organizationId,
+ managedMcp: account.type === 'managed_mcp',
+ }
+ },
+ projectAudit: ({ input, result }) => ({
+ workspaceId: null,
+ action: AuditAction.CREDENTIAL_UPDATED,
+ resourceType: AuditResourceType.CREDENTIAL,
+ resourceId: input.credentialId,
+ description: 'Disconnected own organization account',
+ metadata: { organizationId: result.organizationId },
+ }),
+ afterSuccess: async ({ result }) => {
+ if (result.managedMcp)
+ await evictMcpServerConnections(
+ result.credentialId,
+ 'contributor disconnected organization account'
+ )
+ },
+})
diff --git a/apps/sim/lib/credential-groups/application/public-enrollment.test.ts b/apps/sim/lib/credential-groups/application/public-enrollment.test.ts
index 32ff083b2ae..97b50175865 100644
--- a/apps/sim/lib/credential-groups/application/public-enrollment.test.ts
+++ b/apps/sim/lib/credential-groups/application/public-enrollment.test.ts
@@ -6,6 +6,7 @@ import { sha256Hex } from '@sim/security/hash'
import { beforeEach, describe, expect, it, vi } from 'vitest'
const mocks = vi.hoisted(() => ({
+ bind: vi.fn(),
completeEnrollment: vi.fn(),
completeOAuth: vi.fn(),
fireTrigger: vi.fn(),
@@ -20,6 +21,7 @@ const mocks = vi.hoisted(() => ({
}))
vi.mock('@/lib/credential-groups/enrollments', () => ({
+ bindCredentialGroupEnrollmentUser: mocks.bind,
completeAuthorizedCredentialGroupEnrollment: mocks.completeEnrollment,
getAuthorizedCredentialGroupMcpOAuthContext: mocks.getMcpOAuthContext,
getAuthorizedCredentialGroupOAuthContext: mocks.getOAuthContext,
@@ -54,21 +56,24 @@ import {
const invitationToken = 'invitation-token'
const principal: CredentialGroupEnrollmentPrincipal = {
kind: 'credential_group_enrollment',
- workspaceId: 'workspace-1',
+ userId: 'user-1',
+ organizationId: 'org-1',
credentialGroupId: 'group-1',
enrollmentId: 'enrollment-1',
email: 'person@example.com',
invitationTokenHash: sha256Hex(invitationToken),
}
const identity = {
- workspaceId: principal.workspaceId,
+ organizationId: principal.organizationId,
+ userId: principal.userId,
credentialGroupId: principal.credentialGroupId,
enrollmentId: principal.enrollmentId,
email: principal.email,
invitationTokenHash: principal.invitationTokenHash,
}
const oauthAttempt = {
- workspaceId: principal.workspaceId,
+ organizationId: principal.organizationId,
+ userId: principal.userId,
email: principal.email,
state: 'state-1',
provider: 'gmail' as const,
@@ -87,6 +92,7 @@ const oauthAttempt = {
describe('public Credential Group enrollment application operations', () => {
beforeEach(() => {
vi.clearAllMocks()
+ mocks.bind.mockResolvedValue(undefined)
mocks.getEnrollment.mockResolvedValue({
status: 'invited',
credentialGroupName: 'Credential Group',
@@ -235,7 +241,7 @@ describe('public Credential Group enrollment application operations', () => {
expect(result).toEqual({ completed: true })
expect(mocks.fireTrigger).toHaveBeenCalledWith({
event: 'form_submitted',
- workspaceId: 'workspace-1',
+ organizationId: 'org-1',
credentialGroupId: 'group-1',
credentialGroupName: 'Credential Group',
enrollmentId: 'enrollment-1',
@@ -297,30 +303,19 @@ describe('public Credential Group enrollment application operations', () => {
expect.objectContaining({ event: 'credential_reconnected', enrollmentStatus: 'completed' })
)
})
- it('completes a consumed attempt after invitation rotation without granting new public read authority', async () => {
- mocks.getOAuthContext.mockResolvedValue(null)
- mocks.getEnrollment.mockResolvedValue(null)
+ it('rejects a consumed attempt after invitation rotation before exchanging its code', async () => {
+ mocks.bind.mockRejectedValue(new Error('Invitation is invalid or expired'))
await expect(
completePublicCredentialGroupOAuth.execute({
principal,
input: { attempt: oauthAttempt, code: 'code' },
})
- ).resolves.toEqual({ connectedOptionId: 'option-1' })
- expect(mocks.getAttemptContext).toHaveBeenCalledWith(identity, 'option-1')
- expect(mocks.getOAuthContext).not.toHaveBeenCalled()
- await expect(
- readPublicCredentialGroupEnrollment.execute({ principal, input: {} })
- ).rejects.toMatchObject({ code: 'not_found' })
- await expect(
- startPublicCredentialGroupOAuth.execute({
- principal,
- input: { invitationToken, optionId: 'option-1' },
- })
- ).rejects.toMatchObject({ code: 'not_found' })
- expect(mocks.startOAuth).not.toHaveBeenCalled()
+ ).rejects.toThrow('Invitation is invalid or expired')
+ expect(mocks.completeOAuth).not.toHaveBeenCalled()
})
it.each([
- { workspaceId: 'other' },
+ { organizationId: 'other' },
+ { userId: 'other-user' },
{ credentialGroupId: 'other' },
{ enrollmentId: 'other' },
{ email: 'other@example.com' },
@@ -346,30 +341,90 @@ describe('public Credential Group enrollment application operations', () => {
expect(mocks.completeOAuth).not.toHaveBeenCalled()
expect(mocks.fireTrigger).not.toHaveBeenCalled()
})
- it('completes a pinned MCP attempt after another account rotates its invitation, but cannot restart', async () => {
- const attempt = { ...oauthAttempt, mcpServerId: 'mcp-server-1', codeVerifier: 'verifier' }
- mocks.getMcpOAuthContext.mockResolvedValue(null)
- mocks.getMcpAttemptContext.mockResolvedValue({ server: { id: 'mcp-server-1' } })
+ it('emits the personal MCP connection ID after completing the current configuration', async () => {
+ const attempt = {
+ ...oauthAttempt,
+ mcpServerId: 'mcp-server-1',
+ oauthConfigVersion: 2,
+ codeVerifier: 'verifier',
+ }
+ mocks.getMcpAttemptContext.mockResolvedValue({
+ credentialGroupName: 'Accounts',
+ server: {
+ id: 'mcp-server-1',
+ connectorId: 'fireflies',
+ name: 'Fireflies',
+ oauthConfigVersion: 2,
+ },
+ })
mocks.completeMcpOAuth.mockResolvedValue({
- connectionId: 'connection',
+ connectionId: 'mcp-cg-person',
mcpServerId: 'mcp-server-1',
+ created: true,
+ enrollmentStatus: 'in_progress',
})
await expect(
completePublicCredentialGroupMcpOAuth.execute({ principal, input: { attempt, code: 'code' } })
- ).resolves.toEqual({ connectionId: 'connection', mcpServerId: 'mcp-server-1' })
- expect(mocks.getMcpAttemptContext).toHaveBeenCalledWith(identity, 'mcp-server-1')
- expect(mocks.getMcpOAuthContext).not.toHaveBeenCalled()
+ ).resolves.toEqual({ connectionId: 'mcp-cg-person', mcpServerId: 'mcp-server-1' })
+ expect(mocks.completeMcpOAuth).toHaveBeenCalledWith(
+ expect.any(Object),
+ 'verifier',
+ 'code',
+ invitationToken
+ )
+ expect(mocks.fireTrigger).toHaveBeenCalledWith(
+ expect.objectContaining({
+ event: 'credential_added',
+ organizationId: 'org-1',
+ credential: expect.objectContaining({
+ credentialId: 'mcp-cg-person',
+ mcpServerId: 'mcp-server-1',
+ }),
+ })
+ )
+ })
+
+ it('rejects a changed MCP configuration before exchanging the code', async () => {
+ mocks.getMcpAttemptContext.mockResolvedValue({ server: { oauthConfigVersion: 3 } })
await expect(
- startPublicCredentialGroupMcpOAuth.execute({
+ completePublicCredentialGroupMcpOAuth.execute({
principal,
- input: { invitationToken, mcpServerId: 'mcp-server-1' },
+ input: {
+ attempt: {
+ ...oauthAttempt,
+ mcpServerId: 'mcp-server-1',
+ oauthConfigVersion: 2,
+ codeVerifier: 'verifier',
+ },
+ code: 'code',
+ },
})
- ).rejects.toMatchObject({ code: 'not_found' })
- expect(mocks.startMcpOAuth).not.toHaveBeenCalled()
+ ).rejects.toMatchObject({ code: 'conflict' })
+ expect(mocks.completeMcpOAuth).not.toHaveBeenCalled()
+ })
+
+ it('rejects a pending MCP attempt after disconnect rotates its invitation', async () => {
+ mocks.bind.mockRejectedValue(new Error('Invitation is invalid or expired'))
+ await expect(
+ completePublicCredentialGroupMcpOAuth.execute({
+ principal,
+ input: {
+ attempt: {
+ ...oauthAttempt,
+ mcpServerId: 'mcp-server-1',
+ oauthConfigVersion: 2,
+ codeVerifier: 'verifier',
+ },
+ code: 'code',
+ },
+ })
+ ).rejects.toThrow('Invitation is invalid or expired')
+ expect(mocks.completeMcpOAuth).not.toHaveBeenCalled()
})
it.each([
- 'workspaceId',
+ 'organizationId',
+ 'userId',
'credentialGroupId',
'enrollmentId',
'email',
diff --git a/apps/sim/lib/credential-groups/application/public-enrollment.ts b/apps/sim/lib/credential-groups/application/public-enrollment.ts
index 08b00747838..a132e443115 100644
--- a/apps/sim/lib/credential-groups/application/public-enrollment.ts
+++ b/apps/sim/lib/credential-groups/application/public-enrollment.ts
@@ -10,6 +10,7 @@ import {
} from '@/lib/core/resource-scope'
import { credentialGroupEnrollmentOperations } from '@/lib/credential-groups/application/enrollment-operations'
import {
+ bindCredentialGroupEnrollmentUser,
completeAuthorizedCredentialGroupEnrollment,
getAuthorizedCredentialGroupMcpOAuthContext,
getAuthorizedCredentialGroupOAuthContext,
@@ -40,7 +41,7 @@ interface AuthorizedCredentialGroupEnrollmentUseCaseDefinition {
function requireCredentialGroupEnrollmentPrincipal(
principal: Principal
): asserts principal is CredentialGroupEnrollmentPrincipal {
- if (principal.kind !== 'credential_group_enrollment') {
+ if (principal.kind !== 'credential_group_enrollment' || !principal.userId?.trim()) {
throw new OrchestrationError(
'forbidden',
'This operation requires a Credential Group invitation'
@@ -57,6 +58,7 @@ function requireMatchingContext(
context.credentialGroupId !== principal.credentialGroupId ||
context.enrollmentId !== principal.enrollmentId ||
context.email !== principal.email ||
+ context.userId !== principal.userId ||
!safeCompare(context.invitationTokenHash, principal.invitationTokenHash)
) {
throw new OrchestrationError('not_found', 'Invitation is invalid or expired')
@@ -74,6 +76,7 @@ function defineAuthorizedCredentialGroupEnrollmentUseCase<
): OperationUseCase {
async function authorize(principal: Principal, input: I) {
requireCredentialGroupEnrollmentPrincipal(principal)
+ await bindCredentialGroupEnrollmentUser(identityFromPrincipal(principal), principal.userId)
const context = await definition.resolveContext({ principal, input })
requireMatchingContext(principal, context)
return { principal, input, context }
@@ -100,6 +103,7 @@ function identityFromPrincipal(
enrollmentId: principal.enrollmentId,
email: principal.email,
invitationTokenHash: principal.invitationTokenHash,
+ userId: principal.userId,
}
}
@@ -151,10 +155,10 @@ export const completePublicCredentialGroupEnrollment =
resolveContext: ({ principal }) => resolvePublicEnrollmentContext(principal),
async execute({ context }) {
const completion = await completeAuthorizedCredentialGroupEnrollment(context)
- if (completion?.transitioned && context.workspaceId) {
+ if (completion?.transitioned && context.organizationId) {
await fireCredentialGroupTrigger({
event: 'form_submitted',
- workspaceId: context.workspaceId,
+ organizationId: context.organizationId,
credentialGroupId: context.credentialGroupId,
credentialGroupName: context.enrollment.credentialGroupName,
enrollmentId: context.enrollmentId,
@@ -220,6 +224,7 @@ function identityForOAuthAttempt(
| 'enrollmentId'
| 'email'
| 'invitationToken'
+ | 'userId'
>
): PublicCredentialGroupEnrollmentIdentity {
requireInvitationToken(principal, attempt.invitationToken)
@@ -227,7 +232,8 @@ function identityForOAuthAttempt(
!sameResourceScope(resourceScopeFromOwner(attempt), resourceScopeFromOwner(principal)) ||
attempt.email !== principal.email ||
attempt.credentialGroupId !== principal.credentialGroupId ||
- attempt.enrollmentId !== principal.enrollmentId
+ attempt.enrollmentId !== principal.enrollmentId ||
+ attempt.userId !== principal.userId
) {
throw new OrchestrationError('not_found', 'Authorization state does not match this enrollment')
}
@@ -256,10 +262,10 @@ export const completePublicCredentialGroupOAuth = defineAuthorizedCredentialGrou
async execute({ principal, input, context }) {
requireInvitationToken(principal, input.attempt.invitationToken)
const completion = await completeCredentialGroupOAuth(context.oauth, input.attempt, input.code)
- if (context.workspaceId)
+ if (context.organizationId)
await fireCredentialGroupTrigger({
event: completion.created ? 'credential_added' : 'credential_reconnected',
- workspaceId: context.workspaceId,
+ organizationId: context.organizationId,
credentialGroupId: context.credentialGroupId,
credentialGroupName: context.oauth.credentialGroupName,
enrollmentId: context.enrollmentId,
@@ -334,10 +340,36 @@ export const completePublicCredentialGroupMcpOAuth =
input.attempt.mcpServerId
)
if (!oauth) throw new CredentialGroupInvitationUnavailableError()
+ if (oauth.server.oauthConfigVersion !== input.attempt.oauthConfigVersion)
+ throw new OrchestrationError('conflict', 'MCP setup changed. Start authorization again.')
return { ...identity, oauth }
},
async execute({ principal, input, context }) {
requireInvitationToken(principal, input.attempt.invitationToken)
- return completeCredentialGroupMcpOAuth(context.oauth, input.attempt.codeVerifier, input.code)
+ const completion = await completeCredentialGroupMcpOAuth(
+ context.oauth,
+ input.attempt.codeVerifier,
+ input.code,
+ input.attempt.invitationToken
+ )
+ if (context.organizationId)
+ await fireCredentialGroupTrigger({
+ event: completion.created ? 'credential_added' : 'credential_reconnected',
+ organizationId: context.organizationId,
+ credentialGroupId: context.credentialGroupId,
+ credentialGroupName: context.oauth.credentialGroupName,
+ enrollmentId: context.enrollmentId,
+ email: context.email,
+ enrollmentStatus: completion.enrollmentStatus,
+ credential: {
+ credentialId: completion.connectionId,
+ credentialGroupOptionId: null,
+ mcpServerId: completion.mcpServerId,
+ provider: context.oauth.server.connectorId,
+ providerId: context.oauth.server.connectorId,
+ displayName: context.oauth.server.name,
+ },
+ })
+ return { connectionId: completion.connectionId, mcpServerId: completion.mcpServerId }
},
})
diff --git a/apps/sim/lib/credential-groups/application/slack-managed-users.test.ts b/apps/sim/lib/credential-groups/application/slack-managed-users.test.ts
index fe6c49ec391..75514610292 100644
--- a/apps/sim/lib/credential-groups/application/slack-managed-users.test.ts
+++ b/apps/sim/lib/credential-groups/application/slack-managed-users.test.ts
@@ -15,6 +15,10 @@ const mocks = vi.hoisted(() => ({
consume: vi.fn(),
exchange: vi.fn(),
}))
+vi.mock('@/lib/credential-groups/organization-setup', () => ({
+ requireOrganizationAccountsSetup: vi.fn().mockResolvedValue(undefined),
+}))
+
vi.mock('@sim/audit', () => auditMock)
vi.mock('@/lib/core/application/organization-authorization', () => ({
requireOrganizationMembership: mocks.organizationAccess,
diff --git a/apps/sim/lib/credential-groups/application/slack-managed-users.ts b/apps/sim/lib/credential-groups/application/slack-managed-users.ts
index 83f9af9c991..a18f42c8e82 100644
--- a/apps/sim/lib/credential-groups/application/slack-managed-users.ts
+++ b/apps/sim/lib/credential-groups/application/slack-managed-users.ts
@@ -15,6 +15,7 @@ import {
sameResourceScope,
} from '@/lib/core/resource-scope'
import { credentialGroupOperations } from '@/lib/credential-groups/application/operations'
+import { requireOrganizationAccountsSetup } from '@/lib/credential-groups/organization-setup'
import { credentialGroupScopePolicyVersion } from '@/lib/credential-groups/provider-adapter'
import { isScopedCredentialGroupsAvailable } from '@/lib/credential-groups/scoped-availability'
import {
@@ -70,7 +71,9 @@ export interface StartSlackCredentialGroupConfigurationInput {
assertedWorkspaceId?: string
organizationId?: string
credentialGroupId: string
- slackBotCredentialId: string
+ slackBotCredentialId?: string
+ appId?: string
+ teamId?: string
clientId: string
clientSecret: string
requiredScopes?: string[]
@@ -96,11 +99,15 @@ export const startSlackCredentialGroupConfiguration: OperationUseCase<
credentialGroupOperations.startSlackConfiguration,
scope
)
+ if (scope.kind === 'organization')
+ await requireOrganizationAccountsSetup(scope.organizationId, input.credentialGroupId)
return createSlackManagedUsersAttempt({
...resourceScopeFields(scope),
userId: principal.userId,
credentialGroupId: input.credentialGroupId,
slackBotCredentialId: input.slackBotCredentialId,
+ appId: input.appId,
+ teamId: input.teamId,
clientId: input.clientId,
clientSecret: input.clientSecret,
requiredScopes: input.requiredScopes,
@@ -148,6 +155,8 @@ export const completeSlackCredentialGroupConfiguration: OperationUseCase<
credentialGroupOperations.completeSlackConfiguration,
scope
)
+ if (scope.kind === 'organization')
+ await requireOrganizationAccountsSetup(scope.organizationId, pending.credentialGroupId)
const attempt = await consumeSlackManagedUsersAttempt(input.state)
if (
!attempt ||
diff --git a/apps/sim/lib/credential-groups/application/validation.ts b/apps/sim/lib/credential-groups/application/validation.ts
index bec8f12898b..ffb0d74e664 100644
--- a/apps/sim/lib/credential-groups/application/validation.ts
+++ b/apps/sim/lib/credential-groups/application/validation.ts
@@ -23,7 +23,11 @@ function validateOption(
`Credential option ${index + 1} requires a label of at most 100 characters`
)
}
- if (option.provider === 'slack' && !option.slackBotCredentialId.trim()) {
+ if (
+ option.provider === 'slack' &&
+ option.slackBotCredentialId !== undefined &&
+ !option.slackBotCredentialId.trim()
+ ) {
throw new OrchestrationError('validation', 'Select a custom Slack bot')
}
}
diff --git a/apps/sim/lib/credential-groups/application/workspace-access-policy.test.ts b/apps/sim/lib/credential-groups/application/workspace-access-policy.test.ts
new file mode 100644
index 00000000000..c29a825eab1
--- /dev/null
+++ b/apps/sim/lib/credential-groups/application/workspace-access-policy.test.ts
@@ -0,0 +1,54 @@
+/** @vitest-environment node */
+import { describe, expect, it } from 'vitest'
+import {
+ buildOrganizationAccountAccessPolicy,
+ listOrganizationAccountWorkspaceIds,
+ organizationAccountAccessPolicyCodec,
+ organizationAccountPolicyAllowsWorkspace,
+} from '@/lib/credential-groups/application/workspace-access-policy'
+
+describe('organization account workspace policy', () => {
+ it('denies every workspace by default', () => {
+ const policy = buildOrganizationAccountAccessPolicy('group-1', [])
+ expect(organizationAccountPolicyAllowsWorkspace(policy, 'workspace-1')).toBe(false)
+ })
+
+ it('grants only selected workspaces without a workflow or deployment condition', () => {
+ const policy = buildOrganizationAccountAccessPolicy('group-1', ['workspace-2', 'workspace-1'])
+ expect(listOrganizationAccountWorkspaceIds(policy)).toEqual(['workspace-1', 'workspace-2'])
+ expect(organizationAccountPolicyAllowsWorkspace(policy, 'workspace-1')).toBe(true)
+ expect(organizationAccountPolicyAllowsWorkspace(policy, 'workspace-3')).toBe(false)
+ expect(policy.statements[0]).not.toHaveProperty('condition')
+ })
+
+ it('rejects policies naming a different group', () => {
+ expect(() =>
+ organizationAccountAccessPolicyCodec.parse(
+ buildOrganizationAccountAccessPolicy('group-2', []),
+ { type: 'credential_group', id: 'group-1' }
+ )
+ ).toThrow('canonical group')
+ })
+
+ it('rejects workflow grants and indexing grants', () => {
+ for (const principal of [
+ { type: 'workflow', workflowId: 'workflow-1' },
+ { type: 'knowledge_connector', connectorId: 'connector-1' },
+ ]) {
+ const policy = buildOrganizationAccountAccessPolicy('group-1', ['workspace-1'])
+ expect(() =>
+ organizationAccountAccessPolicyCodec.parse(
+ { ...policy, statements: [{ ...policy.statements[0], principals: [principal] }] },
+ { type: 'credential_group', id: 'group-1' }
+ )
+ ).toThrow()
+ }
+ })
+
+ it('rejects duplicate selections and malformed IDs', () => {
+ expect(() =>
+ buildOrganizationAccountAccessPolicy('group-1', ['workspace-1', 'workspace-1'])
+ ).toThrow()
+ expect(() => buildOrganizationAccountAccessPolicy('group-1', [' workspace-1 '])).toThrow()
+ })
+})
diff --git a/apps/sim/lib/credential-groups/application/workspace-access-policy.ts b/apps/sim/lib/credential-groups/application/workspace-access-policy.ts
new file mode 100644
index 00000000000..dd9af53ba90
--- /dev/null
+++ b/apps/sim/lib/credential-groups/application/workspace-access-policy.ts
@@ -0,0 +1,99 @@
+import { z } from 'zod'
+import { ORGANIZATION_ACCOUNT_WORKSPACE_LIMIT } from '@/lib/credential-groups/limits'
+import { evaluateResourcePolicy } from '@/lib/resource-policies/evaluator'
+import { workspaceResourcePolicyPrincipalSchema } from '@/lib/resource-policies/principals/workspace'
+import { CREDENTIAL_GROUP_CREDENTIAL_USE_ACTION } from '@/lib/resource-policies/registry'
+import type { ResourcePolicyCodec } from '@/lib/resource-policies/types'
+
+export const organizationAccountWorkspaceIdsSchema = z
+ .array(workspaceResourcePolicyPrincipalSchema.shape.workspaceId)
+ .max(ORGANIZATION_ACCOUNT_WORKSPACE_LIMIT)
+ .refine((ids) => new Set(ids).size === ids.length, 'Workspace IDs must be unique')
+
+const workspaceAccessStatementSchema = z
+ .object({
+ sid: z.literal('WorkspaceCredentialAccess'),
+ effect: z.literal('allow'),
+ actions: z.tuple([z.literal(CREDENTIAL_GROUP_CREDENTIAL_USE_ACTION)]),
+ principals: z
+ .array(workspaceResourcePolicyPrincipalSchema)
+ .min(1)
+ .max(ORGANIZATION_ACCOUNT_WORKSPACE_LIMIT),
+ })
+ .strict()
+ .refine(
+ ({ principals }) =>
+ principals.every(
+ (principal, index) =>
+ index === 0 || principals[index - 1].workspaceId < principal.workspaceId
+ ),
+ 'Workspace principals must be sorted and unique'
+ )
+
+export const organizationAccountAccessPolicySchema = z
+ .object({
+ version: z.literal(2),
+ resource: z
+ .object({ type: z.literal('credential_group'), id: z.string().min(1).max(128) })
+ .strict(),
+ statements: z.array(workspaceAccessStatementSchema).max(1),
+ })
+ .strict()
+
+export type OrganizationAccountAccessPolicy = z.output
+
+export const organizationAccountAccessPolicyCodec: ResourcePolicyCodec<
+ 'credential_group',
+ OrganizationAccountAccessPolicy
+> = {
+ resourceType: 'credential_group',
+ parse(value, expected) {
+ const document = organizationAccountAccessPolicySchema.parse(value)
+ if (document.resource.type !== expected.type || document.resource.id !== expected.id) {
+ throw new Error('Connected accounts policy does not match its canonical group')
+ }
+ return document
+ },
+}
+
+export function buildOrganizationAccountAccessPolicy(
+ credentialGroupId: string,
+ workspaceIds: string[]
+): OrganizationAccountAccessPolicy {
+ const ids = organizationAccountWorkspaceIdsSchema.parse(workspaceIds).sort()
+ return organizationAccountAccessPolicySchema.parse({
+ version: 2,
+ resource: { type: 'credential_group', id: credentialGroupId },
+ statements: ids.length
+ ? [
+ {
+ sid: 'WorkspaceCredentialAccess',
+ effect: 'allow',
+ actions: [CREDENTIAL_GROUP_CREDENTIAL_USE_ACTION],
+ principals: ids.map((workspaceId) => ({ type: 'workspace', workspaceId })),
+ },
+ ]
+ : [],
+ })
+}
+
+export function listOrganizationAccountWorkspaceIds(
+ document: OrganizationAccountAccessPolicy
+): string[] {
+ return document.statements.flatMap((statement) =>
+ statement.principals.map((principal) => principal.workspaceId)
+ )
+}
+
+export function organizationAccountPolicyAllowsWorkspace(
+ document: OrganizationAccountAccessPolicy,
+ workspaceId: string
+): boolean {
+ return (
+ evaluateResourcePolicy({
+ document,
+ action: CREDENTIAL_GROUP_CREDENTIAL_USE_ACTION,
+ facts: { currentWorkspaceId: workspaceId },
+ }).decision === 'allow'
+ )
+}
diff --git a/apps/sim/lib/credential-groups/application/workspace-organization-accounts.ts b/apps/sim/lib/credential-groups/application/workspace-organization-accounts.ts
new file mode 100644
index 00000000000..c33cf518f70
--- /dev/null
+++ b/apps/sim/lib/credential-groups/application/workspace-organization-accounts.ts
@@ -0,0 +1,106 @@
+import { db } from '@sim/db'
+import { mcpServers, member, organization } from '@sim/db/schema'
+import { and, eq, isNull } from 'drizzle-orm'
+import { defineAuthorizedWorkspaceUseCase, defineWorkspaceOperation } from '@/lib/core/application'
+import { resolveCredentialGroupWorkspaceContext } from '@/lib/credential-groups/application/context'
+import {
+ organizationAccountAccessPolicyCodec,
+ organizationAccountPolicyAllowsWorkspace,
+} from '@/lib/credential-groups/application/workspace-access-policy'
+import { loadScopedAccountsCredentialListContext } from '@/lib/credential-groups/credentials'
+import { getManagedMcpConnector } from '@/lib/credential-groups/managed-mcp-connectors'
+import {
+ getCredentialGroupProviderService,
+ isCredentialGroupProvider,
+} from '@/lib/credential-groups/providers'
+import { isScopedCredentialGroupsAvailable } from '@/lib/credential-groups/scoped-availability'
+import { requireResourcePolicy } from '@/lib/resource-policies/repository'
+
+/**
+ * permission-group-exempt: Workspace readers can see sharing status and provider labels; credential use is authorized separately.
+ */
+export const workspaceOrganizationAccountsOperation = defineWorkspaceOperation({
+ id: 'organization_accounts.workspace_status.read',
+ minimumRole: 'read',
+ workspaceApiKey: 'deny',
+ capability: 'none',
+ principalKinds: ['session'],
+})
+
+/** Read-only workspace projection; provider configuration and people remain organization resources. */
+export const getWorkspaceOrganizationAccounts = defineAuthorizedWorkspaceUseCase({
+ operation: workspaceOrganizationAccountsOperation,
+ resolveContext: ({ input }: { input: { workspaceId: string } }) =>
+ resolveCredentialGroupWorkspaceContext(input.workspaceId),
+ authorizationOptions: {},
+ execute: async ({ principal, context }) => {
+ const organizationId = context.workspaceOrganizationId
+ const result = {
+ organizationId,
+ organizationName: null as string | null,
+ available: false,
+ allowed: false,
+ canManage: false,
+ providers: [] as Array<{ id: string; label: string }>,
+ mcpProviders: [] as Array<{ id: string; label: string }>,
+ }
+ if (!organizationId) return result
+ const [org] = await db
+ .select({ name: organization.name })
+ .from(organization)
+ .where(eq(organization.id, organizationId))
+ .limit(1)
+ if (!org) throw new Error('Workspace organization no longer exists')
+ result.organizationName = org.name
+ const [membership] = await db
+ .select({ role: member.role })
+ .from(member)
+ .where(and(eq(member.organizationId, organizationId), eq(member.userId, principal.userId)))
+ .limit(1)
+ result.canManage = membership?.role === 'admin' || membership?.role === 'owner'
+ result.available = await isScopedCredentialGroupsAvailable({
+ kind: 'organization',
+ organizationId,
+ })
+ if (!result.available) return result
+ const group = await loadScopedAccountsCredentialListContext({
+ kind: 'organization',
+ organizationId,
+ })
+ if (!group || group.status !== 'active') return result
+ const policy = await requireResourcePolicy({
+ organizationId,
+ resourceType: 'credential_group',
+ resourceId: group.credentialGroupId,
+ codec: organizationAccountAccessPolicyCodec,
+ })
+ result.allowed = organizationAccountPolicyAllowsWorkspace(policy.document, context.workspaceId)
+ if (!result.allowed) return result
+ result.providers = group.options
+ .filter((option) => option.status === 'active')
+ .map((option) => {
+ if (!isCredentialGroupProvider(option.provider))
+ throw new Error(`Unsupported organization provider: ${option.provider}`)
+ const service = getCredentialGroupProviderService(option.provider)
+ return { id: service.providerId, label: service.name }
+ })
+ const servers = await db
+ .select({ connectorId: mcpServers.managedConnectorId })
+ .from(mcpServers)
+ .where(
+ and(
+ eq(mcpServers.organizationId, organizationId),
+ eq(mcpServers.credentialGroupId, group.credentialGroupId),
+ eq(mcpServers.enabled, true),
+ isNull(mcpServers.deletedAt)
+ )
+ )
+ result.mcpProviders = servers.map((server) => {
+ if (!server.connectorId)
+ throw new Error('Organization MCP provider is missing its connector ID')
+ const connector = getManagedMcpConnector(server.connectorId)
+ return { id: connector.id, label: connector.name }
+ })
+ return result
+ },
+})
diff --git a/apps/sim/lib/credential-groups/availability.test.ts b/apps/sim/lib/credential-groups/availability.test.ts
index 2a51f7c6225..c0edbe98b86 100644
--- a/apps/sim/lib/credential-groups/availability.test.ts
+++ b/apps/sim/lib/credential-groups/availability.test.ts
@@ -22,12 +22,23 @@ describe('resolveCredentialGroupsAvailability', () => {
vi.clearAllMocks()
})
+ it('does not expose organization accounts in a personal workspace even with the global flag enabled', async () => {
+ mockIsFeatureEnabled.mockResolvedValue(true)
+ await expect(
+ resolveCredentialGroupsAvailability({
+ organizationId: null,
+ ownerBilling: { isEnterprise: true },
+ })
+ ).resolves.toEqual({ available: false, reason: 'feature_disabled' })
+ expect(mockIsFeatureEnabled).not.toHaveBeenCalled()
+ })
+
it('attributes a disabled feature flag before considering the plan', async () => {
mockIsFeatureEnabled.mockResolvedValue(false)
await expect(
resolveCredentialGroupsAvailability({
- workspaceId: 'ws-1',
+ organizationId: 'org-1',
ownerBilling: { isEnterprise: false },
})
).resolves.toEqual({
@@ -41,7 +52,7 @@ describe('resolveCredentialGroupsAvailability', () => {
await expect(
resolveCredentialGroupsAvailability({
- workspaceId: 'ws-1',
+ organizationId: 'org-1',
ownerBilling: { isEnterprise: false },
})
).resolves.toEqual({
@@ -50,23 +61,23 @@ describe('resolveCredentialGroupsAvailability', () => {
})
})
- it('evaluates the flag against the workspace id', async () => {
+ it('evaluates the flag against the organization id', async () => {
mockIsFeatureEnabled.mockResolvedValue(true)
await resolveCredentialGroupsAvailability({
- workspaceId: 'ws-1',
+ organizationId: 'org-1',
ownerBilling: { isEnterprise: true },
})
- expect(mockIsFeatureEnabled).toHaveBeenCalledWith('credential-groups', { workspaceId: 'ws-1' })
+ expect(mockIsFeatureEnabled).toHaveBeenCalledWith('credential-groups', { orgId: 'org-1' })
})
- it('allows Enterprise workspaces when the hosted feature is enabled', async () => {
+ it('allows Enterprise organizations when the hosted feature is enabled', async () => {
mockIsFeatureEnabled.mockResolvedValue(true)
await expect(
resolveCredentialGroupsAvailability({
- workspaceId: 'ws-1',
+ organizationId: 'org-1',
ownerBilling: { isEnterprise: true },
})
).resolves.toEqual({
diff --git a/apps/sim/lib/credential-groups/availability.ts b/apps/sim/lib/credential-groups/availability.ts
index 76852800476..2010afe622b 100644
--- a/apps/sim/lib/credential-groups/availability.ts
+++ b/apps/sim/lib/credential-groups/availability.ts
@@ -6,20 +6,22 @@ export type CredentialGroupsAvailability =
| { available: false; reason: 'feature_disabled' | 'enterprise_plan_required' }
/**
- * The workspace the gate is evaluated for. `workspaceId` is required so no call
- * site can silently fall back to the global clause and reveal the feature to a
- * workspace the AppConfig `credential-groups` allowlist does not name.
+ * The canonical organization and its billing entitlement. Personal workspaces
+ * have no organization and cannot enable connected accounts.
*/
export interface CredentialGroupsAvailabilityInput {
- workspaceId: string
+ organizationId: string | null
ownerBilling: { isEnterprise: boolean }
}
export async function resolveCredentialGroupsAvailability({
- workspaceId,
+ organizationId,
ownerBilling,
}: CredentialGroupsAvailabilityInput): Promise {
- if (!(await isFeatureEnabled('credential-groups', { workspaceId }))) {
+ if (
+ !organizationId ||
+ !(await isFeatureEnabled('credential-groups', { orgId: organizationId }))
+ ) {
return { available: false, reason: 'feature_disabled' }
}
if (isHosted && !ownerBilling.isEnterprise) {
@@ -29,8 +31,8 @@ export async function resolveCredentialGroupsAvailability({
}
/**
- * Credential Groups are gated per workspace (globally or by the AppConfig
- * `workspaceIds` allowlist) and restricted to Enterprise workspaces on Sim Cloud.
+ * Credential Groups use organization rollout targeting and require an active
+ * Enterprise entitlement on Sim Cloud. Workspace flag targeting is not consulted.
*/
export async function isCredentialGroupsAvailable(
input: CredentialGroupsAvailabilityInput
diff --git a/apps/sim/lib/credential-groups/credentials.ts b/apps/sim/lib/credential-groups/credentials.ts
index 3a386692052..ec0fe7889af 100644
--- a/apps/sim/lib/credential-groups/credentials.ts
+++ b/apps/sim/lib/credential-groups/credentials.ts
@@ -5,11 +5,9 @@ import {
credential,
credentialGroup,
credentialGroupEnrollment,
- foldedEmail,
- member,
user,
} from '@sim/db/schema'
-import { and, asc, eq, gt, inArray, isNull, or, type SQL, sql } from 'drizzle-orm'
+import { and, asc, eq, gt, inArray, isNotNull, isNull, or, type SQL, sql } from 'drizzle-orm'
import { type ResourceScope, resourceScopeFromOwner } from '@/lib/core/resource-scope'
import { resourceScopeCondition } from '@/lib/core/resource-scope.server'
import {
@@ -132,7 +130,7 @@ export async function loadCredentialGroupEnrollmentAccess(
email: credentialGroupEnrollment.email,
})
.from(credentialGroupEnrollment)
- .innerJoin(user, eq(foldedEmail(user.email), credentialGroupEnrollment.email))
+ .innerJoin(user, eq(user.id, credentialGroupEnrollment.userId))
.where(
and(
eq(user.id, userId),
@@ -220,6 +218,7 @@ export async function loadManagedCredentialGroupBinding(
.select({
credentialId: credential.id,
createdBy: credential.createdBy,
+ enrollmentUserId: credentialGroupEnrollment.userId,
workspaceId: credential.workspaceId,
organizationId: credential.organizationId,
providerId: credential.providerId,
@@ -257,13 +256,9 @@ export async function loadManagedCredentialGroupBinding(
.limit(1)
if (!row) return null
if (row.organizationId) {
- if (!row.createdBy) return null
- const [membership] = await db
- .select({ id: member.id })
- .from(member)
- .where(and(eq(member.organizationId, row.organizationId), eq(member.userId, row.createdBy)))
- .limit(1)
- if (!membership) return null
+ if (!row.enrollmentUserId || row.enrollmentUserId !== row.createdBy) {
+ throw new Error('Organization credential is not bound to its enrolled user')
+ }
}
if (!row.providerId) throw new Error(`Managed credential ${row.credentialId} has no provider ID`)
if (!row.credentialGroupOptionId) {
@@ -415,6 +410,12 @@ export async function listCredentialGroupCredentialReferences({
eq(credential.type, 'managed_oauth'),
eq(credential.managedOauthStatus, 'active'),
eq(credentialGroupEnrollment.credentialGroupId, credentialGroupId),
+ organizationId
+ ? and(
+ isNotNull(credentialGroupEnrollment.userId),
+ eq(credential.createdBy, credentialGroupEnrollment.userId)
+ )
+ : undefined,
email ? eq(credentialGroupEnrollment.email, email) : undefined,
inArray(credential.credentialGroupOptionId, credentialGroupOptionIds),
credentialProviderIds?.length
diff --git a/apps/sim/lib/credential-groups/enrollments.test.ts b/apps/sim/lib/credential-groups/enrollments.test.ts
index 1c583e48525..e0380fadb13 100644
--- a/apps/sim/lib/credential-groups/enrollments.test.ts
+++ b/apps/sim/lib/credential-groups/enrollments.test.ts
@@ -40,6 +40,7 @@ vi.mock('@/lib/credential-groups/provider-registry', () => ({
import { getOrganizationSubscriptionUsable } from '@/lib/billing/core/subscription'
import { isFeatureEnabled } from '@/lib/core/config/feature-flags'
import {
+ bindCredentialGroupEnrollmentUser,
completeCredentialGroupEnrollment,
createCredentialGroupInvitationLink,
createCredentialGroupSelfEnrollmentLink,
@@ -610,6 +611,7 @@ describe('enrollment context for session-authorized or consumed-attempt OAuth',
const row = {
enrollment: {
...ENROLLMENT,
+ userId: 'person-1',
invitationTokenHash: 'rotated-hash',
invitationExpiresAt: new Date(0),
},
@@ -712,7 +714,7 @@ describe('enrollment context for session-authorized or consumed-attempt OAuth',
})
})
-describe('organization enrollment membership boundary', () => {
+describe('organization enrollment bound identity', () => {
const identity = {
organizationId: 'organization-1',
credentialGroupId: 'group-1',
@@ -721,7 +723,7 @@ describe('organization enrollment membership boundary', () => {
invitationTokenHash: ENROLLMENT.invitationTokenHash,
}
const row = {
- enrollment: ENROLLMENT,
+ enrollment: { ...ENROLLMENT, userId: 'person-1' },
groupId: 'group-1',
groupName: 'Accounts',
groupStatus: 'active',
@@ -748,21 +750,23 @@ describe('organization enrollment membership boundary', () => {
getAuthorizedCredentialGroupOAuthContext(identity, 'gmail-option')
).resolves.toMatchObject({
organizationId: 'organization-1',
- credentialOwnerId: 'member-1',
+ credentialOwnerId: 'person-1',
workspaceName: 'Acme',
})
})
- it('denies a valid invitation after the person leaves the organization', async () => {
+ it('retains the bound identity when the contributor is not an organization member', async () => {
queueTableRows(schemaMock.member, [])
await expect(
getAuthorizedCredentialGroupOAuthContext(identity, 'gmail-option')
- ).resolves.toBeNull()
+ ).resolves.toMatchObject({ credentialOwnerId: 'person-1' })
+ expect(dbChainMockFns.from).not.toHaveBeenCalledWith(schemaMock.member)
})
- it('denies ambiguous verified identities', async () => {
+ it('uses the bound user instead of looking up matching member emails', async () => {
queueTableRows(schemaMock.member, [{ userId: 'member-1' }, { userId: 'member-2' }])
await expect(
getAuthorizedCredentialGroupOAuthContext(identity, 'gmail-option')
- ).resolves.toBeNull()
+ ).resolves.toMatchObject({ credentialOwnerId: 'person-1' })
+ expect(dbChainMockFns.from).not.toHaveBeenCalledWith(schemaMock.member)
})
it('conceals an enrollment from a different asserted organization', async () => {
queueTableRows(schemaMock.member, [{ userId: 'member-1' }])
@@ -774,3 +778,67 @@ describe('organization enrollment membership boundary', () => {
).resolves.toBeNull()
})
})
+
+describe('verified immutable enrollment identity binding', () => {
+ const identity = {
+ workspaceId: 'workspace-1',
+ credentialGroupId: 'group-1',
+ enrollmentId: ENROLLMENT.id,
+ email: ENROLLMENT.email,
+ invitationTokenHash: ENROLLMENT.invitationTokenHash,
+ }
+ beforeEach(() => {
+ vi.clearAllMocks()
+ resetDbChainMock()
+ })
+ it('binds an invitation once to its verified recipient', async () => {
+ queueTableRows(schemaMock.credentialGroupEnrollment, [
+ { enrollment: { ...ENROLLMENT, userId: null }, email: ENROLLMENT.email, verified: true },
+ ])
+ await bindCredentialGroupEnrollmentUser(identity, 'recipient')
+ expect(dbChainMockFns.set).toHaveBeenCalledWith(
+ expect.objectContaining({ userId: 'recipient' })
+ )
+ expect(isNull).toHaveBeenCalledWith(schemaMock.credentialGroupEnrollment.userId)
+ expect(eq).toHaveBeenCalledWith(
+ schemaMock.credentialGroupEnrollment.invitationTokenHash,
+ identity.invitationTokenHash
+ )
+ })
+ it.each([
+ { email: 'someone-else@example.com', verified: true },
+ { email: ENROLLMENT.email, verified: false },
+ ])('rejects a different or unverified recipient', async ({ email, verified }) => {
+ queueTableRows(schemaMock.credentialGroupEnrollment, [
+ { enrollment: { ...ENROLLMENT, userId: null }, email, verified },
+ ])
+ await expect(bindCredentialGroupEnrollmentUser(identity, 'recipient')).rejects.toThrow(
+ 'verified email'
+ )
+ expect(dbChainMockFns.update).not.toHaveBeenCalled()
+ })
+ it('keeps the same bound user after an email change', async () => {
+ queueTableRows(schemaMock.credentialGroupEnrollment, [
+ {
+ enrollment: { ...ENROLLMENT, userId: 'recipient' },
+ email: 'new@example.com',
+ verified: true,
+ },
+ ])
+ await expect(bindCredentialGroupEnrollmentUser(identity, 'recipient')).resolves.toBeUndefined()
+ expect(dbChainMockFns.update).not.toHaveBeenCalled()
+ })
+ it('never reassigns a bound enrollment to a different user with the old email', async () => {
+ queueTableRows(schemaMock.credentialGroupEnrollment, [
+ {
+ enrollment: { ...ENROLLMENT, userId: 'recipient' },
+ email: ENROLLMENT.email,
+ verified: true,
+ },
+ ])
+ await expect(bindCredentialGroupEnrollmentUser(identity, 'new-user')).rejects.toThrow(
+ 'verified email'
+ )
+ expect(dbChainMockFns.update).not.toHaveBeenCalled()
+ })
+})
diff --git a/apps/sim/lib/credential-groups/enrollments.ts b/apps/sim/lib/credential-groups/enrollments.ts
index 3223ada9b96..fe6a93eefdf 100644
--- a/apps/sim/lib/credential-groups/enrollments.ts
+++ b/apps/sim/lib/credential-groups/enrollments.ts
@@ -5,7 +5,6 @@ import {
credentialGroup,
credentialGroupEnrollment,
mcpServers,
- member,
organization,
user,
workspace,
@@ -27,6 +26,7 @@ import { resourceScopeCondition } from '@/lib/core/resource-scope.server'
import { getBaseUrl } from '@/lib/core/utils/urls'
import type { ManagedMcpConnectorId } from '@/lib/credential-groups/managed-mcp-connectors'
import { getManagedMcpConnector } from '@/lib/credential-groups/managed-mcp-connectors'
+import { requireOrganizationAccountsSetup } from '@/lib/credential-groups/organization-setup'
import { getCredentialGroupProviderAdapter } from '@/lib/credential-groups/provider-registry'
import type { CredentialGroupProvider } from '@/lib/credential-groups/providers'
import {
@@ -135,19 +135,25 @@ export interface CredentialGroupOAuthContext {
}
export interface CredentialGroupMcpOAuthContext {
+ credentialGroupName: string
+ userId: string
enrollmentId: string
credentialGroupId: string
- workspaceId: string
+ workspaceId?: string
+ organizationId?: string
email: string
enrollmentStatus: EnrollmentRow['status']
server: {
+ connectorId: ManagedMcpConnectorId
id: string
name: string
url: string
+ oauthConfigVersion: number
}
}
export interface PublicCredentialGroupEnrollmentIdentity {
+ userId?: string
enrollmentId: string
credentialGroupId: string
workspaceId?: string
@@ -277,26 +283,9 @@ async function loadLiveEnrollmentRow(scope: SQL | undefined) {
const ownerScope = resourceScopeFromOwner(row)
if (!(await isScopedCredentialGroupsAvailable(ownerScope))) return null
- let credentialOwnerId = row.workspaceOwnerId
- if (ownerScope.kind === 'organization') {
- const memberships = await db
- .select({ userId: user.id })
- .from(member)
- .innerJoin(user, eq(user.id, member.userId))
- .where(
- and(
- eq(member.organizationId, ownerScope.organizationId),
- eq(user.emailVerified, true),
- sql`lower(btrim(${user.email})) = ${row.enrollment.email}`
- )
- )
- .limit(2)
- if (memberships.length !== 1) return null
- credentialOwnerId = memberships[0].userId
- }
return {
...row,
- credentialOwnerId,
+ credentialOwnerId: row.enrollment.userId,
workspaceName: row.organizationName ?? row.workspaceName ?? '',
}
}
@@ -324,6 +313,7 @@ function identityForPublicEnrollmentRow(
...resourceScopeFields(resourceScopeFromOwner(row)),
email: row.enrollment.email,
invitationTokenHash: row.enrollment.invitationTokenHash,
+ ...(row.enrollment.userId ? { userId: row.enrollment.userId } : {}),
}
}
@@ -345,13 +335,82 @@ async function resolveAuthorizedPublicEnrollmentRow(
!row ||
row.groupId !== identity.credentialGroupId ||
!sameResourceScope(resourceScopeFromOwner(row), resourceScopeFromOwner(identity)) ||
- row.enrollment.email !== identity.email
+ row.enrollment.email !== identity.email ||
+ (identity.userId !== undefined && row.enrollment.userId !== identity.userId)
) {
return null
}
return row
}
+/** Binds invitation authority to a verified signed-in user once, under the enrollment lifecycle lock. */
+export async function bindCredentialGroupEnrollmentUser(
+ identity: PublicCredentialGroupEnrollmentIdentity,
+ userId: string
+): Promise {
+ if (!userId.trim())
+ throw new CredentialGroupEnrollmentError('Sign in to connect your accounts', 400)
+ if (identity.organizationId)
+ await requireOrganizationAccountsSetup(identity.organizationId, identity.credentialGroupId)
+ await db.transaction(async (tx) => {
+ await lockCredentialGroupEnrollmentLifecycle(tx, identity.enrollmentId)
+ const [row] = await tx
+ .select({
+ enrollment: credentialGroupEnrollment,
+ email: user.email,
+ verified: user.emailVerified,
+ })
+ .from(credentialGroupEnrollment)
+ .innerJoin(
+ credentialGroup,
+ eq(credentialGroup.id, credentialGroupEnrollment.credentialGroupId)
+ )
+ .innerJoin(user, eq(user.id, userId))
+ .where(
+ and(
+ eq(credentialGroupEnrollment.id, identity.enrollmentId),
+ eq(credentialGroupEnrollment.credentialGroupId, identity.credentialGroupId),
+ eq(credentialGroupEnrollment.invitationTokenHash, identity.invitationTokenHash),
+ eq(credentialGroupEnrollment.email, identity.email),
+ resourceScopeCondition(credentialGroup, resourceScopeFromOwner(identity)),
+ eq(credentialGroup.status, 'active')
+ )
+ )
+ .limit(1)
+ .for('update', { of: credentialGroupEnrollment })
+ if (
+ !row ||
+ row.enrollment.invitationExpiresAt.getTime() <= Date.now() ||
+ row.enrollment.revokedAt ||
+ ['revoked', 'delivery_failed'].includes(row.enrollment.status)
+ ) {
+ throw new CredentialGroupEnrollmentError('Invitation is invalid or expired', 404)
+ }
+ if (
+ !row.verified ||
+ (row.enrollment.userId
+ ? row.enrollment.userId !== userId
+ : normalizeEmail(row.email) !== identity.email)
+ ) {
+ throw new CredentialGroupEnrollmentError(
+ 'Sign in with the verified email address this invitation was sent to',
+ 400
+ )
+ }
+ if (!row.enrollment.userId) {
+ await tx
+ .update(credentialGroupEnrollment)
+ .set({ userId, updatedAt: new Date() })
+ .where(
+ and(
+ eq(credentialGroupEnrollment.id, identity.enrollmentId),
+ isNull(credentialGroupEnrollment.userId)
+ )
+ )
+ }
+ })
+}
+
function toCredentialGroupEnrollment(row: EnrollmentRow): CredentialGroupEnrollmentRecord {
return {
id: row.id,
@@ -417,7 +476,7 @@ async function getInvitationContext(
.from(mcpServers)
.where(
and(
- scope.kind === 'workspace' ? eq(mcpServers.workspaceId, scope.workspaceId) : sql`false`,
+ resourceScopeCondition(mcpServers, scope),
eq(mcpServers.credentialGroupId, groupId),
eq(mcpServers.authType, 'oauth'),
eq(mcpServers.enabled, true),
@@ -636,7 +695,7 @@ export async function listCredentialGroupEnrollments(
.from(mcpServers)
.where(
and(
- scope.kind === 'workspace' ? eq(mcpServers.workspaceId, scope.workspaceId) : sql`false`,
+ resourceScopeCondition(mcpServers, scope),
eq(mcpServers.credentialGroupId, groupId),
eq(mcpServers.authType, 'oauth'),
eq(mcpServers.enabled, true),
@@ -973,6 +1032,57 @@ export async function deleteCredentialGroupEnrollment(
})
}
+/** Revocation preserves the bound owner and prevents pending callbacks from restoring grants. */
+export async function revokeCredentialGroupEnrollment(
+ scope: ResourceScope,
+ groupId: string,
+ enrollmentId: string
+) {
+ return db.transaction(async (tx) => {
+ await lockCredentialGroupEnrollmentLifecycle(tx, enrollmentId)
+ const [row] = await tx
+ .select({ enrollment: credentialGroupEnrollment })
+ .from(credentialGroupEnrollment)
+ .innerJoin(
+ credentialGroup,
+ eq(credentialGroup.id, credentialGroupEnrollment.credentialGroupId)
+ )
+ .where(
+ and(
+ eq(credentialGroupEnrollment.id, enrollmentId),
+ eq(credentialGroup.id, groupId),
+ resourceScopeCondition(credentialGroup, scope)
+ )
+ )
+ .limit(1)
+ .for('update', { of: credentialGroupEnrollment })
+ if (!row) throw new CredentialGroupEnrollmentError('Enrollment not found', 404)
+ const now = new Date()
+ const credentials = await tx
+ .update(credential)
+ .set({ managedOauthStatus: 'revoked', revokedAt: now, updatedAt: now })
+ .where(
+ and(
+ eq(credential.credentialGroupEnrollmentId, enrollmentId),
+ resourceScopeCondition(credential, scope)
+ )
+ )
+ .returning({ id: credential.id, type: credential.type })
+ const [updated] = await tx
+ .update(credentialGroupEnrollment)
+ .set({ status: 'revoked', revokedAt: now, updatedAt: now })
+ .where(eq(credentialGroupEnrollment.id, enrollmentId))
+ .returning()
+ if (!updated) throw new Error('Enrollment revocation returned no row')
+ return {
+ credentialGroupEnrollment: toCredentialGroupEnrollment(updated),
+ retiredMcpConnectionIds: credentials
+ .filter((row) => row.type === 'managed_mcp')
+ .map((row) => row.id),
+ }
+ })
+}
+
export async function getPublicCredentialGroupEnrollment(
token: string
): Promise {
@@ -1025,7 +1135,7 @@ async function buildPublicCredentialGroupEnrollment(
.from(mcpServers)
.where(
and(
- row.workspaceId ? eq(mcpServers.workspaceId, row.workspaceId) : sql`false`,
+ resourceScopeCondition(mcpServers, resourceScopeFromOwner(row)),
eq(mcpServers.credentialGroupId, row.groupId),
eq(mcpServers.authType, 'oauth'),
eq(mcpServers.enabled, true),
@@ -1248,7 +1358,7 @@ export async function getAuthorizedCredentialGroupOAuthContext(
function loadEnrollmentRowForIdentity(
identity: Pick<
PublicCredentialGroupEnrollmentIdentity,
- 'workspaceId' | 'organizationId' | 'credentialGroupId' | 'enrollmentId' | 'email'
+ 'workspaceId' | 'organizationId' | 'credentialGroupId' | 'enrollmentId' | 'email' | 'userId'
>
) {
return loadLiveEnrollmentRow(
@@ -1256,7 +1366,8 @@ function loadEnrollmentRowForIdentity(
eq(credentialGroupEnrollment.id, identity.enrollmentId),
eq(credentialGroupEnrollment.email, identity.email),
eq(credentialGroup.id, identity.credentialGroupId),
- resourceScopeCondition(credentialGroup, resourceScopeFromOwner(identity))
+ resourceScopeCondition(credentialGroup, resourceScopeFromOwner(identity)),
+ identity.userId ? eq(credentialGroupEnrollment.userId, identity.userId) : undefined
)
)
}
@@ -1265,7 +1376,7 @@ function loadEnrollmentRowForIdentity(
export async function getCredentialGroupOAuthContextForEnrollment(
identity: Pick<
PublicCredentialGroupEnrollmentIdentity,
- 'workspaceId' | 'organizationId' | 'credentialGroupId' | 'enrollmentId' | 'email'
+ 'workspaceId' | 'organizationId' | 'credentialGroupId' | 'enrollmentId' | 'email' | 'userId'
>,
optionId: string
): Promise {
@@ -1288,7 +1399,7 @@ export async function getAuthorizedCredentialGroupMcpOAuthContext(
export async function getCredentialGroupMcpOAuthContextForEnrollment(
identity: Pick<
PublicCredentialGroupEnrollmentIdentity,
- 'workspaceId' | 'organizationId' | 'credentialGroupId' | 'enrollmentId' | 'email'
+ 'workspaceId' | 'organizationId' | 'credentialGroupId' | 'enrollmentId' | 'email' | 'userId'
>,
mcpServerId: string
): Promise {
@@ -1300,19 +1411,20 @@ async function credentialGroupMcpOAuthContextFromRow(
row: NonNullable>>,
mcpServerId: string
): Promise {
- if (!row.workspaceId) return null
+ if (!row.enrollment.userId) return null
const [server] = await db
.select({
id: mcpServers.id,
name: mcpServers.name,
url: mcpServers.url,
managedConnectorId: mcpServers.managedConnectorId,
+ oauthConfigVersion: mcpServers.oauthConfigVersion,
})
.from(mcpServers)
.where(
and(
eq(mcpServers.id, mcpServerId),
- row.workspaceId ? eq(mcpServers.workspaceId, row.workspaceId) : sql`false`,
+ resourceScopeCondition(mcpServers, resourceScopeFromOwner(row)),
eq(mcpServers.credentialGroupId, row.groupId),
eq(mcpServers.authType, 'oauth'),
eq(mcpServers.enabled, true),
@@ -1327,11 +1439,19 @@ async function credentialGroupMcpOAuthContextFromRow(
getManagedMcpConnector(server.managedConnectorId)
return {
enrollmentId: row.enrollment.id,
+ userId: row.enrollment.userId,
credentialGroupId: row.groupId,
- workspaceId: row.workspaceId,
+ credentialGroupName: row.groupName,
+ ...resourceScopeFields(resourceScopeFromOwner(row)),
email: row.enrollment.email,
enrollmentStatus: row.enrollment.status,
- server: { id: server.id, name: server.name, url: server.url },
+ server: {
+ connectorId: getManagedMcpConnector(server.managedConnectorId).id,
+ id: server.id,
+ name: server.name,
+ url: server.url,
+ oauthConfigVersion: server.oauthConfigVersion,
+ },
}
}
diff --git a/apps/sim/lib/credential-groups/indexing.test.ts b/apps/sim/lib/credential-groups/indexing.test.ts
new file mode 100644
index 00000000000..376192e7cec
--- /dev/null
+++ b/apps/sim/lib/credential-groups/indexing.test.ts
@@ -0,0 +1,23 @@
+/** @vitest-environment node */
+import { describe, expect, it } from 'vitest'
+import { getCredentialGroupIndexingConnector } from '@/lib/credential-groups/indexing'
+
+describe('connected account indexing capabilities', () => {
+ it.each([
+ ['gmail', 'gmail'],
+ ['google-drive', 'google_drive'],
+ ['google-calendar', 'google_calendar'],
+ ['github-repositories', 'github'],
+ ['confluence', 'confluence'],
+ ['jira', 'jira'],
+ ['slack', 'slack'],
+ ] as const)('uses the permission-aware connector registry for %s', (provider, type) => {
+ expect(getCredentialGroupIndexingConnector(provider)?.type).toBe(type)
+ })
+ it.each(['notion', 'outlook', 'hubspot'] as const)(
+ 'does not advertise generic KB ingestion as per-person indexing for %s',
+ (provider) => {
+ expect(getCredentialGroupIndexingConnector(provider)).toBeUndefined()
+ }
+ )
+})
diff --git a/apps/sim/lib/credential-groups/indexing.ts b/apps/sim/lib/credential-groups/indexing.ts
new file mode 100644
index 00000000000..0958c7d8564
--- /dev/null
+++ b/apps/sim/lib/credential-groups/indexing.ts
@@ -0,0 +1,12 @@
+import {
+ type CredentialGroupProvider,
+ findCredentialGroupProviderFromProviderId,
+} from '@/lib/credential-groups/providers'
+import { SEARCH_CONNECTORS } from '@/lib/sim-search/connectors'
+
+/** Only connectors with permission-scoped OAuth ingestion can index connected accounts. */
+export function getCredentialGroupIndexingConnector(provider: CredentialGroupProvider) {
+ return SEARCH_CONNECTORS.find(
+ (connector) => findCredentialGroupProviderFromProviderId(connector.providerId) === provider
+ )
+}
diff --git a/apps/sim/lib/credential-groups/limits.ts b/apps/sim/lib/credential-groups/limits.ts
index f7df344e79c..cad8c92b28b 100644
--- a/apps/sim/lib/credential-groups/limits.ts
+++ b/apps/sim/lib/credential-groups/limits.ts
@@ -1,6 +1,8 @@
export const CREDENTIAL_GROUP_MCP_SERVER_LIMIT = 50
+export const ORGANIZATION_ACCOUNT_WORKSPACE_LIMIT = 1000
export const CREDENTIAL_GROUP_WORKFLOW_ACCESS_LIMIT = 50
export const CREDENTIAL_GROUP_WORKFLOW_CATALOG_LIMIT = 500
export const CREDENTIAL_GROUP_WORKFLOW_NAME_MAX_LENGTH = 255
/** Knowledge connectors one credential option may back at once. */
export const CREDENTIAL_GROUP_KNOWLEDGE_CONNECTOR_ACCESS_LIMIT = 32
+export const ORGANIZATION_ACCOUNT_INDEXING_SOURCE_LIMIT = 100
diff --git a/apps/sim/lib/credential-groups/managed-mcp-service.ts b/apps/sim/lib/credential-groups/managed-mcp-service.ts
index 27982aeb8ad..76590487f1b 100644
--- a/apps/sim/lib/credential-groups/managed-mcp-service.ts
+++ b/apps/sim/lib/credential-groups/managed-mcp-service.ts
@@ -7,7 +7,13 @@ import {
mcpServers,
} from '@sim/db/schema'
import { getPostgresErrorCode } from '@sim/utils/errors'
-import { and, eq, inArray, isNull, ne } from 'drizzle-orm'
+import { and, eq, inArray, isNull, ne, sql } from 'drizzle-orm'
+import {
+ resourceScopeColumns,
+ resourceScopeFromOwner,
+ resourceScopeKey,
+} from '@/lib/core/resource-scope'
+import { resourceScopeCondition } from '@/lib/core/resource-scope.server'
import { encryptSecret } from '@/lib/core/security/encryption'
import {
getManagedMcpConnector,
@@ -81,6 +87,34 @@ function toSummary(row: typeof mcpServers.$inferSelect): ManagedMcpConnectorSumm
}
}
+/** Loads setup fields without reading the stored OAuth client secret. */
+export async function loadOrganizationDatabricksSetup(
+ organizationId: string,
+ credentialGroupId: string
+) {
+ const [server] = await db
+ .select({
+ id: mcpServers.id,
+ name: mcpServers.name,
+ url: mcpServers.url,
+ oauthClientId: mcpServers.oauthClientId,
+ hasOauthClientSecret: sql`${mcpServers.oauthClientSecret} IS NOT NULL`,
+ enabled: mcpServers.enabled,
+ })
+ .from(mcpServers)
+ .where(
+ and(
+ eq(mcpServers.organizationId, organizationId),
+ eq(mcpServers.credentialGroupId, credentialGroupId),
+ eq(mcpServers.managedConnectorId, 'databricks'),
+ isNull(mcpServers.deletedAt)
+ )
+ )
+ .limit(1)
+ if (!server) throw new ManagedMcpConnectorError('Databricks has not been added', 'not_found')
+ return server
+}
+
async function validateServerUrl(url: string): Promise {
try {
validateMcpDomain(url)
@@ -143,18 +177,23 @@ async function retireManagedMcpCredentials(
}
export async function createManagedMcpConnector(params: {
- workspaceId: string
+ workspaceId?: string
+ organizationId?: string
credentialGroupId: string
userId: string
input: CreateManagedMcpConnectorInput
}): Promise {
+ const scope = resourceScopeFromOwner(params)
const connector = getManagedMcpConnector(params.input.connectorId)
const url = resolveManagedMcpConnectorUrl(
connector.id,
params.input.connectorId === 'databricks' ? params.input.url : undefined
)
await validateServerUrl(url)
- const serverId = generateMcpServerId(params.workspaceId, url)
+ const serverId = generateMcpServerId(
+ scope.kind === 'workspace' ? scope.workspaceId : resourceScopeKey(scope),
+ url
+ )
const oauthClientId =
params.input.connectorId === 'databricks' ? params.input.oauthClientId.trim() : null
const oauthClientSecret =
@@ -176,7 +215,7 @@ export async function createManagedMcpConnector(params: {
.where(
and(
eq(credentialGroup.id, params.credentialGroupId),
- eq(credentialGroup.workspaceId, params.workspaceId)
+ resourceScopeCondition(credentialGroup, scope)
)
)
.limit(1)
@@ -188,7 +227,7 @@ export async function createManagedMcpConnector(params: {
.from(mcpServers)
.where(
and(
- eq(mcpServers.workspaceId, params.workspaceId),
+ resourceScopeCondition(mcpServers, scope),
eq(mcpServers.credentialGroupId, params.credentialGroupId),
eq(mcpServers.managedConnectorId, connector.id),
isNull(mcpServers.deletedAt)
@@ -207,7 +246,7 @@ export async function createManagedMcpConnector(params: {
.from(mcpServers)
.where(
and(
- eq(mcpServers.workspaceId, params.workspaceId),
+ resourceScopeCondition(mcpServers, scope),
eq(mcpServers.url, url),
isNull(mcpServers.deletedAt)
)
@@ -224,7 +263,7 @@ export async function createManagedMcpConnector(params: {
const [existingUrl] = await tx
.select()
.from(mcpServers)
- .where(and(eq(mcpServers.id, serverId), eq(mcpServers.workspaceId, params.workspaceId)))
+ .where(and(eq(mcpServers.id, serverId), resourceScopeCondition(mcpServers, scope)))
.limit(1)
.for('update')
const now = new Date()
@@ -242,6 +281,7 @@ export async function createManagedMcpConnector(params: {
authType: 'oauth',
oauthClientId,
oauthClientSecret,
+ oauthConfigVersion: existingUrl.oauthConfigVersion + 1,
headers: {},
enabled: true,
connectionStatus: 'disconnected',
@@ -260,7 +300,7 @@ export async function createManagedMcpConnector(params: {
.insert(mcpServers)
.values({
id: serverId,
- workspaceId: params.workspaceId,
+ ...resourceScopeColumns(scope),
credentialGroupId: params.credentialGroupId,
managedConnectorId: connector.id,
createdBy: params.userId,
@@ -299,11 +339,13 @@ export async function createManagedMcpConnector(params: {
}
export async function updateManagedMcpConnector(params: {
- workspaceId: string
+ workspaceId?: string
+ organizationId?: string
credentialGroupId: string
connectorId: ManagedMcpConnectorId
input: UpdateManagedMcpConnectorInput
}): Promise {
+ const scope = resourceScopeFromOwner(params)
if (params.connectorId !== 'databricks') {
throw new ManagedMcpConnectorError(
'Only Databricks connector settings can be changed',
@@ -315,7 +357,7 @@ export async function updateManagedMcpConnector(params: {
.from(mcpServers)
.where(
and(
- eq(mcpServers.workspaceId, params.workspaceId),
+ resourceScopeCondition(mcpServers, scope),
eq(mcpServers.credentialGroupId, params.credentialGroupId),
eq(mcpServers.managedConnectorId, params.connectorId),
isNull(mcpServers.deletedAt)
@@ -343,7 +385,7 @@ export async function updateManagedMcpConnector(params: {
.where(
and(
eq(credentialGroup.id, params.credentialGroupId),
- eq(credentialGroup.workspaceId, params.workspaceId)
+ resourceScopeCondition(credentialGroup, scope)
)
)
.limit(1)
@@ -356,7 +398,7 @@ export async function updateManagedMcpConnector(params: {
.where(
and(
eq(mcpServers.id, current.id),
- eq(mcpServers.workspaceId, params.workspaceId),
+ resourceScopeCondition(mcpServers, scope),
eq(mcpServers.credentialGroupId, params.credentialGroupId),
eq(mcpServers.managedConnectorId, 'databricks'),
isNull(mcpServers.deletedAt)
@@ -366,7 +408,10 @@ export async function updateManagedMcpConnector(params: {
.for('update')
if (!locked) throw new ManagedMcpConnectorError('Managed MCP connector not found', 'not_found')
const urlChanged = url !== locked.url
- const targetServerId = generateMcpServerId(params.workspaceId, url)
+ const targetServerId = generateMcpServerId(
+ scope.kind === 'workspace' ? scope.workspaceId : resourceScopeKey(scope),
+ url
+ )
if (urlChanged && targetServerId === locked.id) {
throw new Error(`MCP server ID collision for ${locked.id}`)
}
@@ -376,7 +421,7 @@ export async function updateManagedMcpConnector(params: {
.from(mcpServers)
.where(
and(
- eq(mcpServers.workspaceId, params.workspaceId),
+ resourceScopeCondition(mcpServers, scope),
eq(mcpServers.url, url),
ne(mcpServers.id, locked.id),
isNull(mcpServers.deletedAt)
@@ -416,6 +461,9 @@ export async function updateManagedMcpConnector(params: {
.set({
name: nextName,
oauthClientId: nextOauthClientId,
+ oauthConfigVersion: changedCredentials
+ ? locked.oauthConfigVersion + 1
+ : locked.oauthConfigVersion,
...(encryptedSecret !== undefined ? { oauthClientSecret: encryptedSecret } : {}),
...(changedCredentials
? { connectionStatus: 'disconnected', lastConnected: null, lastError: null }
@@ -435,7 +483,7 @@ export async function updateManagedMcpConnector(params: {
const [target] = await tx
.select()
.from(mcpServers)
- .where(and(eq(mcpServers.id, targetServerId), eq(mcpServers.workspaceId, params.workspaceId)))
+ .where(and(eq(mcpServers.id, targetServerId), resourceScopeCondition(mcpServers, scope)))
.limit(1)
.for('update')
if (target?.deletedAt === null) {
@@ -463,6 +511,7 @@ export async function updateManagedMcpConnector(params: {
authType: 'oauth',
oauthClientId: nextOauthClientId,
oauthClientSecret: nextOauthClientSecret,
+ oauthConfigVersion: locked.oauthConfigVersion + 1,
headers: {},
enabled: true,
connectionStatus: 'disconnected',
@@ -477,7 +526,7 @@ export async function updateManagedMcpConnector(params: {
.insert(mcpServers)
.values({
id: targetServerId,
- workspaceId: params.workspaceId,
+ ...resourceScopeColumns(scope),
...rowValues,
createdAt: now,
})
@@ -493,7 +542,8 @@ export async function updateManagedMcpConnector(params: {
}
export async function deleteManagedMcpConnector(params: {
- workspaceId: string
+ workspaceId?: string
+ organizationId?: string
credentialGroupId: string
connectorId: ManagedMcpConnectorId
}): Promise<{
@@ -501,6 +551,7 @@ export async function deleteManagedMcpConnector(params: {
serverIds: string[]
retiredMcpConnectionIds: string[]
}> {
+ const scope = resourceScopeFromOwner(params)
return db.transaction(async (tx) => {
const [group] = await tx
.select({ id: credentialGroup.id })
@@ -508,7 +559,7 @@ export async function deleteManagedMcpConnector(params: {
.where(
and(
eq(credentialGroup.id, params.credentialGroupId),
- eq(credentialGroup.workspaceId, params.workspaceId)
+ resourceScopeCondition(credentialGroup, scope)
)
)
.limit(1)
@@ -520,7 +571,7 @@ export async function deleteManagedMcpConnector(params: {
.from(mcpServers)
.where(
and(
- eq(mcpServers.workspaceId, params.workspaceId),
+ resourceScopeCondition(mcpServers, scope),
eq(mcpServers.credentialGroupId, params.credentialGroupId),
eq(mcpServers.managedConnectorId, params.connectorId),
isNull(mcpServers.deletedAt)
diff --git a/apps/sim/lib/credential-groups/mcp-connections.ts b/apps/sim/lib/credential-groups/mcp-connections.ts
index b3aeb1c3044..12a891a9427 100644
--- a/apps/sim/lib/credential-groups/mcp-connections.ts
+++ b/apps/sim/lib/credential-groups/mcp-connections.ts
@@ -1,6 +1,8 @@
import { db } from '@sim/db'
import { credential, credentialGroup, credentialGroupEnrollment, mcpServers } from '@sim/db/schema'
-import { and, asc, eq, gt, inArray, isNull, or, sql } from 'drizzle-orm'
+import { and, asc, eq, gt, inArray, isNotNull, isNull, or, sql } from 'drizzle-orm'
+import { resourceScopeFromOwner } from '@/lib/core/resource-scope'
+import { resourceScopeCondition } from '@/lib/core/resource-scope.server'
import { getManagedMcpConnector } from '@/lib/credential-groups/managed-mcp-connectors'
export const MAX_CREDENTIAL_GROUP_MCP_CONNECTION_PAGE_SIZE = 100
@@ -22,12 +24,14 @@ export class CredentialGroupMcpConnectionCursorNotFoundError extends Error {
}
interface ListCredentialGroupMcpConnectionReferencesInput {
- workspaceId: string
+ workspaceId?: string
+ organizationId?: string
credentialGroupId: string
limit: number
cursor?: string
email?: string
mcpServerId?: string
+ connectorId?: string
}
function decodeToolNames(value: unknown): string[] {
@@ -41,31 +45,37 @@ function decodeToolNames(value: unknown): string[] {
/** Lists one bounded page of active managed MCP connections without selecting token material. */
export async function listCredentialGroupMcpConnectionReferences({
workspaceId,
+ organizationId,
credentialGroupId,
limit,
cursor,
email,
mcpServerId,
+ connectorId,
}: ListCredentialGroupMcpConnectionReferencesInput): Promise<{
mcpConnections: CredentialGroupMcpConnectionReference[]
nextCursor: string | null
}> {
+ const ownerScope = resourceScopeFromOwner({ workspaceId, organizationId })
const scope = () =>
and(
- eq(credential.workspaceId, workspaceId),
+ resourceScopeCondition(credential, ownerScope),
eq(credential.type, 'managed_mcp'),
eq(credential.managedOauthStatus, 'active'),
+ eq(credential.mcpOauthConfigVersion, mcpServers.oauthConfigVersion),
eq(credentialGroup.id, credentialGroupId),
- eq(credentialGroup.workspaceId, workspaceId),
+ resourceScopeCondition(credentialGroup, ownerScope),
eq(credentialGroup.status, 'active'),
inArray(credentialGroupEnrollment.status, ['in_progress', 'completed']),
- eq(mcpServers.workspaceId, workspaceId),
+ resourceScopeCondition(mcpServers, ownerScope),
+ organizationId ? isNotNull(credentialGroupEnrollment.userId) : undefined,
eq(mcpServers.authType, 'oauth'),
eq(mcpServers.enabled, true),
isNull(mcpServers.deletedAt),
sql`${mcpServers.credentialGroupId} = ${credentialGroup.id}`,
email ? eq(credentialGroupEnrollment.email, email) : undefined,
- mcpServerId ? eq(mcpServers.id, mcpServerId) : undefined
+ mcpServerId ? eq(mcpServers.id, mcpServerId) : undefined,
+ connectorId ? eq(mcpServers.managedConnectorId, connectorId) : undefined
)
let cursorPosition: { id: string; createdAt: Date } | undefined
diff --git a/apps/sim/lib/credential-groups/mcp-oauth-state.test.ts b/apps/sim/lib/credential-groups/mcp-oauth-state.test.ts
index dc0bfa802d4..e87ada68848 100644
--- a/apps/sim/lib/credential-groups/mcp-oauth-state.test.ts
+++ b/apps/sim/lib/credential-groups/mcp-oauth-state.test.ts
@@ -2,6 +2,7 @@
* @vitest-environment node
*/
import { beforeEach, describe, expect, it, vi } from 'vitest'
+import { CredentialGroupOAuthStateVersionError } from '@/lib/credential-groups/oauth-attempt-version'
const { mockRedis, values } = vi.hoisted(() => {
const values = new Map()
@@ -57,9 +58,11 @@ describe('Credential Group MCP OAuth state', () => {
await createCredentialGroupMcpOAuthAttempt({
state,
workspaceId: 'workspace-1',
+ userId: 'user-1',
email: 'person@example.com',
enrollmentId: 'enrollment-1',
credentialGroupId: 'group-1',
+ oauthConfigVersion: 1,
mcpServerId: 'mcp-server-1',
codeVerifier: 'code-verifier',
invitationToken: 'invitation-token',
@@ -72,9 +75,11 @@ describe('Credential Group MCP OAuth state', () => {
await expect(consumeCredentialGroupMcpOAuthAttempt(state)).resolves.toMatchObject({
state,
workspaceId: 'workspace-1',
+ userId: 'user-1',
email: 'person@example.com',
enrollmentId: 'enrollment-1',
credentialGroupId: 'group-1',
+ oauthConfigVersion: 1,
mcpServerId: 'mcp-server-1',
codeVerifier: 'code-verifier',
invitationToken: 'invitation-token',
@@ -89,9 +94,11 @@ describe('Credential Group MCP OAuth state', () => {
createCredentialGroupMcpOAuthAttempt({
state: 'mcp_cg_state-1',
workspaceId: 'workspace-1',
+ userId: 'user-1',
email: 'person@example.com',
enrollmentId: 'enrollment-1',
credentialGroupId: 'group-1',
+ oauthConfigVersion: 1,
mcpServerId: 'mcp-server-1',
codeVerifier: 'code-verifier',
invitationToken: 'invitation-token',
@@ -104,9 +111,11 @@ describe('Credential Group MCP OAuth state', () => {
createCredentialGroupMcpOAuthAttempt({
state: 'ordinary-state',
workspaceId: 'workspace-1',
+ userId: 'user-1',
email: 'person@example.com',
enrollmentId: 'enrollment-1',
credentialGroupId: 'group-1',
+ oauthConfigVersion: 1,
mcpServerId: 'mcp-server-1',
codeVerifier: 'code-verifier',
invitationToken: 'invitation-token',
@@ -117,9 +126,11 @@ describe('Credential Group MCP OAuth state', () => {
it('keeps parallel MCP attempts pinned to their original enrollment when the invitation rotates', async () => {
const params = {
workspaceId: 'workspace-1',
+ userId: 'user-1',
email: 'person@example.com',
enrollmentId: 'enrollment-1',
credentialGroupId: 'group-1',
+ oauthConfigVersion: 1,
mcpServerId: 'mcp-server-1',
codeVerifier: 'verifier',
invitationToken: 'first-invitation',
@@ -143,9 +154,11 @@ describe('Credential Group MCP OAuth state', () => {
await createCredentialGroupMcpOAuthAttempt({
state: 'mcp_cg_attempt',
workspaceId: 'workspace-1',
+ userId: 'user-1',
email: 'person@example.com',
enrollmentId: 'enrollment-1',
credentialGroupId: 'group-1',
+ oauthConfigVersion: 1,
mcpServerId: 'mcp-server-1',
codeVerifier: 'verifier',
invitationToken: 'token',
@@ -160,4 +173,28 @@ describe('Credential Group MCP OAuth state', () => {
expect(await consumeCredentialGroupMcpOAuthAttempt('mcp_cg_attempt')).toBeNull()
}
)
+ it('requires a new authorization for pre-binding v2 state', async () => {
+ const state = 'mcp_cg_old'
+ await createCredentialGroupMcpOAuthAttempt({
+ state,
+ organizationId: 'org-1',
+ userId: 'user-1',
+ email: 'person@example.com',
+ enrollmentId: 'enrollment-1',
+ credentialGroupId: 'group-1',
+ oauthConfigVersion: 1,
+ mcpServerId: 'server-1',
+ codeVerifier: 'verifier',
+ invitationToken: 'token',
+ })
+ const [key, raw] = [...values.entries()][0]
+ const stored = { ...JSON.parse(raw), version: 2 }
+ stored.userId = undefined
+ stored.oauthConfigVersion = undefined
+ values.set(key, JSON.stringify(stored))
+ await expect(consumeCredentialGroupMcpOAuthAttempt(state)).rejects.toBeInstanceOf(
+ CredentialGroupOAuthStateVersionError
+ )
+ await expect(consumeCredentialGroupMcpOAuthAttempt(state)).resolves.toBeNull()
+ })
})
diff --git a/apps/sim/lib/credential-groups/mcp-oauth-state.ts b/apps/sim/lib/credential-groups/mcp-oauth-state.ts
index e82b76c699c..b8dfd1bf875 100644
--- a/apps/sim/lib/credential-groups/mcp-oauth-state.ts
+++ b/apps/sim/lib/credential-groups/mcp-oauth-state.ts
@@ -1,9 +1,11 @@
import { sha256Hex } from '@sim/security/hash'
import { getRedisClient } from '@/lib/core/config/redis'
+import { resourceScopeFields, resourceScopeFromOwner } from '@/lib/core/resource-scope'
import { decryptSecret, encryptSecret } from '@/lib/core/security/encryption'
+import { assertCredentialGroupOAuthAttemptVersion } from '@/lib/credential-groups/oauth-attempt-version'
const MCP_OAUTH_ATTEMPT_TTL_MS = 10 * 60 * 1000
-const MCP_OAUTH_ATTEMPT_VERSION = 2 as const
+const MCP_OAUTH_ATTEMPT_VERSION = 3 as const
const MCP_OAUTH_STATE_PREFIX = 'mcp_cg_'
const CONSUME_SCRIPT = `
@@ -25,8 +27,11 @@ return #keys
`
interface StoredCredentialGroupMcpOAuthAttempt {
+ oauthConfigVersion: number
+ userId: string
version: typeof MCP_OAUTH_ATTEMPT_VERSION
- workspaceId: string
+ workspaceId?: string
+ organizationId?: string
email: string
enrollmentId: string
credentialGroupId: string
@@ -37,8 +42,11 @@ interface StoredCredentialGroupMcpOAuthAttempt {
}
export interface CredentialGroupMcpOAuthAttempt {
+ oauthConfigVersion: number
+ userId: string
state: string
- workspaceId: string
+ workspaceId?: string
+ organizationId?: string
email: string
enrollmentId: string
credentialGroupId: string
@@ -67,8 +75,17 @@ function isStoredAttempt(value: unknown): value is StoredCredentialGroupMcpOAuth
const candidate = value as Record
return (
candidate.version === MCP_OAUTH_ATTEMPT_VERSION &&
- typeof candidate.workspaceId === 'string' &&
- candidate.workspaceId.length > 0 &&
+ typeof candidate.oauthConfigVersion === 'number' &&
+ Number.isInteger(candidate.oauthConfigVersion) &&
+ candidate.oauthConfigVersion > 0 &&
+ typeof candidate.userId === 'string' &&
+ candidate.userId.length > 0 &&
+ ((typeof candidate.workspaceId === 'string' &&
+ candidate.workspaceId.length > 0 &&
+ candidate.organizationId === undefined) ||
+ (typeof candidate.organizationId === 'string' &&
+ candidate.organizationId.length > 0 &&
+ candidate.workspaceId === undefined)) &&
typeof candidate.email === 'string' &&
candidate.email.length >= 3 &&
candidate.email.length <= 320 &&
@@ -86,8 +103,11 @@ export function isCredentialGroupMcpOAuthState(state: string): boolean {
}
export async function createCredentialGroupMcpOAuthAttempt(params: {
+ oauthConfigVersion: number
+ userId: string
state: string
- workspaceId: string
+ workspaceId?: string
+ organizationId?: string
email: string
enrollmentId: string
credentialGroupId: string
@@ -105,7 +125,9 @@ export async function createCredentialGroupMcpOAuthAttempt(params: {
])
const attempt: StoredCredentialGroupMcpOAuthAttempt = {
version: MCP_OAUTH_ATTEMPT_VERSION,
- workspaceId: params.workspaceId,
+ oauthConfigVersion: params.oauthConfigVersion,
+ userId: params.userId,
+ ...resourceScopeFields(resourceScopeFromOwner(params)),
email: params.email,
enrollmentId: params.enrollmentId,
credentialGroupId: params.credentialGroupId,
@@ -134,6 +156,7 @@ export async function consumeCredentialGroupMcpOAuthAttempt(
if (raw === null) return null
if (typeof raw !== 'string') throw new Error('Credential Group MCP OAuth state is malformed')
const parsed: unknown = JSON.parse(raw)
+ assertCredentialGroupOAuthAttemptVersion(parsed, MCP_OAUTH_ATTEMPT_VERSION)
if (!isStoredAttempt(parsed)) throw new Error('Credential Group MCP OAuth state is malformed')
await requireRedis().srem(serverAttemptsKey(parsed.mcpServerId), attemptKey(state))
if (Date.now() - parsed.createdAt > MCP_OAUTH_ATTEMPT_TTL_MS) return null
@@ -143,7 +166,9 @@ export async function consumeCredentialGroupMcpOAuthAttempt(
])
return {
state,
- workspaceId: parsed.workspaceId,
+ oauthConfigVersion: parsed.oauthConfigVersion,
+ userId: parsed.userId,
+ ...resourceScopeFields(resourceScopeFromOwner(parsed)),
email: parsed.email,
enrollmentId: parsed.enrollmentId,
credentialGroupId: parsed.credentialGroupId,
diff --git a/apps/sim/lib/credential-groups/mcp-oauth.ts b/apps/sim/lib/credential-groups/mcp-oauth.ts
index 1f9d4415eec..98cf25f6ee4 100644
--- a/apps/sim/lib/credential-groups/mcp-oauth.ts
+++ b/apps/sim/lib/credential-groups/mcp-oauth.ts
@@ -1,4 +1,6 @@
import type { OAuthTokens } from '@modelcontextprotocol/sdk/shared/auth.js'
+import { sha256Hex } from '@sim/security/hash'
+import { resourceScopeFields, resourceScopeFromOwner } from '@/lib/core/resource-scope'
import type { CredentialGroupMcpOAuthContext } from '@/lib/credential-groups/enrollments'
import { createCredentialGroupMcpOAuthAttempt } from '@/lib/credential-groups/mcp-oauth-state'
import { encryptManagedMcpTokens, persistManagedMcpCredential } from '@/lib/credentials/managed-mcp'
@@ -21,7 +23,7 @@ export async function startCredentialGroupMcpOAuth(
return withMcpOauthRefreshLock(context.server.id, async () => {
const clientRow = await getOrCreateOauthRow({
mcpServerId: context.server.id,
- workspaceId: context.workspaceId,
+ ...resourceScopeFields(resourceScopeFromOwner(context)),
})
const preregistered = await loadPreregisteredClient(context.server.id)
const provider = new ManagedMcpOauthProvider({
@@ -42,8 +44,10 @@ export async function startCredentialGroupMcpOAuth(
if (!(error instanceof McpOauthRedirectRequired)) throw error
const attempt = provider.requireAuthorizationAttempt()
await createCredentialGroupMcpOAuthAttempt({
+ oauthConfigVersion: context.server.oauthConfigVersion,
...attempt,
- workspaceId: context.workspaceId,
+ userId: context.userId,
+ ...resourceScopeFields(resourceScopeFromOwner(context)),
email: context.email,
enrollmentId: context.enrollmentId,
credentialGroupId: context.credentialGroupId,
@@ -58,12 +62,13 @@ export async function startCredentialGroupMcpOAuth(
export async function completeCredentialGroupMcpOAuth(
context: CredentialGroupMcpOAuthContext,
codeVerifier: string,
- authorizationCode: string
-): Promise<{ connectionId: string; mcpServerId: string }> {
+ authorizationCode: string,
+ invitationToken: string
+) {
assertSafeOauthServerUrl(context.server.url)
const clientRow = await getOrCreateOauthRow({
mcpServerId: context.server.id,
- workspaceId: context.workspaceId,
+ ...resourceScopeFields(resourceScopeFromOwner(context)),
})
const preregistered = await loadPreregisteredClient(context.server.id)
let grantedTokens: OAuthTokens | undefined
@@ -89,16 +94,19 @@ export async function completeCredentialGroupMcpOAuth(
}
const tools = await mcpService.discoverManagedMcpTools(
context.server.id,
- context.workspaceId,
+ resourceScopeFromOwner(context),
provider,
undefined,
{ requireComplete: true }
)
- const connectionId = await persistManagedMcpCredential({
+ const completion = await persistManagedMcpCredential({
+ invitationTokenHash: sha256Hex(invitationToken),
+ oauthConfigVersion: context.server.oauthConfigVersion,
enrollmentId: context.enrollmentId,
credentialGroupId: context.credentialGroupId,
email: context.email,
- workspaceId: context.workspaceId,
+ userId: context.userId,
+ ...resourceScopeFields(resourceScopeFromOwner(context)),
mcpServerId: context.server.id,
mcpServerName: context.server.name,
tokens: grantedTokens,
@@ -108,5 +116,5 @@ export async function completeCredentialGroupMcpOAuth(
inputSchema: tool.inputSchema,
})),
})
- return { connectionId, mcpServerId: context.server.id }
+ return { ...completion, mcpServerId: context.server.id }
}
diff --git a/apps/sim/lib/credential-groups/oauth-attempt-version.ts b/apps/sim/lib/credential-groups/oauth-attempt-version.ts
new file mode 100644
index 00000000000..8c4159b56eb
--- /dev/null
+++ b/apps/sim/lib/credential-groups/oauth-attempt-version.ts
@@ -0,0 +1,20 @@
+/** An attempt from another release cannot establish the identity and configuration bindings required now. */
+export class CredentialGroupOAuthStateVersionError extends Error {
+ constructor() {
+ super('Authorization was started before an update. Reopen your invitation and connect again.')
+ this.name = 'CredentialGroupOAuthStateVersionError'
+ }
+}
+
+/** Rejects a different state protocol before decrypting any authorization material. */
+export function assertCredentialGroupOAuthAttemptVersion(value: unknown, expected: number): void {
+ if (
+ value !== null &&
+ typeof value === 'object' &&
+ 'version' in value &&
+ typeof value.version === 'number' &&
+ value.version !== expected
+ ) {
+ throw new CredentialGroupOAuthStateVersionError()
+ }
+}
diff --git a/apps/sim/lib/credential-groups/oauth-state.test.ts b/apps/sim/lib/credential-groups/oauth-state.test.ts
index 28bab899e50..43367d7a768 100644
--- a/apps/sim/lib/credential-groups/oauth-state.test.ts
+++ b/apps/sim/lib/credential-groups/oauth-state.test.ts
@@ -2,6 +2,7 @@
* @vitest-environment node
*/
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
+import { CredentialGroupOAuthStateVersionError } from '@/lib/credential-groups/oauth-attempt-version'
const { mockRedis, values } = vi.hoisted(() => {
const values = new Map()
@@ -58,6 +59,7 @@ describe('credential group OAuth state', () => {
const created = await createCredentialGroupOAuthAttempt({
provider: 'gmail',
workspaceId: 'workspace-1',
+ userId: 'user-1',
email: 'person@example.com',
enrollmentId: 'enrollment-1',
credentialGroupId: 'group-1',
@@ -81,6 +83,7 @@ describe('credential group OAuth state', () => {
expect(consumed).toMatchObject({
provider: 'gmail',
workspaceId: 'workspace-1',
+ userId: 'user-1',
email: 'person@example.com',
enrollmentId: 'enrollment-1',
credentialGroupId: 'group-1',
@@ -100,6 +103,7 @@ describe('credential group OAuth state', () => {
createCredentialGroupOAuthAttempt({
provider: 'gmail',
workspaceId: 'workspace-1',
+ userId: 'user-1',
email: 'person@example.com',
enrollmentId: 'enrollment-1',
credentialGroupId: 'group-1',
@@ -118,6 +122,7 @@ describe('credential group OAuth state', () => {
const created = await createCredentialGroupOAuthAttempt({
provider: 'github-repositories',
workspaceId: 'workspace-1',
+ userId: 'user-1',
email: 'person@example.com',
enrollmentId: 'enrollment-1',
credentialGroupId: 'group-1',
@@ -146,6 +151,7 @@ describe('credential group OAuth state', () => {
const created = await createCredentialGroupOAuthAttempt({
provider: 'slack',
workspaceId: 'workspace-1',
+ userId: 'user-1',
email: 'person@example.com',
enrollmentId: 'enrollment-1',
credentialGroupId: 'group-1',
@@ -173,6 +179,7 @@ describe('credential group OAuth state', () => {
const params = {
provider: 'gmail' as const,
workspaceId: 'workspace-1',
+ userId: 'user-1',
email: 'person@example.com',
enrollmentId: 'enrollment-1',
credentialGroupId: 'group-1',
@@ -193,6 +200,7 @@ describe('credential group OAuth state', () => {
const secondAttempt = await consumeCredentialGroupOAuthAttempt(second.state)
expect(firstAttempt).toMatchObject({
workspaceId: params.workspaceId,
+ userId: 'user-1',
email: params.email,
invitationToken: params.invitationToken,
optionId: 'option-1',
@@ -200,6 +208,7 @@ describe('credential group OAuth state', () => {
})
expect(secondAttempt).toMatchObject({
workspaceId: params.workspaceId,
+ userId: 'user-1',
email: params.email,
invitationToken: 'second-invitation',
optionId: 'option-2',
@@ -213,6 +222,7 @@ describe('credential group OAuth state', () => {
const created = await createCredentialGroupOAuthAttempt({
provider: 'gmail',
workspaceId: 'workspace-1',
+ userId: 'user-1',
email: 'person@example.com',
enrollmentId: 'enrollment-1',
credentialGroupId: 'group-1',
@@ -235,6 +245,7 @@ describe('credential group OAuth state', () => {
const created = await createCredentialGroupOAuthAttempt({
provider: 'gmail',
workspaceId: 'workspace-1',
+ userId: 'user-1',
email: 'person@example.com',
enrollmentId: 'enrollment-1',
credentialGroupId: 'group-1',
@@ -259,6 +270,7 @@ describe('organization enrollment OAuth state', () => {
const input = {
provider: 'gmail' as const,
organizationId: 'org-1',
+ userId: 'user-1',
email: 'person@example.com',
enrollmentId: 'enrollment-1',
credentialGroupId: 'group-1',
@@ -278,7 +290,7 @@ describe('organization enrollment OAuth state', () => {
it('round trips explicit organization ownership and preserves the setup return destination', async () => {
const { state } = await createCredentialGroupOAuthAttempt(input)
const raw = JSON.parse([...values.values()][0]!)
- expect(raw.version).toBe(4)
+ expect(raw.version).toBe(5)
expect(raw.organizationId).toBe('org-1')
expect(raw.workspaceId).toBeUndefined()
const attempt = await consumeCredentialGroupOAuthAttempt(state)
@@ -296,11 +308,15 @@ describe('organization enrollment OAuth state', () => {
).rejects.toThrow('exactly one')
expect(mockRedis.set).not.toHaveBeenCalled()
})
- it('does not interpret old workspace-only state as organization authority', async () => {
+ it.each([3, 4])('requires a new authorization for pre-binding v%s state', async (version) => {
const { state } = await createCredentialGroupOAuthAttempt(input)
const [key, raw] = [...values.entries()][0]!
- values.set(key, JSON.stringify({ ...JSON.parse(raw), version: 3 }))
- await expect(consumeCredentialGroupOAuthAttempt(state)).rejects.toThrow('malformed')
+ const stored = { ...JSON.parse(raw), version }
+ stored.userId = undefined
+ values.set(key, JSON.stringify(stored))
+ await expect(consumeCredentialGroupOAuthAttempt(state)).rejects.toBeInstanceOf(
+ CredentialGroupOAuthStateVersionError
+ )
await expect(consumeCredentialGroupOAuthAttempt(state)).resolves.toBeNull()
})
})
diff --git a/apps/sim/lib/credential-groups/oauth-state.ts b/apps/sim/lib/credential-groups/oauth-state.ts
index d6cda024387..e832e82ebca 100644
--- a/apps/sim/lib/credential-groups/oauth-state.ts
+++ b/apps/sim/lib/credential-groups/oauth-state.ts
@@ -4,14 +4,14 @@ import { generateId } from '@sim/utils/id'
import { getRedisClient } from '@/lib/core/config/redis'
import { resourceScopeFields, resourceScopeFromOwner } from '@/lib/core/resource-scope'
import { decryptSecret, encryptSecret } from '@/lib/core/security/encryption'
+import { assertCredentialGroupOAuthAttemptVersion } from '@/lib/credential-groups/oauth-attempt-version'
import {
type CredentialGroupProvider,
isCredentialGroupProvider,
} from '@/lib/credential-groups/providers'
const OAUTH_ATTEMPT_TTL_MS = 10 * 60 * 1000
-const OAUTH_ATTEMPT_VERSION = 4 as const
-const LEGACY_OAUTH_ATTEMPT_VERSION = 3 as const
+const OAUTH_ATTEMPT_VERSION = 5 as const
const OAUTH_ATTEMPT_STATE_PREFIX = 'cg_'
const CONSUME_SCRIPT = `
@@ -24,7 +24,8 @@ return value
`
interface StoredCredentialGroupOAuthAttempt {
- version: typeof OAUTH_ATTEMPT_VERSION | typeof LEGACY_OAUTH_ATTEMPT_VERSION
+ version: typeof OAUTH_ATTEMPT_VERSION
+ userId: string
provider: CredentialGroupProvider
workspaceId?: string
organizationId?: string
@@ -45,6 +46,7 @@ interface StoredCredentialGroupOAuthAttempt {
}
export interface CredentialGroupOAuthAttempt {
+ userId: string
state: string
provider: CredentialGroupProvider
nonceHash: string
@@ -66,6 +68,7 @@ export interface CredentialGroupOAuthAttempt {
}
interface CreateCredentialGroupOAuthAttemptParams {
+ userId: string
provider: CredentialGroupProvider
workspaceId?: string
organizationId?: string
@@ -99,8 +102,9 @@ function isStoredAttempt(value: unknown): value is StoredCredentialGroupOAuthAtt
if (!value || typeof value !== 'object') return false
const candidate = value as Record
return (
- (candidate.version === OAUTH_ATTEMPT_VERSION ||
- candidate.version === LEGACY_OAUTH_ATTEMPT_VERSION) &&
+ candidate.version === OAUTH_ATTEMPT_VERSION &&
+ typeof candidate.userId === 'string' &&
+ candidate.userId.length > 0 &&
typeof candidate.provider === 'string' &&
isCredentialGroupProvider(candidate.provider) &&
((typeof candidate.workspaceId === 'string' &&
@@ -146,7 +150,8 @@ export async function createCredentialGroupOAuthAttempt(
encryptSecret(params.invitationToken),
])
const attempt: StoredCredentialGroupOAuthAttempt = {
- version: params.organizationId ? OAUTH_ATTEMPT_VERSION : LEGACY_OAUTH_ATTEMPT_VERSION,
+ version: OAUTH_ATTEMPT_VERSION,
+ userId: params.userId,
provider: params.provider,
...resourceScopeFields(resourceScopeFromOwner(params)),
email: params.email,
@@ -190,6 +195,7 @@ export async function consumeCredentialGroupOAuthAttempt(
if (typeof raw !== 'string') throw new Error('Credential group OAuth state is malformed')
const parsed: unknown = JSON.parse(raw)
+ assertCredentialGroupOAuthAttemptVersion(parsed, OAUTH_ATTEMPT_VERSION)
if (!isStoredAttempt(parsed)) throw new Error('Credential group OAuth state is malformed')
if (Date.now() - parsed.createdAt > OAUTH_ATTEMPT_TTL_MS) return null
@@ -199,6 +205,7 @@ export async function consumeCredentialGroupOAuthAttempt(
])
return {
state,
+ userId: parsed.userId,
provider: parsed.provider,
nonceHash: parsed.nonceHash,
...resourceScopeFields(resourceScopeFromOwner(parsed)),
diff --git a/apps/sim/lib/credential-groups/oauth.test.ts b/apps/sim/lib/credential-groups/oauth.test.ts
index b4ec99900be..e748e546aa2 100644
--- a/apps/sim/lib/credential-groups/oauth.test.ts
+++ b/apps/sim/lib/credential-groups/oauth.test.ts
@@ -47,6 +47,7 @@ import {
startCredentialGroupOAuth,
} from '@/lib/credential-groups/oauth'
import { CredentialGroupInvitationUnavailableError } from '@/lib/credential-groups/provider-adapter'
+import { dispatchMemberSyncsForCredentialOption } from '@/lib/knowledge/connectors/member-queue'
const POLICY = {
provider: 'gmail' as const,
@@ -63,6 +64,7 @@ const CONTEXT = {
workspaceId: 'workspace-1',
workspaceName: 'Workspace',
workspaceOwnerId: 'owner-1',
+ credentialOwnerId: 'person-1',
email: 'person@example.com',
enrollmentStatus: 'in_progress' as const,
option: {
@@ -144,6 +146,7 @@ describe('credential group OAuth persistence', () => {
CONTEXT,
{
state: 'state-1',
+ userId: 'person-1',
provider: 'gmail',
nonceHash: 'nonce-hash',
workspaceId: CONTEXT.workspaceId,
@@ -180,6 +183,7 @@ describe('credential group OAuth persistence', () => {
CONTEXT,
{
state: 'state-1',
+ userId: 'person-1',
provider: 'gmail',
nonceHash: 'nonce-hash',
workspaceId: CONTEXT.workspaceId,
@@ -211,7 +215,7 @@ describe('credential group OAuth persistence', () => {
})
it.each([true, false])(
- 'rechecks organization membership after the provider exchange (member=%s)',
+ 'accepts the bound person without requiring organization membership (member=%s)',
async (currentMember) => {
const context = {
...CONTEXT,
@@ -222,6 +226,7 @@ describe('credential group OAuth persistence', () => {
}
const attempt = {
state: 'state-1',
+ userId: 'person-1',
provider: 'gmail' as const,
nonceHash: 'nonce',
organizationId: 'org-1',
@@ -244,23 +249,25 @@ describe('credential group OAuth persistence', () => {
dbChainMockFns.returning
.mockResolvedValueOnce([{ id: 'credential-1' }])
.mockResolvedValueOnce([{ id: CONTEXT.enrollmentId }])
- if (currentMember) {
- await expect(
- completeCredentialGroupOAuth(context, attempt, 'authorization-code')
- ).resolves.toMatchObject({ credentialId: 'credential-1', created: true })
- expect(dbChainMockFns.values).toHaveBeenCalledWith(
- expect.objectContaining({
- organizationId: 'org-1',
- workspaceId: null,
- createdBy: 'person-1',
- })
- )
- } else {
- await expect(
- completeCredentialGroupOAuth(context, attempt, 'authorization-code')
- ).rejects.toBeInstanceOf(CredentialGroupInvitationUnavailableError)
- expect(dbChainMockFns.insert).not.toHaveBeenCalled()
- }
+ await expect(
+ completeCredentialGroupOAuth(context, attempt, 'authorization-code')
+ ).resolves.toMatchObject({ credentialId: 'credential-1', created: true })
+ expect(dbChainMockFns.values).toHaveBeenCalledWith(
+ expect.objectContaining({
+ organizationId: 'org-1',
+ workspaceId: null,
+ createdBy: 'person-1',
+ })
+ )
+ expect(eq).toHaveBeenCalledWith(schemaMock.credentialGroupEnrollment.userId, 'person-1')
+ expect(dispatchMemberSyncsForCredentialOption).toHaveBeenCalledWith({
+ organizationId: 'org-1',
+ credentialGroupOptionId: 'option-1',
+ })
+ expect(eq).toHaveBeenCalledWith(
+ schemaMock.credentialGroupEnrollment.invitationTokenHash,
+ expect.any(String)
+ )
}
)
@@ -283,6 +290,7 @@ describe('credential group OAuth persistence', () => {
{ ...CONTEXT, enrollmentStatus: 'completed' },
{
state: 'state-1',
+ userId: 'person-1',
provider: 'gmail',
nonceHash: 'nonce-hash',
workspaceId: CONTEXT.workspaceId,
@@ -343,6 +351,7 @@ describe('credential group OAuth persistence', () => {
{ ...CONTEXT, enrollmentStatus: 'completed' },
{
state: 'state-1',
+ userId: 'person-1',
provider: 'gmail',
nonceHash: 'nonce-hash',
workspaceId: CONTEXT.workspaceId,
@@ -380,6 +389,7 @@ describe('credential group OAuth persistence', () => {
CONTEXT,
{
state: 'state',
+ userId: 'person-1',
provider: 'gmail',
workspaceId: CONTEXT.workspaceId,
email: CONTEXT.email,
diff --git a/apps/sim/lib/credential-groups/oauth.ts b/apps/sim/lib/credential-groups/oauth.ts
index 3ad58a58d16..8bb3afb9494 100644
--- a/apps/sim/lib/credential-groups/oauth.ts
+++ b/apps/sim/lib/credential-groups/oauth.ts
@@ -1,10 +1,10 @@
import { db } from '@sim/db'
-import { credential, credentialGroup, credentialGroupEnrollment, member } from '@sim/db/schema'
+import { credential, credentialGroup, credentialGroupEnrollment } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
+import { sha256Hex } from '@sim/security/hash'
import { getErrorMessage } from '@sim/utils/errors'
import { generateId } from '@sim/utils/id'
import { and, eq, ne, sql } from 'drizzle-orm'
-import { acquireOrganizationUserMutationLocks } from '@/lib/billing/organizations/membership'
import {
resourceScopeColumns,
resourceScopeFields,
@@ -112,10 +112,12 @@ export async function startCredentialGroupOAuth(
invitationToken: string,
options: { completionRedirect?: boolean; returnTo?: 'search' } = {}
): Promise {
+ if (!context.credentialOwnerId) throw new CredentialGroupInvitationUnavailableError()
const adapter = getOptionAdapter(context)
const policy = await assertCurrentPolicy(context, adapter)
const prepared = await adapter.prepareAuthorization(context, policy)
const { state, nonce } = await createCredentialGroupOAuthAttempt({
+ userId: context.credentialOwnerId,
provider: policy.provider,
...resourceScopeFields(resourceScopeFromOwner(context)),
email: context.email,
@@ -138,31 +140,15 @@ async function persistGrant(
context: CredentialGroupOAuthContext,
adapter: CredentialGroupProviderAdapter,
policy: CredentialGroupProviderPolicy,
- grant: VerifiedCredentialGroupGrant
+ grant: VerifiedCredentialGroupGrant,
+ invitationTokenHash: string
): Promise {
if (grant.providerId !== policy.providerId) {
throw new CredentialGroupOAuthError('Provider returned a credential for another app.', 502)
}
const completion: CredentialGroupOAuthCompletion = await db.transaction(async (tx) => {
- if (context.organizationId) {
- if (!context.credentialOwnerId) throw new CredentialGroupInvitationUnavailableError()
- await acquireOrganizationUserMutationLocks(tx, {
- userId: context.credentialOwnerId,
- organizationIds: [context.organizationId],
- })
- const [membership] = await tx
- .select({ id: member.id })
- .from(member)
- .where(
- and(
- eq(member.organizationId, context.organizationId),
- eq(member.userId, context.credentialOwnerId)
- )
- )
- .limit(1)
- if (!membership) throw new CredentialGroupInvitationUnavailableError()
- }
+ if (!context.credentialOwnerId) throw new CredentialGroupInvitationUnavailableError()
await lockCredentialGroupEnrollmentLifecycle(tx, context.enrollmentId)
await tx.execute(
sql`SELECT pg_advisory_xact_lock(hashtextextended(${`credential-group-oauth:${context.enrollmentId}:${context.option.id}`}, 0))`
@@ -177,7 +163,9 @@ async function persistGrant(
and(
eq(credentialGroupEnrollment.id, context.enrollmentId),
eq(credentialGroupEnrollment.credentialGroupId, context.credentialGroupId),
- eq(credentialGroupEnrollment.email, context.email)
+ eq(credentialGroupEnrollment.email, context.email),
+ eq(credentialGroupEnrollment.userId, context.credentialOwnerId),
+ eq(credentialGroupEnrollment.invitationTokenHash, invitationTokenHash)
)
)
.limit(1)
@@ -301,7 +289,7 @@ async function persistGrant(
.values({
id: generateId(),
...values,
- createdBy: context.credentialOwnerId ?? context.workspaceOwnerId,
+ createdBy: context.credentialOwnerId,
createdAt: now,
})
.returning({ id: credential.id })
@@ -371,6 +359,7 @@ export async function completeCredentialGroupOAuth(
attempt.email !== context.email ||
attempt.enrollmentId !== context.enrollmentId ||
attempt.credentialGroupId !== context.credentialGroupId ||
+ attempt.userId !== context.credentialOwnerId ||
attempt.optionId !== context.option.id ||
attempt.provider !== context.option.provider
) {
@@ -379,5 +368,5 @@ export async function completeCredentialGroupOAuth(
const adapter = getOptionAdapter(context)
const policy = await assertCurrentPolicy(context, adapter, attempt)
const grant = await adapter.exchangeAndVerify({ context, attempt, code, policy })
- return persistGrant(context, adapter, policy, grant)
+ return persistGrant(context, adapter, policy, grant, sha256Hex(attempt.invitationToken))
}
diff --git a/apps/sim/lib/credential-groups/organization-setup.test.ts b/apps/sim/lib/credential-groups/organization-setup.test.ts
new file mode 100644
index 00000000000..b9dd5cef58c
--- /dev/null
+++ b/apps/sim/lib/credential-groups/organization-setup.test.ts
@@ -0,0 +1,29 @@
+/** @vitest-environment node */
+import { beforeEach, describe, expect, it, vi } from 'vitest'
+
+const mocks = vi.hoisted(() => ({ policy: vi.fn() }))
+vi.mock('@/lib/resource-policies/repository', () => ({
+ requireResourcePolicy: mocks.policy,
+ ResourcePolicyNotFoundError: class extends Error {},
+}))
+
+import { requireOrganizationAccountsSetup } from '@/lib/credential-groups/organization-setup'
+import { ResourcePolicyNotFoundError } from '@/lib/resource-policies/repository'
+
+describe('fresh organization account setup', () => {
+ beforeEach(() => vi.clearAllMocks())
+ it('requires an existing org policy instead of creating grants for a legacy group', async () => {
+ mocks.policy.mockRejectedValue(
+ new ResourcePolicyNotFoundError('credential_group', 'legacy-group')
+ )
+ await expect(requireOrganizationAccountsSetup('org-1', 'legacy-group')).rejects.toMatchObject({
+ code: 'conflict',
+ })
+ })
+ it('does not conceal a malformed policy', async () => {
+ mocks.policy.mockRejectedValue(new Error('Malformed policy'))
+ await expect(requireOrganizationAccountsSetup('org-1', 'group-1')).rejects.toThrow(
+ 'Malformed policy'
+ )
+ })
+})
diff --git a/apps/sim/lib/credential-groups/organization-setup.ts b/apps/sim/lib/credential-groups/organization-setup.ts
new file mode 100644
index 00000000000..8eaf50b58d7
--- /dev/null
+++ b/apps/sim/lib/credential-groups/organization-setup.ts
@@ -0,0 +1,33 @@
+import { OrchestrationError } from '@/lib/core/orchestration/types'
+import { organizationAccountAccessPolicyCodec } from '@/lib/credential-groups/application/workspace-access-policy'
+import type { DbOrTx } from '@/lib/db/types'
+import {
+ ResourcePolicyNotFoundError,
+ requireResourcePolicy,
+} from '@/lib/resource-policies/repository'
+
+/** A v2 policy is created atomically with a fresh org group; legacy grants are never adopted automatically. */
+export async function requireOrganizationAccountsSetup(
+ organizationId: string,
+ credentialGroupId: string,
+ executor?: DbOrTx
+): Promise {
+ try {
+ await requireResourcePolicy(
+ {
+ organizationId,
+ resourceType: 'credential_group',
+ resourceId: credentialGroupId,
+ codec: organizationAccountAccessPolicyCodec,
+ },
+ executor
+ )
+ } catch (error) {
+ if (error instanceof ResourcePolicyNotFoundError)
+ throw new OrchestrationError(
+ 'conflict',
+ 'Existing organization accounts require a migration review. Check Search dependencies and reconnect people before enabling workflow sharing.'
+ )
+ throw error
+ }
+}
diff --git a/apps/sim/lib/credential-groups/provider-configuration.ts b/apps/sim/lib/credential-groups/provider-configuration.ts
index eb1e496371e..a5b5fef6461 100644
--- a/apps/sim/lib/credential-groups/provider-configuration.ts
+++ b/apps/sim/lib/credential-groups/provider-configuration.ts
@@ -12,7 +12,7 @@ const CREDENTIAL_GROUP_PROVIDER_CONFIGURATION_TYPE =
const CREDENTIAL_GROUP_PROVIDER_CONFIGURATION_VERSION = 1 as const
export interface SlackCredentialGroupConfiguration {
- slackBotCredentialId: string
+ slackBotCredentialId?: string
clientId: string
clientSecret: string
appId: string
@@ -31,7 +31,8 @@ function isSlackConfiguration(value: unknown): value is SlackCredentialGroupConf
if (!value || typeof value !== 'object') return false
const candidate = value as Record
return (
- typeof candidate.slackBotCredentialId === 'string' &&
+ (candidate.slackBotCredentialId === undefined ||
+ typeof candidate.slackBotCredentialId === 'string') &&
typeof candidate.clientId === 'string' &&
typeof candidate.clientSecret === 'string' &&
typeof candidate.appId === 'string' &&
@@ -118,7 +119,7 @@ export async function getSlackCredentialGroupConfiguration(params: {
export async function listSlackCredentialGroupConfigurationsForBot(params: {
workspaceId?: string | null
organizationId?: string | null
- slackBotCredentialId: string
+ slackBotCredentialId?: string
}): Promise {
const rows = await db
.select({ encryptedProviderConfiguration: credentialGroup.encryptedProviderConfiguration })
diff --git a/apps/sim/lib/credential-groups/scoped-availability.test.ts b/apps/sim/lib/credential-groups/scoped-availability.test.ts
index 7644078e212..f9b3a6e9523 100644
--- a/apps/sim/lib/credential-groups/scoped-availability.test.ts
+++ b/apps/sim/lib/credential-groups/scoped-availability.test.ts
@@ -54,15 +54,15 @@ describe('owner-scoped connected accounts availability', () => {
isScopedCredentialGroupsAvailable({ kind: 'organization', organizationId: 'org-1' })
).resolves.toBe(false)
})
- it('retains the existing workspace gate and payer semantics', async () => {
- const billing = { isEnterprise: true }
+ it('resolves a workspace to its organization instead of evaluating a workspace rollout', async () => {
+ const billing = { isEnterprise: true, organizationId: 'org-parent' }
mocks.workspace.mockResolvedValue(billing)
mocks.workspaceAvailable.mockResolvedValue(true)
await expect(
isScopedCredentialGroupsAvailable({ kind: 'workspace', workspaceId: 'ws-1' })
).resolves.toBe(true)
expect(mocks.workspaceAvailable).toHaveBeenCalledWith({
- workspaceId: 'ws-1',
+ organizationId: 'org-parent',
ownerBilling: billing,
})
expect(mocks.subscription).not.toHaveBeenCalled()
diff --git a/apps/sim/lib/credential-groups/scoped-availability.ts b/apps/sim/lib/credential-groups/scoped-availability.ts
index b5ec16ec3f8..8eb4649287a 100644
--- a/apps/sim/lib/credential-groups/scoped-availability.ts
+++ b/apps/sim/lib/credential-groups/scoped-availability.ts
@@ -8,11 +8,14 @@ import { isFeatureEnabled } from '@/lib/core/config/feature-flags'
import type { ResourceScope } from '@/lib/core/resource-scope'
import { isCredentialGroupsAvailable } from '@/lib/credential-groups/availability'
-/** Uses the resource's exact payer and flag scope; organization membership never implies workspace access. */
+/** Workspace callers inherit their canonical organization's rollout; authorization remains separate. */
export async function isScopedCredentialGroupsAvailable(scope: ResourceScope): Promise {
if (scope.kind === 'workspace') {
const ownerBilling = await getWorkspaceOwnerSubscriptionAccess(scope.workspaceId)
- return isCredentialGroupsAvailable({ workspaceId: scope.workspaceId, ownerBilling })
+ return isCredentialGroupsAvailable({
+ organizationId: ownerBilling.organizationId,
+ ownerBilling,
+ })
}
if (!(await isFeatureEnabled('credential-groups', { orgId: scope.organizationId }))) return false
if (!isHosted) return true
diff --git a/apps/sim/lib/credential-groups/self-enrollment.test.ts b/apps/sim/lib/credential-groups/self-enrollment.test.ts
index 44a83e7b0b4..a2afa86b899 100644
--- a/apps/sim/lib/credential-groups/self-enrollment.test.ts
+++ b/apps/sim/lib/credential-groups/self-enrollment.test.ts
@@ -2,9 +2,15 @@
import { dbChainMockFns, resetDbChainMock } from '@sim/testing'
import { beforeEach, describe, expect, it, vi } from 'vitest'
-const { issue } = vi.hoisted(() => ({ issue: vi.fn() }))
+const { issue, authenticate, bind } = vi.hoisted(() => ({
+ issue: vi.fn(),
+ authenticate: vi.fn(),
+ bind: vi.fn(),
+}))
vi.mock('@/lib/credential-groups/enrollments', () => ({
createCredentialGroupSelfEnrollmentLink: issue,
+ authenticatePublicCredentialGroupEnrollment: authenticate,
+ bindCredentialGroupEnrollmentUser: bind,
CredentialGroupEnrollmentError: class extends Error {
constructor(
message: string,
@@ -24,12 +30,15 @@ describe('viewer account enrollment', () => {
beforeEach(() => {
vi.clearAllMocks()
resetDbChainMock()
+ authenticate.mockResolvedValue({ enrollmentId: 'enrollment' })
+ bind.mockResolvedValue(undefined)
})
it('uses the verified account email rather than a caller-supplied address', async () => {
dbChainMockFns.limit
.mockResolvedValueOnce([{ email: ' Viewer@Example.com ', emailVerified: true }])
.mockResolvedValueOnce([])
+ .mockResolvedValueOnce([])
issue.mockResolvedValue({
enrollment: { id: 'enrollment' },
invitationLink: 'https://sim.test/enroll/token',
@@ -53,6 +62,7 @@ describe('viewer account enrollment', () => {
it('refuses a revoked enrollment without minting a link', async () => {
dbChainMockFns.limit
.mockResolvedValueOnce([{ email: 'viewer@example.com', emailVerified: true }])
+ .mockResolvedValueOnce([])
.mockResolvedValueOnce([{ status: 'revoked' }])
await expect(createViewerCredentialGroupEnrollment(input)).rejects.toThrow(
'removed your access'
@@ -64,6 +74,7 @@ describe('viewer account enrollment', () => {
dbChainMockFns.limit
.mockResolvedValueOnce([{ email: 'viewer@example.com', emailVerified: true }])
.mockResolvedValueOnce([])
+ .mockResolvedValueOnce([])
.mockResolvedValueOnce([{ status: 'revoked' }])
issue.mockRejectedValue(new CredentialGroupEnrollmentError('Revoked', 409))
await expect(createViewerCredentialGroupEnrollment(input)).rejects.toThrow(
diff --git a/apps/sim/lib/credential-groups/self-enrollment.ts b/apps/sim/lib/credential-groups/self-enrollment.ts
index 626a868fca8..14e7b4bea61 100644
--- a/apps/sim/lib/credential-groups/self-enrollment.ts
+++ b/apps/sim/lib/credential-groups/self-enrollment.ts
@@ -5,6 +5,8 @@ import { and, eq } from 'drizzle-orm'
import { OrchestrationError } from '@/lib/core/orchestration/types'
import { resourceScopeFromOwner } from '@/lib/core/resource-scope'
import {
+ authenticatePublicCredentialGroupEnrollment,
+ bindCredentialGroupEnrollmentUser,
CredentialGroupEnrollmentError,
createCredentialGroupSelfEnrollmentLink,
} from '@/lib/credential-groups/enrollments'
@@ -28,18 +30,34 @@ export async function createViewerCredentialGroupEnrollment(input: {
'Verify your email address before connecting an account'
)
}
- const email = normalizeEmail(viewer.email)
+ const [boundEnrollment] = await db
+ .select({ email: credentialGroupEnrollment.email })
+ .from(credentialGroupEnrollment)
+ .where(
+ and(
+ eq(credentialGroupEnrollment.credentialGroupId, input.credentialGroupId),
+ eq(credentialGroupEnrollment.userId, input.userId)
+ )
+ )
+ .limit(1)
+ const email = boundEnrollment?.email ?? normalizeEmail(viewer.email)
const revoked = new OrchestrationError(
'forbidden',
'An admin removed your access to Connected accounts'
)
if (await isEnrollmentRevoked(input.credentialGroupId, email)) throw revoked
try {
- return await createCredentialGroupSelfEnrollmentLink(
+ const result = await createCredentialGroupSelfEnrollmentLink(
resourceScopeFromOwner(input),
input.credentialGroupId,
email
)
+ const token = new URL(result.invitationLink).pathname.split('/').at(-1)
+ if (!token) throw new Error('Enrollment link is missing its token')
+ const identity = await authenticatePublicCredentialGroupEnrollment(token)
+ if (!identity) throw new Error('Enrollment is no longer available')
+ await bindCredentialGroupEnrollmentUser(identity, input.userId)
+ return result
} catch (error) {
/** The issue refused a revocation that landed after the read above; report it as such. */
if (
diff --git a/apps/sim/lib/credential-groups/service.ts b/apps/sim/lib/credential-groups/service.ts
index d67564355b8..d0d766f4e57 100644
--- a/apps/sim/lib/credential-groups/service.ts
+++ b/apps/sim/lib/credential-groups/service.ts
@@ -20,6 +20,7 @@ import {
import { resourceScopeCondition } from '@/lib/core/resource-scope.server'
import { decodeCredentialGroupWorkflowAccessPolicy } from '@/lib/credential-groups/application/workflow-access-policy'
import { getManagedMcpConnector } from '@/lib/credential-groups/managed-mcp-connectors'
+import { requireOrganizationAccountsSetup } from '@/lib/credential-groups/organization-setup'
import { credentialGroupScopePolicyVersion } from '@/lib/credential-groups/provider-adapter'
import { decryptCredentialGroupProviderConfiguration } from '@/lib/credential-groups/provider-configuration'
import { getCredentialGroupProviderAdapter } from '@/lib/credential-groups/provider-registry'
@@ -165,7 +166,7 @@ async function toCredentialGroup(
if (option.provider !== 'slack') {
return { ...common, provider: option.provider, configurationStatus: 'ready' as const }
}
- if (!option.slackBotCredentialId) {
+ if (row.workspaceId && !option.slackBotCredentialId) {
throw new Error(`Slack credential option ${option.id} has no custom bot`)
}
return {
@@ -175,7 +176,8 @@ async function toCredentialGroup(
requiredScopes: resolveSlackManagedUserScopes(option.requiredScopes),
configurationStatus:
!providerConfiguration.slack ||
- providerConfiguration.slack.slackBotCredentialId !== option.slackBotCredentialId
+ (row.workspaceId &&
+ providerConfiguration.slack.slackBotCredentialId !== option.slackBotCredentialId)
? ('not_configured' as const)
: option.scopeVersion !==
credentialGroupScopePolicyVersion(
@@ -282,6 +284,9 @@ export async function ensureWorkspaceAccountsGroup(
'Connected accounts is disabled. Enable it in Settings before connecting a source.'
)
}
+ if (scope.kind === 'organization') {
+ await requireOrganizationAccountsSetup(scope.organizationId, existing.id, tx)
+ }
if (!preparedOption) return existing
const matching = existing.options.filter(
(candidate) => candidate.provider === preparedOption.provider
@@ -527,6 +532,7 @@ export async function getOrganizationAccountsGroup(
.where(resourceScopeCondition(credentialGroup, { kind: 'organization', organizationId }))
.limit(1)
if (!row?.organizationId || row.workspaceId) return null
+ await requireOrganizationAccountsSetup(organizationId, row.id)
return {
...(await toCredentialGroup(row, await listLinkedMcpServers(row.id))),
workspaceId: null,
diff --git a/apps/sim/lib/credential-groups/slack-managed-users.test.ts b/apps/sim/lib/credential-groups/slack-managed-users.test.ts
index c1e2f1cf857..ba85371a37b 100644
--- a/apps/sim/lib/credential-groups/slack-managed-users.test.ts
+++ b/apps/sim/lib/credential-groups/slack-managed-users.test.ts
@@ -95,13 +95,16 @@ describe('Slack managed-user authorization', () => {
organizationId: 'org-1',
userId: 'user-1',
credentialGroupId: 'group-1',
- slackBotCredentialId: 'bot-1',
+ appId: 'A123',
+ teamId: 'T123',
clientId: 'client-1',
clientSecret: 'private-client-secret',
})
const loaded = await loadSlackManagedUsersAttempt(created.state)
expect(loaded).toMatchObject({ organizationId: 'org-1', userId: 'user-1' })
expect(loaded).not.toHaveProperty('workspaceId')
+ expect(loaded).not.toHaveProperty('slackBotCredentialId')
+ expect(fetch).not.toHaveBeenCalled()
const [key, stored] = [...attempts.entries()][0]
expect(stored).not.toContain('private-client-secret')
attempts.set(key, JSON.stringify({ ...JSON.parse(stored), workspaceId: 'workspace-1' }))
@@ -497,7 +500,7 @@ describe('Slack managed-user authorization', () => {
},
code: 'single-use-code',
})
- ).rejects.toThrow('different Slack app or workspace')
+ ).rejects.toThrow('different app or workspace')
expect(fetchMock).toHaveBeenNthCalledWith(
2,
'https://slack.com/api/auth.revoke',
diff --git a/apps/sim/lib/credential-groups/slack-managed-users.ts b/apps/sim/lib/credential-groups/slack-managed-users.ts
index 621a3bacfa1..8c3f9719cc8 100644
--- a/apps/sim/lib/credential-groups/slack-managed-users.ts
+++ b/apps/sim/lib/credential-groups/slack-managed-users.ts
@@ -26,7 +26,7 @@ import { SLACK_CUSTOM_BOT_PROVIDER_ID, SLACK_CUSTOM_BOT_SECRET_TYPE } from '@/li
const logger = createLogger('SlackManagedUsers')
const SLACK_MANAGED_USERS_ATTEMPT_TTL_MS = 10 * 60 * 1000
-const SLACK_MANAGED_USERS_ATTEMPT_VERSION = 3 as const
+const SLACK_MANAGED_USERS_ATTEMPT_VERSION = 4 as const
const MAX_SLACK_RESPONSE_BYTES = 64 * 1024
const CONSUME_SCRIPT = `
local value = redis.call('GET', KEYS[1])
@@ -54,8 +54,8 @@ interface StoredSlackManagedUsersAttempt {
userId: string
credentialGroupId: string
credentialGroupUpdatedAt: number
- slackBotCredentialId: string
- slackBotCredentialUpdatedAt: number
+ slackBotCredentialId?: string
+ slackBotCredentialUpdatedAt?: number
expectedAppId: string
expectedTeamId: string
clientId: string
@@ -71,8 +71,8 @@ export interface SlackManagedUsersAttempt {
userId: string
credentialGroupId: string
credentialGroupUpdatedAt: number
- slackBotCredentialId: string
- slackBotCredentialUpdatedAt: number
+ slackBotCredentialId?: string
+ slackBotCredentialUpdatedAt?: number
expectedAppId: string
expectedTeamId: string
clientId: string
@@ -144,8 +144,11 @@ function isStoredAttempt(value: unknown): value is StoredSlackManagedUsersAttemp
typeof candidate.userId === 'string' &&
typeof candidate.credentialGroupId === 'string' &&
typeof candidate.credentialGroupUpdatedAt === 'number' &&
- typeof candidate.slackBotCredentialId === 'string' &&
- typeof candidate.slackBotCredentialUpdatedAt === 'number' &&
+ (candidate.organizationId !== undefined
+ ? candidate.slackBotCredentialId === undefined &&
+ candidate.slackBotCredentialUpdatedAt === undefined
+ : typeof candidate.slackBotCredentialId === 'string' &&
+ typeof candidate.slackBotCredentialUpdatedAt === 'number') &&
typeof candidate.expectedAppId === 'string' &&
typeof candidate.expectedTeamId === 'string' &&
typeof candidate.clientId === 'string' &&
@@ -439,7 +442,9 @@ export async function createSlackManagedUsersAttempt(params: {
organizationId?: string
userId: string
credentialGroupId: string
- slackBotCredentialId: string
+ slackBotCredentialId?: string
+ appId?: string
+ teamId?: string
clientId: string
clientSecret: string
requiredScopes?: string[]
@@ -466,17 +471,33 @@ export async function createSlackManagedUsersAttempt(params: {
existingOption?.requiredScopes ??
(existingOption ? undefined : SLACK_SEARCH_USER_SCOPES)
)
- const bot = await getSlackCustomBotCredential({
- ...resourceScopeFields(scope),
- credentialId: params.slackBotCredentialId,
- })
- if (!bot) throw new SlackManagedUsersError('Custom Slack bot not found.', 'invalid_response')
- const identity = await verifySlackCustomBotAppIdentity(bot.botToken)
- if (identity.teamId !== bot.teamId) {
- throw new SlackManagedUsersError(
- 'The custom bot token no longer belongs to its stored Slack workspace.',
- 'invalid_response'
+ let bot: Awaited> = null
+ let identity: { appId: string; teamId: string }
+ if (scope.kind === 'organization') {
+ if (
+ params.slackBotCredentialId ||
+ !params.appId?.match(/^A[A-Z0-9]+$/) ||
+ !params.teamId?.match(/^T[A-Z0-9]+$/)
)
+ throw new SlackManagedUsersError(
+ 'Organization Slack setup requires an App ID and workspace ID.',
+ 'invalid_response'
+ )
+ identity = { appId: params.appId, teamId: params.teamId }
+ } else {
+ if (!params.slackBotCredentialId)
+ throw new SlackManagedUsersError('Select a custom Slack bot.', 'invalid_response')
+ bot = await getSlackCustomBotCredential({
+ ...resourceScopeFields(scope),
+ credentialId: params.slackBotCredentialId,
+ })
+ if (!bot) throw new SlackManagedUsersError('Custom Slack bot not found.', 'invalid_response')
+ identity = await verifySlackCustomBotAppIdentity(bot.botToken)
+ if (identity.teamId !== bot.teamId)
+ throw new SlackManagedUsersError(
+ 'The bot no longer belongs to its stored Slack workspace.',
+ 'invalid_response'
+ )
}
const redis = requireRedis()
const state = generateId()
@@ -488,8 +509,9 @@ export async function createSlackManagedUsersAttempt(params: {
userId: params.userId,
credentialGroupId: group.id,
credentialGroupUpdatedAt: group.updatedAt.getTime(),
- slackBotCredentialId: bot.id,
- slackBotCredentialUpdatedAt: bot.updatedAt.getTime(),
+ ...(bot
+ ? { slackBotCredentialId: bot.id, slackBotCredentialUpdatedAt: bot.updatedAt.getTime() }
+ : {}),
expectedAppId: identity.appId,
expectedTeamId: identity.teamId,
clientId: params.clientId,
@@ -546,8 +568,12 @@ async function parseSlackManagedUsersAttempt(
userId: parsed.userId,
credentialGroupId: parsed.credentialGroupId,
credentialGroupUpdatedAt: parsed.credentialGroupUpdatedAt,
- slackBotCredentialId: parsed.slackBotCredentialId,
- slackBotCredentialUpdatedAt: parsed.slackBotCredentialUpdatedAt,
+ ...(parsed.workspaceId
+ ? {
+ slackBotCredentialId: parsed.slackBotCredentialId,
+ slackBotCredentialUpdatedAt: parsed.slackBotCredentialUpdatedAt,
+ }
+ : {}),
expectedAppId: parsed.expectedAppId,
expectedTeamId: parsed.expectedTeamId,
clientId: parsed.clientId,
@@ -564,7 +590,7 @@ export async function exchangeAndConfigureSlackManagedUsers(params: {
}): Promise<{
credentialGroupId: string
credentialGroupName: string
- slackBotCredentialId: string
+ slackBotCredentialId?: string
appId: string
teamId: string
requiredScopes: string[]
@@ -593,7 +619,7 @@ export async function exchangeAndConfigureSlackManagedUsers(params: {
grant.teamId !== params.attempt.expectedTeamId
) {
throw new SlackManagedUsersError(
- 'The Client ID and Client Secret belong to a different Slack app or workspace than the selected custom bot.',
+ 'Slack returned a different app or workspace than the configured App ID and workspace ID.',
'invalid_response'
)
}
@@ -636,58 +662,62 @@ export async function exchangeAndConfigureSlackManagedUsers(params: {
'invalid_state'
)
}
- const [botRow] = await tx
- .select({
- id: credential.id,
- updatedAt: credential.updatedAt,
- encryptedServiceAccountKey: credential.encryptedServiceAccountKey,
- })
- .from(credential)
- .where(
- and(
- eq(credential.id, params.attempt.slackBotCredentialId),
- resourceScopeCondition(credential, resourceScopeFromOwner(params.attempt)),
- eq(credential.type, 'service_account'),
- eq(credential.providerId, SLACK_CUSTOM_BOT_PROVIDER_ID)
+ if (params.attempt.workspaceId) {
+ if (!params.attempt.slackBotCredentialId)
+ throw new SlackManagedUsersError('Workspace Slack bot is missing.', 'invalid_state')
+ const [botRow] = await tx
+ .select({
+ id: credential.id,
+ updatedAt: credential.updatedAt,
+ encryptedServiceAccountKey: credential.encryptedServiceAccountKey,
+ })
+ .from(credential)
+ .where(
+ and(
+ eq(credential.id, params.attempt.slackBotCredentialId),
+ resourceScopeCondition(credential, resourceScopeFromOwner(params.attempt)),
+ eq(credential.type, 'service_account'),
+ eq(credential.providerId, SLACK_CUSTOM_BOT_PROVIDER_ID)
+ )
)
- )
- .limit(1)
- if (
- !botRow?.encryptedServiceAccountKey ||
- botRow.updatedAt.getTime() !== params.attempt.slackBotCredentialUpdatedAt
- ) {
- throw new SlackManagedUsersError(
- 'The custom bot changed while Slack authorization was in progress. Start again.',
- 'invalid_state'
- )
- }
- const decrypted = await decryptSecret(botRow.encryptedServiceAccountKey)
- const botSecret = parseSlackCustomBotSecret(JSON.parse(decrypted.decrypted) as unknown)
- if (botSecret.teamId !== grant.teamId) {
- throw new SlackManagedUsersError(
- 'The custom bot no longer belongs to the verified Slack workspace.',
- 'invalid_state'
- )
+ .limit(1)
+ if (
+ !botRow?.encryptedServiceAccountKey ||
+ botRow.updatedAt.getTime() !== params.attempt.slackBotCredentialUpdatedAt
+ ) {
+ throw new SlackManagedUsersError(
+ 'The custom bot changed while Slack authorization was in progress. Start again.',
+ 'invalid_state'
+ )
+ }
+ const decrypted = await decryptSecret(botRow.encryptedServiceAccountKey)
+ const botSecret = parseSlackCustomBotSecret(JSON.parse(decrypted.decrypted) as unknown)
+ if (botSecret.teamId !== grant.teamId) {
+ throw new SlackManagedUsersError(
+ 'The custom bot no longer belongs to the verified Slack workspace.',
+ 'invalid_state'
+ )
+ }
+ const sanitizedBotSecret = await encryptSecret(JSON.stringify(botSecret))
+ const [cleanedBot] = await tx
+ .update(credential)
+ .set({
+ encryptedServiceAccountKey: sanitizedBotSecret.encrypted,
+ authorizationAppId: null,
+ managedOauthScopeVersion: null,
+ updatedAt: now,
+ })
+ .where(eq(credential.id, botRow.id))
+ .returning({ id: credential.id })
+ if (!cleanedBot) throw new Error('Slack custom bot cleanup returned no row')
}
- const sanitizedBotSecret = await encryptSecret(JSON.stringify(botSecret))
- const [cleanedBot] = await tx
- .update(credential)
- .set({
- encryptedServiceAccountKey: sanitizedBotSecret.encrypted,
- authorizationAppId: null,
- managedOauthScopeVersion: null,
- updatedAt: now,
- })
- .where(eq(credential.id, botRow.id))
- .returning({ id: credential.id })
- if (!cleanedBot) throw new Error('Slack custom bot cleanup returned no row')
const currentConfiguration = await decryptCredentialGroupProviderConfiguration(
group.encryptedProviderConfiguration
)
const encryptedConfiguration = await encryptCredentialGroupProviderConfiguration({
...currentConfiguration,
slack: {
- slackBotCredentialId: botRow.id,
+ slackBotCredentialId: params.attempt.slackBotCredentialId,
clientId: params.attempt.clientId,
clientSecret: params.attempt.clientSecret,
appId: grant.appId,
@@ -701,7 +731,7 @@ export async function exchangeAndConfigureSlackManagedUsers(params: {
id: existingOption?.id ?? generateId(),
provider: 'slack',
label: existingOption?.label ?? 'Slack',
- slackBotCredentialId: botRow.id,
+ slackBotCredentialId: params.attempt.slackBotCredentialId,
authorizationAppId,
requiredScopes: params.attempt.requiredScopes,
scopeVersion,
@@ -744,7 +774,7 @@ export async function exchangeAndConfigureSlackManagedUsers(params: {
return {
credentialGroupId: group.id,
credentialGroupName: group.name,
- slackBotCredentialId: botRow.id,
+ slackBotCredentialId: params.attempt.slackBotCredentialId,
appId: grant.appId,
teamId: grant.teamId,
requiredScopes: params.attempt.requiredScopes,
diff --git a/apps/sim/lib/credential-groups/slack-provider.ts b/apps/sim/lib/credential-groups/slack-provider.ts
index 97b3419feb8..0860ff43313 100644
--- a/apps/sim/lib/credential-groups/slack-provider.ts
+++ b/apps/sim/lib/credential-groups/slack-provider.ts
@@ -40,7 +40,7 @@ async function getSlackPolicy(params: {
executor?: DbOrTx
}): Promise<
CredentialGroupProviderPolicy & {
- slackBotCredentialId: string
+ slackBotCredentialId?: string
clientId: string
clientSecret: string
appId: string
@@ -60,19 +60,27 @@ async function getSlackPolicy(params: {
'The selected custom Slack bot does not match this Credential Group configuration'
)
}
- const app = await getSlackCustomBotCredential({
- ...resourceScopeColumns(resourceScopeFromOwner(params)),
- credentialId: managed.slackBotCredentialId,
- ...(params.executor ? { executor: params.executor } : {}),
- })
- if (!app) {
- throw new CredentialGroupProviderConfigurationError(
- 'The selected custom Slack bot is unavailable'
- )
- }
- if (app.teamId !== managed.teamId) {
+ if (params.workspaceId) {
+ if (!managed.slackBotCredentialId)
+ throw new CredentialGroupProviderConfigurationError('Configure the workspace Slack bot')
+ const app = await getSlackCustomBotCredential({
+ ...resourceScopeColumns(resourceScopeFromOwner(params)),
+ credentialId: managed.slackBotCredentialId,
+ ...(params.executor ? { executor: params.executor } : {}),
+ })
+ if (!app) {
+ throw new CredentialGroupProviderConfigurationError(
+ 'The selected custom Slack bot is unavailable'
+ )
+ }
+ if (app.teamId !== managed.teamId) {
+ throw new CredentialGroupProviderConfigurationError(
+ 'The custom Slack bot no longer belongs to the configured Slack workspace'
+ )
+ }
+ } else if (managed.slackBotCredentialId) {
throw new CredentialGroupProviderConfigurationError(
- 'The custom Slack bot no longer belongs to the configured Slack workspace'
+ 'Reconfigure Slack as an organization personal OAuth app'
)
}
const service = getCredentialGroupProviderService(PROVIDER)
diff --git a/apps/sim/lib/credential-groups/trigger-subscriptions.ts b/apps/sim/lib/credential-groups/trigger-subscriptions.ts
index 9c27581f6ac..16b208ced3b 100644
--- a/apps/sim/lib/credential-groups/trigger-subscriptions.ts
+++ b/apps/sim/lib/credential-groups/trigger-subscriptions.ts
@@ -1,6 +1,6 @@
import { db } from '@sim/db'
-import { webhook, workflow, workflowDeploymentVersion } from '@sim/db/schema'
-import { and, eq, inArray, isNull, or } from 'drizzle-orm'
+import { webhook, workflow, workflowDeploymentVersion, workspace } from '@sim/db/schema'
+import { and, eq, inArray, isNull, sql } from 'drizzle-orm'
import { CREDENTIAL_GROUP_TRIGGER_PROVIDER } from '@/lib/credential-groups/trigger-constants'
import { deliverableWebhookPredicate } from '@/lib/webhooks/delivery-predicate'
import type { WebhookRecord, WorkflowRecord } from '@/lib/webhooks/polling/types'
@@ -10,17 +10,18 @@ export interface CredentialGroupTriggerSubscription {
workflow: WorkflowRecord
}
-/** Loads only deployed subscriptions in the source workspace that may read this group. */
+/** Loads opted-in Credential triggers deployed in currently allowed organization workspaces. */
export async function fetchCredentialGroupTriggerSubscriptions(
- workspaceId: string,
- allowedWorkflowIds: string[]
+ organizationId: string,
+ allowedWorkspaceIds: string[]
): Promise {
- if (allowedWorkflowIds.length === 0) return []
- return db
+ if (allowedWorkspaceIds.length === 0) return []
+ const subscriptions = await db
.select({ webhook, workflow })
.from(webhook)
.innerJoin(workflow, eq(webhook.workflowId, workflow.id))
- .leftJoin(
+ .innerJoin(workspace, eq(workspace.id, workflow.workspaceId))
+ .innerJoin(
workflowDeploymentVersion,
and(
eq(workflowDeploymentVersion.workflowId, workflow.id),
@@ -31,14 +32,17 @@ export async function fetchCredentialGroupTriggerSubscriptions(
and(
eq(webhook.provider, CREDENTIAL_GROUP_TRIGGER_PROVIDER),
deliverableWebhookPredicate(webhook),
- eq(workflow.workspaceId, workspaceId),
- inArray(workflow.id, allowedWorkflowIds),
+ eq(workspace.organizationId, organizationId),
+ isNull(workspace.archivedAt),
+ inArray(workflow.workspaceId, allowedWorkspaceIds),
eq(workflow.isDeployed, true),
isNull(workflow.archivedAt),
- or(
- eq(webhook.deploymentVersionId, workflowDeploymentVersion.id),
- and(isNull(workflowDeploymentVersion.id), isNull(webhook.deploymentVersionId))
- )
+ eq(webhook.deploymentVersionId, workflowDeploymentVersion.id),
+ sql`${workflowDeploymentVersion.state}::jsonb -> 'blocks' -> ${webhook.blockId} ->> 'type' = 'credential'`
)
)
+ .limit(1001)
+ if (subscriptions.length > 1000)
+ throw new Error('Organization connected account events exceed the 1000 subscriber limit')
+ return subscriptions
}
diff --git a/apps/sim/lib/credential-groups/trigger.test.ts b/apps/sim/lib/credential-groups/trigger.test.ts
index 97e74af2ad6..1f8d1ac446d 100644
--- a/apps/sim/lib/credential-groups/trigger.test.ts
+++ b/apps/sim/lib/credential-groups/trigger.test.ts
@@ -1,21 +1,22 @@
/**
* @vitest-environment node
*/
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
+import { OrchestrationError } from '@/lib/core/orchestration/types'
+import { buildOrganizationAccountAccessPolicy } from '@/lib/credential-groups/application/workspace-access-policy'
const mocks = vi.hoisted(() => ({
- decodePolicy: vi.fn(),
+ resolveWorkspace: vi.fn(),
+ requireAccess: vi.fn(),
fetchSubscriptions: vi.fn(),
processEvent: vi.fn(),
requirePolicy: vi.fn(),
}))
-vi.mock('@/lib/credential-groups/application/workflow-access-policy', () => ({
- credentialGroupWorkflowAccessPolicyCodec: {
- resourceType: 'credential_group',
- parse: (value: unknown) => value,
- },
- decodeCredentialGroupWorkflowAccessPolicy: mocks.decodePolicy,
+vi.mock('@/lib/credential-groups/application/organization-workspace-access', () => ({
+ resolveOrganizationAccountsWorkspaceContext: mocks.resolveWorkspace,
+ requireOrganizationAccountsWorkspaceAccess: mocks.requireAccess,
}))
vi.mock('@/lib/resource-policies/repository', () => ({
@@ -37,7 +38,7 @@ import {
const EVENT = {
event: 'credential_added' as const,
- workspaceId: 'workspace-1',
+ organizationId: 'org-1',
credentialGroupId: 'group-1',
credentialGroupName: 'Credential Group',
enrollmentId: 'enrollment-1',
@@ -71,12 +72,19 @@ function subscription(params: { workflowId: string; workspaceId?: string; eventT
describe('Credential Group trigger delivery', () => {
beforeEach(() => {
vi.clearAllMocks()
- mocks.requirePolicy.mockResolvedValue({ document: {} })
- mocks.decodePolicy.mockReturnValue(['workflow-allowed'])
+ mocks.requirePolicy.mockResolvedValue({
+ document: buildOrganizationAccountAccessPolicy('group-1', ['workspace-1', 'workspace-2']),
+ })
+ mocks.resolveWorkspace.mockImplementation(async (workspaceId: string) => ({
+ workspaceId,
+ credentialGroupId: 'group-1',
+ status: 'active',
+ }))
+ mocks.requireAccess.mockResolvedValue(undefined)
mocks.processEvent.mockResolvedValue({ success: true })
})
- it('delivers only to an allowed workflow in the source workspace watching the event', async () => {
+ it('delivers to matching subscribers across allowed workspaces without workflow grants', async () => {
const allowed = subscription({ workflowId: 'workflow-allowed' })
mocks.fetchSubscriptions.mockResolvedValue([
allowed,
@@ -87,7 +95,7 @@ describe('Credential Group trigger delivery', () => {
await fireCredentialGroupTrigger(EVENT)
- expect(mocks.processEvent).toHaveBeenCalledOnce()
+ expect(mocks.processEvent).toHaveBeenCalledTimes(3)
expect(mocks.processEvent).toHaveBeenCalledWith(
allowed.webhook,
allowed.workflow,
@@ -100,8 +108,10 @@ describe('Credential Group trigger delivery', () => {
)
})
- it('does not scan subscriptions when no workflow has group access', async () => {
- mocks.decodePolicy.mockReturnValue([])
+ it('does not scan subscriptions when no workspace has access', async () => {
+ mocks.requirePolicy.mockResolvedValue({
+ document: buildOrganizationAccountAccessPolicy('group-1', []),
+ })
await fireCredentialGroupTrigger(EVENT)
@@ -109,6 +119,30 @@ describe('Credential Group trigger delivery', () => {
expect(mocks.processEvent).not.toHaveBeenCalled()
})
+ it('skips a workspace revoked during fanout and continues to other subscribers', async () => {
+ mocks.fetchSubscriptions.mockResolvedValue([
+ subscription({ workflowId: 'revoked' }),
+ subscription({ workflowId: 'allowed', workspaceId: 'workspace-2' }),
+ ])
+ mocks.requireAccess.mockRejectedValueOnce(new OrchestrationError('forbidden', 'Revoked'))
+ await fireCredentialGroupTrigger(EVENT)
+ expect(mocks.processEvent).toHaveBeenCalledOnce()
+ expect(mocks.processEvent).toHaveBeenCalledWith(
+ expect.any(Object),
+ expect.objectContaining({ id: 'allowed' }),
+ expect.any(Object),
+ expect.any(String)
+ )
+ })
+
+ it('propagates malformed policy and delivery failures', async () => {
+ mocks.requirePolicy.mockRejectedValueOnce(new Error('Malformed policy'))
+ await expect(fireCredentialGroupTrigger(EVENT)).rejects.toThrow('Malformed policy')
+ mocks.fetchSubscriptions.mockResolvedValue([subscription({ workflowId: 'allowed' })])
+ mocks.processEvent.mockResolvedValue({ success: false, statusCode: 500, error: 'Failed' })
+ await expect(fireCredentialGroupTrigger(EVENT)).rejects.toThrow('Failed to deliver')
+ })
+
it('uses null credential fields for form submissions', () => {
expect(
buildCredentialGroupTriggerPayload({
diff --git a/apps/sim/lib/credential-groups/trigger.ts b/apps/sim/lib/credential-groups/trigger.ts
index dd3b15fc14a..c9fa7e964a6 100644
--- a/apps/sim/lib/credential-groups/trigger.ts
+++ b/apps/sim/lib/credential-groups/trigger.ts
@@ -1,10 +1,15 @@
-import { createLogger } from '@sim/logger'
import { generateShortId } from '@sim/utils/id'
import { isRecordLike } from '@sim/utils/object'
+import { OrchestrationError } from '@/lib/core/orchestration/types'
import {
- credentialGroupWorkflowAccessPolicyCodec,
- decodeCredentialGroupWorkflowAccessPolicy,
-} from '@/lib/credential-groups/application/workflow-access-policy'
+ requireOrganizationAccountsWorkspaceAccess,
+ resolveOrganizationAccountsWorkspaceContext,
+} from '@/lib/credential-groups/application/organization-workspace-access'
+import {
+ listOrganizationAccountWorkspaceIds,
+ organizationAccountAccessPolicyCodec,
+} from '@/lib/credential-groups/application/workspace-access-policy'
+import type { ManagedMcpConnectorId } from '@/lib/credential-groups/managed-mcp-connectors'
import type { CredentialGroupProvider } from '@/lib/credential-groups/providers'
import {
CREDENTIAL_GROUP_EVENT_TRIGGER_ID,
@@ -14,10 +19,9 @@ import {
import { fetchCredentialGroupTriggerSubscriptions } from '@/lib/credential-groups/trigger-subscriptions'
import { requireResourcePolicy } from '@/lib/resource-policies/repository'
-const logger = createLogger('CredentialGroupTrigger')
-
interface CredentialGroupTriggerEventBase {
- workspaceId: string
+ workspaceId?: string
+ organizationId?: string
credentialGroupId: string
credentialGroupName: string
enrollmentId: string
@@ -27,8 +31,9 @@ interface CredentialGroupTriggerEventBase {
interface CredentialGroupTriggerCredential {
credentialId: string
- credentialGroupOptionId: string
- provider: CredentialGroupProvider
+ credentialGroupOptionId: string | null
+ mcpServerId?: string
+ provider: CredentialGroupProvider | ManagedMcpConnectorId
providerId: string
displayName: string
}
@@ -53,7 +58,8 @@ export interface CredentialGroupTriggerPayload {
enrollmentStatus: 'in_progress' | 'completed'
credentialId: string | null
credentialGroupOptionId: string | null
- provider: CredentialGroupProvider | null
+ mcpServerId: string | null
+ provider: CredentialGroupProvider | ManagedMcpConnectorId | null
providerId: string | null
displayName: string | null
}
@@ -94,6 +100,7 @@ export function buildCredentialGroupTriggerPayload(
enrollmentStatus: event.enrollmentStatus,
credentialId: credential?.credentialId ?? null,
credentialGroupOptionId: credential?.credentialGroupOptionId ?? null,
+ mcpServerId: credential?.mcpServerId ?? null,
provider: credential?.provider ?? null,
providerId: credential?.providerId ?? null,
displayName: credential?.displayName ?? null,
@@ -102,55 +109,54 @@ export function buildCredentialGroupTriggerPayload(
/**
* Fires deployed Credential Group triggers after the source mutation commits.
- * Delivery is restricted to workflows explicitly allowed by the group's resource policy.
+ * Delivery requires an opted-in deployed Credential trigger and current organization workspace access.
*/
export async function fireCredentialGroupTrigger(
event: CredentialGroupTriggerEvent
): Promise {
- try {
- const policy = await requireResourcePolicy({
- workspaceId: event.workspaceId,
- resourceType: 'credential_group',
- resourceId: event.credentialGroupId,
- codec: credentialGroupWorkflowAccessPolicyCodec,
- })
- const allowedWorkflowIds = new Set(
- decodeCredentialGroupWorkflowAccessPolicy(policy.document, event.credentialGroupId)
- )
- if (allowedWorkflowIds.size === 0) return
+ if (!event.organizationId) return
+ const policy = await requireResourcePolicy({
+ organizationId: event.organizationId,
+ resourceType: 'credential_group',
+ resourceId: event.credentialGroupId,
+ codec: organizationAccountAccessPolicyCodec,
+ })
+ const allowedWorkspaceIds = listOrganizationAccountWorkspaceIds(policy.document)
+ if (allowedWorkspaceIds.length === 0) return
+ const subscriptions = await fetchCredentialGroupTriggerSubscriptions(
+ event.organizationId,
+ allowedWorkspaceIds
+ )
+ const matchingSubscriptions = subscriptions.filter(({ webhook }) => {
+ const config = parseCredentialGroupTriggerConfig(webhook.providerConfig)
+ return config.eventType === event.event
+ })
+ if (matchingSubscriptions.length === 0) return
- const subscriptions = await fetchCredentialGroupTriggerSubscriptions(event.workspaceId, [
- ...allowedWorkflowIds,
- ])
- const matchingSubscriptions = subscriptions.filter(({ webhook, workflow }) => {
- if (workflow.workspaceId !== event.workspaceId) return false
- if (!allowedWorkflowIds.has(workflow.id)) return false
- const config = parseCredentialGroupTriggerConfig(webhook.providerConfig)
- return config.eventType === event.event
- })
- if (matchingSubscriptions.length === 0) return
-
- const payload = buildCredentialGroupTriggerPayload(event)
- const { processPolledWebhookEvent } = await import('@/lib/webhooks/processor')
- for (const { webhook, workflow } of matchingSubscriptions) {
- const requestId = generateShortId()
- const result = await processPolledWebhookEvent(webhook, workflow, payload, requestId)
- if (!result.success) {
- logger.error(`[${requestId}] Failed to fire Credential Group trigger`, {
- event: event.event,
- credentialGroupId: event.credentialGroupId,
- subscriberWorkflowId: workflow.id,
- statusCode: result.statusCode,
- error: result.error,
- })
- }
+ const payload = buildCredentialGroupTriggerPayload(event)
+ const { processPolledWebhookEvent } = await import('@/lib/webhooks/processor')
+ for (const { webhook, workflow } of matchingSubscriptions) {
+ if (!workflow.workspaceId) throw new Error('Subscribed workflow is missing its workspace')
+ try {
+ const context = await resolveOrganizationAccountsWorkspaceContext(workflow.workspaceId)
+ if (context.credentialGroupId !== event.credentialGroupId || context.status !== 'active')
+ continue
+ await requireOrganizationAccountsWorkspaceAccess(context)
+ } catch (error) {
+ /** Revocations and workspace moves remove subscribers between discovery and delivery. */
+ if (
+ error instanceof OrchestrationError &&
+ (error.code === 'forbidden' || error.code === 'not_found')
+ )
+ continue
+ throw error
+ }
+ const requestId = generateShortId()
+ const result = await processPolledWebhookEvent(webhook, workflow, payload, requestId)
+ if (!result.success) {
+ throw new Error(
+ `Failed to deliver connected account event to workflow ${workflow.id}: ${result.error ?? result.statusCode}`
+ )
}
- } catch (error) {
- logger.error('Failed to emit Credential Group event', {
- error,
- event: event.event,
- credentialGroupId: event.credentialGroupId,
- enrollmentId: event.enrollmentId,
- })
}
}
diff --git a/apps/sim/lib/credential-groups/types.ts b/apps/sim/lib/credential-groups/types.ts
index 444fe6d48bf..accddd6511e 100644
--- a/apps/sim/lib/credential-groups/types.ts
+++ b/apps/sim/lib/credential-groups/types.ts
@@ -12,7 +12,7 @@ export type CredentialGroupOptionInput =
})
| (CredentialGroupOptionInputBase & {
provider: 'slack'
- slackBotCredentialId: string
+ slackBotCredentialId?: string
})
export type CredentialGroupOptionUpdateInput = CredentialGroupOptionInput & { id?: string }
@@ -45,7 +45,7 @@ export type CredentialGroupOption =
})
| (CredentialGroupOptionBase & {
provider: 'slack'
- slackBotCredentialId: string
+ slackBotCredentialId?: string
configurationStatus: 'not_configured' | 'ready' | 'needs_update'
})
diff --git a/apps/sim/lib/credential-groups/workspace-accounts.ts b/apps/sim/lib/credential-groups/workspace-accounts.ts
index 4816760b15b..a17968a9c56 100644
--- a/apps/sim/lib/credential-groups/workspace-accounts.ts
+++ b/apps/sim/lib/credential-groups/workspace-accounts.ts
@@ -1,9 +1,10 @@
-import { type CredentialGroupOptionConfig, credentialGroup } from '@sim/db/schema'
+import { type CredentialGroupOptionConfig, credentialGroup, resourcePolicy } from '@sim/db/schema'
import { generateId } from '@sim/utils/id'
import {
credentialGroupWorkflowAccessPolicyCodec,
requireDefaultCredentialGroupWorkflowAccessPolicy,
} from '@/lib/credential-groups/application/workflow-access-policy'
+import { buildOrganizationAccountAccessPolicy } from '@/lib/credential-groups/application/workspace-access-policy'
import type { DbOrTx } from '@/lib/db/types'
import { requireResourcePolicy } from '@/lib/resource-policies/repository'
@@ -58,11 +59,20 @@ export async function createOrganizationAccountsGroup(
organizationId,
publicId: generateId(),
name: 'Connected accounts',
- description: 'Accounts connected for organization search.',
+ description: 'Accounts shared with workflows in approved workspaces.',
options,
createdBy: userId,
})
.returning()
if (!created) throw new Error('Connected accounts insert returned no row')
+ await executor.insert(resourcePolicy).values({
+ id: generateId(),
+ organizationId,
+ resourceType: 'credential_group',
+ resourceId: created.id,
+ document: buildOrganizationAccountAccessPolicy(created.id, []),
+ createdBy: userId,
+ updatedBy: userId,
+ })
return created
}
diff --git a/apps/sim/lib/credentials/application/discover-managed-mcp-tools.test.ts b/apps/sim/lib/credentials/application/discover-managed-mcp-tools.test.ts
index edec8f3d8a9..cd7349bddd1 100644
--- a/apps/sim/lib/credentials/application/discover-managed-mcp-tools.test.ts
+++ b/apps/sim/lib/credentials/application/discover-managed-mcp-tools.test.ts
@@ -84,6 +84,9 @@ describe('discoverManagedMcpToolsUseCase', () => {
mcpServerId: context.mcpServerId,
mcpServerName: context.mcpServerName,
workspaceId: context.workspaceId,
+ scope: { kind: 'organization', organizationId: 'org-1' },
+ oauthConfigVersion: 2,
+ grantedAt: new Date('2026-09-01'),
tokenVersion: 'encrypted-token-version-1',
tokens: { access_token: 'access-token' },
tools: [],
@@ -120,7 +123,7 @@ describe('discoverManagedMcpToolsUseCase', () => {
expect(mocks.loadRuntime).toHaveBeenCalledWith(context.credentialId, context.workspaceId)
expect(mocks.discoverTools).toHaveBeenCalledWith(
context.mcpServerId,
- context.workspaceId,
+ { kind: 'organization', organizationId: 'org-1' },
{ credentialId: context.credentialId, loadProvider: expect.any(Function) },
signal,
{ requireComplete: true }
@@ -132,12 +135,17 @@ describe('discoverManagedMcpToolsUseCase', () => {
serverName: context.mcpServerName,
}),
])
- expect(mocks.saveToolSnapshot).toHaveBeenCalledWith(context.credentialId, [
- {
- name: 'search_transcripts',
- description: 'Search transcripts',
- inputSchema: { type: 'object', properties: {} },
- },
- ])
+ expect(mocks.saveToolSnapshot).toHaveBeenCalledWith(
+ context.credentialId,
+ [
+ {
+ name: 'search_transcripts',
+ description: 'Search transcripts',
+ inputSchema: { type: 'object', properties: {} },
+ },
+ ],
+ 2,
+ new Date('2026-09-01')
+ )
})
})
diff --git a/apps/sim/lib/credentials/application/discover-managed-mcp-tools.ts b/apps/sim/lib/credentials/application/discover-managed-mcp-tools.ts
index 67ac74c7c04..5e0c81eec3e 100644
--- a/apps/sim/lib/credentials/application/discover-managed-mcp-tools.ts
+++ b/apps/sim/lib/credentials/application/discover-managed-mcp-tools.ts
@@ -21,7 +21,10 @@ export interface DiscoverManagedMcpToolsInput {
export const discoverManagedMcpToolsUseCase = defineAuthorizedWorkspaceUseCase({
operation: credentialOperations.useManagedMcp,
resolveContext: async ({ input }: { input: DiscoverManagedMcpToolsInput }) => {
- const context = await loadManagedMcpCredentialApplicationContext(input.credentialId)
+ const context = await loadManagedMcpCredentialApplicationContext(
+ input.credentialId,
+ input.workspaceId
+ )
if (!context || context.workspaceId !== input.workspaceId) {
throw new OrchestrationError('not_found', 'Managed MCP connection not found')
}
@@ -36,7 +39,7 @@ export const discoverManagedMcpToolsUseCase = defineAuthorizedWorkspaceUseCase({
const runtime = await loadManagedMcpRuntimeCredential(context.credentialId, context.workspaceId)
const tools = await mcpService.discoverManagedMcpTools(
runtime.mcpServerId,
- runtime.workspaceId,
+ runtime.scope,
{
credentialId: runtime.credentialId,
loadProvider: () => loadManagedMcpAuthProvider(runtime.credentialId, runtime.workspaceId),
@@ -50,7 +53,9 @@ export const discoverManagedMcpToolsUseCase = defineAuthorizedWorkspaceUseCase({
name: tool.name,
...(tool.description ? { description: tool.description } : {}),
inputSchema: tool.inputSchema,
- }))
+ })),
+ runtime.oauthConfigVersion,
+ runtime.grantedAt
)
return {
tools: tools.map((tool) => ({
diff --git a/apps/sim/lib/credentials/application/resolve-managed-oauth-token.ts b/apps/sim/lib/credentials/application/resolve-managed-oauth-token.ts
index 418a0e455b5..f614c563a9d 100644
--- a/apps/sim/lib/credentials/application/resolve-managed-oauth-token.ts
+++ b/apps/sim/lib/credentials/application/resolve-managed-oauth-token.ts
@@ -1,4 +1,5 @@
import { AuditAction, AuditResourceType } from '@sim/audit'
+import type { Principal } from '@sim/auth/principal'
import { defineAuthorizedWorkspaceUseCase } from '@/lib/core/application'
import { OrchestrationError } from '@/lib/core/orchestration/types'
import { requireCredentialGroupCredentialAccess } from '@/lib/credential-groups/application/authorization'
@@ -19,8 +20,19 @@ export interface ResolveManagedOAuthTokenInput {
export const resolveManagedOAuthCredentialToken = defineAuthorizedWorkspaceUseCase({
operation: credentialOperations.useManagedOAuth,
- resolveContext: async ({ input }: { input: ResolveManagedOAuthTokenInput }) => {
- const context = await loadManagedOAuthCredentialApplicationContext(input.credentialId)
+ resolveContext: async ({
+ input,
+ principal,
+ }: {
+ input: ResolveManagedOAuthTokenInput
+ principal: Principal
+ }) => {
+ if (principal.kind !== 'delegated')
+ throw new OrchestrationError('forbidden', 'Managed credentials require delegated execution')
+ const context = await loadManagedOAuthCredentialApplicationContext(
+ input.credentialId,
+ principal.workspaceId
+ )
if (!context) throw new OrchestrationError('not_found', 'Managed credential not found')
return context
},
@@ -31,7 +43,9 @@ export const resolveManagedOAuthCredentialToken = defineAuthorizedWorkspaceUseCa
execute: async ({ input, context }): Promise =>
resolveManagedOAuthToken({
credentialId: context.credentialId,
- workspaceId: context.workspaceId,
+ ...(context.organizationId
+ ? { organizationId: context.organizationId }
+ : { workspaceId: context.workspaceId }),
expectedProviderId: input.expectedProviderId,
requiredScopes: input.requiredScopes,
}),
diff --git a/apps/sim/lib/credentials/application/resolve-workflow-credentials.ts b/apps/sim/lib/credentials/application/resolve-workflow-credentials.ts
new file mode 100644
index 00000000000..d771a44b67f
--- /dev/null
+++ b/apps/sim/lib/credentials/application/resolve-workflow-credentials.ts
@@ -0,0 +1,65 @@
+import { db } from '@sim/db'
+import { credential } from '@sim/db/schema'
+import { and, asc, eq, inArray } from 'drizzle-orm'
+import { defineAuthorizedWorkspaceUseCase, defineWorkspaceOperation } from '@/lib/core/application'
+import { OrchestrationError } from '@/lib/core/orchestration/types'
+import {
+ CREDENTIAL_GROUP_DELEGATION_AUDIENCE,
+ requireCredentialGroupWorkflowActor,
+} from '@/lib/credential-groups/application/authorization'
+import { resolveCredentialGroupWorkspaceContext } from '@/lib/credential-groups/application/context'
+
+interface ResolveWorkflowCredentialsInput {
+ workspaceId: string
+ credentialId?: string
+ providerIds?: string[]
+}
+
+/** Resolves workspace credential references under the current workflow's authorization. */
+export const resolveWorkflowCredentials = defineAuthorizedWorkspaceUseCase({
+ /**
+ * permission-group-exempt: Reference selection preserves workspace workflow access; token use enforces credential permissions.
+ */
+ operation: defineWorkspaceOperation({
+ id: 'credentials.workflow_references.resolve',
+ minimumRole: 'read',
+ workspaceApiKey: 'deny',
+ capability: 'none',
+ principalKinds: ['delegated'],
+ delegatedServices: ['executor'],
+ }),
+ resolveContext: ({ input }: { input: ResolveWorkflowCredentialsInput }) =>
+ resolveCredentialGroupWorkspaceContext(input.workspaceId),
+ authorizationOptions: {
+ delegation: {
+ audience: CREDENTIAL_GROUP_DELEGATION_AUDIENCE,
+ isWithinScope: (principal) => principal.resourceScope === undefined,
+ },
+ },
+ authorizeResource: ({ principal }) => {
+ requireCredentialGroupWorkflowActor(principal)
+ },
+ execute: async ({ input, context }) => {
+ const records = await db.query.credential.findMany({
+ where: and(
+ eq(credential.workspaceId, context.workspaceId),
+ eq(credential.type, 'oauth'),
+ input.credentialId ? eq(credential.id, input.credentialId) : undefined,
+ input.providerIds?.length ? inArray(credential.providerId, input.providerIds) : undefined
+ ),
+ columns: { id: true, displayName: true, providerId: true },
+ orderBy: [asc(credential.displayName)],
+ })
+ if (input.credentialId && records.length !== 1)
+ throw new OrchestrationError('not_found', 'Credential not found in this workspace')
+ return records.map((record) => {
+ if (!record.providerId)
+ throw new Error(`OAuth credential ${record.id} is missing its provider`)
+ return {
+ credentialId: record.id,
+ displayName: record.displayName,
+ providerId: record.providerId,
+ }
+ })
+ },
+})
diff --git a/apps/sim/lib/credentials/environment.ts b/apps/sim/lib/credentials/environment.ts
index fb0ba4e1a0b..4d77e3dcb01 100644
--- a/apps/sim/lib/credentials/environment.ts
+++ b/apps/sim/lib/credentials/environment.ts
@@ -4,7 +4,6 @@ import {
credentialGroup,
credentialGroupEnrollment,
credentialMember,
- foldedEmail,
permissions,
user,
workspace,
@@ -876,11 +875,21 @@ export async function getEnrolledManagedOAuthCredentials(
eq(credentialGroupEnrollment.id, credential.credentialGroupEnrollmentId)
)
.innerJoin(credentialGroup, eq(credentialGroup.id, credentialGroupEnrollment.credentialGroupId))
- .innerJoin(user, eq(foldedEmail(user.email), credentialGroupEnrollment.email))
+ .innerJoin(user, eq(user.id, credentialGroupEnrollment.userId))
+ .innerJoin(workspace, eq(workspace.id, workspaceId))
.where(
and(
- eq(credential.workspaceId, workspaceId),
- eq(credentialGroup.workspaceId, workspaceId),
+ or(
+ and(
+ eq(credential.workspaceId, workspaceId),
+ eq(credentialGroup.workspaceId, workspaceId)
+ ),
+ and(
+ eq(credential.organizationId, workspace.organizationId),
+ eq(credentialGroup.organizationId, workspace.organizationId)
+ )
+ ),
+ eq(credential.createdBy, userId),
eq(credential.type, 'managed_oauth'),
credentialId === undefined ? undefined : eq(credential.id, credentialId),
eq(user.id, userId),
diff --git a/apps/sim/lib/credentials/managed-mcp.test.ts b/apps/sim/lib/credentials/managed-mcp.test.ts
index e2fcec8e4e3..ee285d9e2b7 100644
--- a/apps/sim/lib/credentials/managed-mcp.test.ts
+++ b/apps/sim/lib/credentials/managed-mcp.test.ts
@@ -21,10 +21,17 @@ vi.mock('@/lib/workspaces/application/workspace-context', () => ({
loadActiveWorkspaceApplicationContext: vi.fn(),
}))
-import { persistManagedMcpCredential } from '@/lib/credentials/managed-mcp'
+import {
+ persistManagedMcpCredential,
+ saveManagedMcpRuntimeTokens,
+ saveManagedMcpToolSnapshot,
+} from '@/lib/credentials/managed-mcp'
const input = {
- workspaceId: 'workspace-1',
+ organizationId: 'org-1',
+ userId: 'person-1',
+ oauthConfigVersion: 2,
+ invitationTokenHash: 'current-invitation-hash',
credentialGroupId: 'group-1',
enrollmentId: 'enrollment-1',
email: 'person@example.com',
@@ -70,4 +77,49 @@ describe('managed MCP grant persistence', () => {
expect(dbChainMockFns.update).not.toHaveBeenCalled()
}
)
+ it('stores an organization grant bound to the current enrollment user and MCP setup', async () => {
+ queueTableRows(schemaMock.credentialGroupEnrollment, [source])
+ queueTableRows(schemaMock.credential, [])
+ dbChainMockFns.returning
+ .mockResolvedValueOnce([{ id: 'mcp-cg-person' }])
+ .mockResolvedValueOnce([{ id: 'enrollment-1' }])
+ await expect(persistManagedMcpCredential(input)).resolves.toMatchObject({
+ created: true,
+ enrollmentStatus: 'in_progress',
+ connectionId: expect.stringMatching(/^mcp-cg-/),
+ })
+ expect(eq).toHaveBeenCalledWith(schemaMock.credentialGroupEnrollment.userId, 'person-1')
+ expect(eq).toHaveBeenCalledWith(
+ schemaMock.credentialGroupEnrollment.invitationTokenHash,
+ input.invitationTokenHash
+ )
+ expect(eq).toHaveBeenCalledWith(schemaMock.mcpServers.oauthConfigVersion, 2)
+ expect(dbChainMockFns.values).toHaveBeenCalledWith(
+ expect.objectContaining({
+ organizationId: 'org-1',
+ workspaceId: null,
+ mcpOauthConfigVersion: 2,
+ })
+ )
+ })
+
+ it('compares refresh writes with the encrypted token version', async () => {
+ queueTableRows(schemaMock.credential, [{ enrollmentId: 'enrollment-1' }])
+ dbChainMockFns.returning.mockResolvedValue([{ id: 'mcp-cg-person' }])
+ await saveManagedMcpRuntimeTokens('mcp-cg-person', input.tokens, 'previous-encrypted-token')
+ expect(eq).toHaveBeenCalledWith(
+ schemaMock.credential.encryptedOauthTokenSet,
+ 'previous-encrypted-token'
+ )
+ })
+
+ it('refuses a tool snapshot captured before reconnect or configuration replacement', async () => {
+ const grantedAt = new Date('2026-09-01')
+ dbChainMockFns.returning.mockResolvedValue([])
+ await expect(saveManagedMcpToolSnapshot('mcp-cg-person', [], 2, grantedAt)).rejects.toThrow(
+ 'grant changed'
+ )
+ expect(eq).toHaveBeenCalledWith(schemaMock.credential.mcpOauthConfigVersion, 2)
+ expect(eq).toHaveBeenCalledWith(schemaMock.credential.grantedAt, grantedAt)
+ })
})
diff --git a/apps/sim/lib/credentials/managed-mcp.ts b/apps/sim/lib/credentials/managed-mcp.ts
index 8b723df0217..df621ae31c7 100644
--- a/apps/sim/lib/credentials/managed-mcp.ts
+++ b/apps/sim/lib/credentials/managed-mcp.ts
@@ -9,12 +9,20 @@ import {
} from '@sim/db/schema'
import { getErrorMessage } from '@sim/utils/errors'
import { and, eq, isNull, ne } from 'drizzle-orm'
-import { getWorkspaceOwnerSubscriptionAccess } from '@/lib/billing/core/workspace-access'
import type { WorkspaceAuthorizationContext } from '@/lib/core/application'
+import {
+ type ResourceScope,
+ resourceScopeColumns,
+ resourceScopeFromOwner,
+} from '@/lib/core/resource-scope'
+import {
+ resourceScopeCondition,
+ sameResourceScopeCondition,
+} from '@/lib/core/resource-scope.server'
import { decryptSecret, encryptSecret } from '@/lib/core/security/encryption'
-import { isCredentialGroupsAvailable } from '@/lib/credential-groups/availability'
import { lockCredentialGroupEnrollmentLifecycle } from '@/lib/credential-groups/enrollments'
import { getManagedMcpConnector } from '@/lib/credential-groups/managed-mcp-connectors'
+import { isScopedCredentialGroupsAvailable } from '@/lib/credential-groups/scoped-availability'
import { generateManagedMcpConnectionId } from '@/lib/mcp/utils'
import { loadActiveWorkspaceApplicationContext } from '@/lib/workspaces/application/workspace-context'
@@ -28,6 +36,7 @@ interface ManagedMcpTokenEnvelope {
}
export interface ManagedMcpCredentialApplicationContext extends WorkspaceAuthorizationContext {
+ organizationId?: string
credentialId: string
credentialGroupId: string
credentialGroupEnrollmentId: string
@@ -36,6 +45,10 @@ export interface ManagedMcpCredentialApplicationContext extends WorkspaceAuthori
}
export interface ManagedMcpRuntimeCredential {
+ grantedAt: Date
+ oauthConfigVersion: number
+ scope: ResourceScope
+ credentialGroupId: string
credentialId: string
mcpServerId: string
mcpServerName: string
@@ -100,12 +113,14 @@ export async function decryptManagedMcpTokens(encrypted: string): Promise {
const [row] = await db
.select({
credentialId: credential.id,
workspaceId: credential.workspaceId,
+ organizationId: credential.organizationId,
credentialGroupId: credentialGroup.id,
credentialGroupEnrollmentId: credentialGroupEnrollment.id,
mcpServerId: mcpServers.id,
@@ -119,23 +134,44 @@ export async function loadManagedMcpCredentialApplicationContext(
)
.innerJoin(credentialGroup, eq(credentialGroup.id, credentialGroupEnrollment.credentialGroupId))
.innerJoin(mcpServers, eq(mcpServers.id, credential.mcpServerId))
- .where(and(eq(credential.id, credentialId), eq(credential.type, 'managed_mcp')))
+ .where(
+ and(
+ eq(credential.id, credentialId),
+ eq(credential.type, 'managed_mcp'),
+ sameResourceScopeCondition(credential, credentialGroup),
+ sameResourceScopeCondition(credential, mcpServers),
+ eq(mcpServers.credentialGroupId, credentialGroup.id)
+ )
+ )
.limit(1)
- if (!row?.workspaceId) return null
+ if (!row) return null
+ const workspaceId = executingWorkspaceId ?? row.workspaceId
+ if (!workspaceId) return null
if (!row.managedConnectorId) {
throw new Error(`Managed MCP server ${row.mcpServerId} has no connector ID`)
}
getManagedMcpConnector(row.managedConnectorId)
- const workspaceContext = await loadActiveWorkspaceApplicationContext(row.workspaceId)
- return workspaceContext ? { ...row, ...workspaceContext } : null
+ const workspaceContext = await loadActiveWorkspaceApplicationContext(workspaceId)
+ if (
+ !workspaceContext ||
+ (row.organizationId
+ ? row.organizationId !== workspaceContext.workspaceOrganizationId
+ : row.workspaceId !== workspaceId)
+ )
+ return null
+ return { ...row, ...workspaceContext, organizationId: row.organizationId ?? undefined }
}
export async function loadManagedMcpRuntimeCredential(
credentialId: string,
workspaceId: string
): Promise {
- const ownerBilling = await getWorkspaceOwnerSubscriptionAccess(workspaceId)
- if (!(await isCredentialGroupsAvailable({ workspaceId, ownerBilling }))) {
+ const context = await loadManagedMcpCredentialApplicationContext(credentialId, workspaceId)
+ if (!context) throw new ManagedMcpCredentialError('Managed MCP credential not found', 404)
+ const scope = resourceScopeFromOwner(
+ context.organizationId ? { organizationId: context.organizationId } : { workspaceId }
+ )
+ if (!(await isScopedCredentialGroupsAvailable(scope))) {
throw new ManagedMcpCredentialError(
'Managed MCP credentials are not available for this workspace',
403
@@ -149,7 +185,11 @@ export async function loadManagedMcpRuntimeCredential(
status: credential.managedOauthStatus,
encryptedTokens: credential.encryptedOauthTokenSet,
tools: credential.mcpTools,
+ oauthConfigVersion: credential.mcpOauthConfigVersion,
+ serverOauthConfigVersion: mcpServers.oauthConfigVersion,
+ grantedAt: credential.grantedAt,
enrollmentStatus: credentialGroupEnrollment.status,
+ enrollmentUserId: credentialGroupEnrollment.userId,
groupStatus: credentialGroup.status,
credentialGroupId: credentialGroup.id,
linkedCredentialGroupId: mcpServers.credentialGroupId,
@@ -167,9 +207,10 @@ export async function loadManagedMcpRuntimeCredential(
.where(
and(
eq(credential.id, credentialId),
- eq(credential.workspaceId, workspaceId),
+ resourceScopeCondition(credential, scope),
eq(credential.type, 'managed_mcp'),
- eq(mcpServers.workspaceId, workspaceId),
+ resourceScopeCondition(mcpServers, scope),
+ resourceScopeCondition(credentialGroup, scope),
eq(mcpServers.authType, 'oauth'),
eq(mcpServers.enabled, true),
isNull(mcpServers.deletedAt)
@@ -185,6 +226,8 @@ export async function loadManagedMcpRuntimeCredential(
row.status !== 'active' ||
row.groupStatus !== 'active' ||
!['in_progress', 'completed'].includes(row.enrollmentStatus) ||
+ (scope.kind === 'organization' && !row.enrollmentUserId) ||
+ row.oauthConfigVersion !== row.serverOauthConfigVersion ||
row.linkedCredentialGroupId !== row.credentialGroupId
) {
throw new ManagedMcpCredentialError('Managed MCP credential needs authorization', 401)
@@ -193,27 +236,42 @@ export async function loadManagedMcpRuntimeCredential(
throw new ManagedMcpCredentialError('Managed MCP credential token data is missing', 500)
}
if (!row.tools) throw new ManagedMcpCredentialError('Managed MCP tool metadata is missing', 500)
+ if (!row.grantedAt)
+ throw new ManagedMcpCredentialError('Managed MCP grant version is missing', 500)
return {
credentialId: row.credentialId,
+ oauthConfigVersion: row.serverOauthConfigVersion,
+ credentialGroupId: row.credentialGroupId,
+ scope,
workspaceId,
mcpServerId: row.mcpServerId,
mcpServerName: row.mcpServerName,
tokenVersion: row.encryptedTokens,
+ grantedAt: row.grantedAt,
tokens: await decryptManagedMcpTokens(row.encryptedTokens),
tools: row.tools,
}
}
export async function persistManagedMcpCredential(params: {
+ invitationTokenHash: string
+ oauthConfigVersion: number
+ userId: string
credentialGroupId: string
email: string
enrollmentId: string
- workspaceId: string
+ workspaceId?: string
+ organizationId?: string
mcpServerId: string
mcpServerName: string
tokens: OAuthTokens
tools: Array<{ name: string; description?: string; inputSchema: Record }>
-}): Promise {
+}): Promise<{
+ connectionId: string
+ created: boolean
+ enrollmentStatus: 'in_progress' | 'completed'
+}> {
+ const scope = resourceScopeFromOwner(params)
const encryptedOauthTokenSet = await encryptManagedMcpTokens(params.tokens)
const now = new Date()
const accessTokenExpiresAt =
@@ -242,8 +300,11 @@ export async function persistManagedMcpCredential(params: {
eq(credentialGroupEnrollment.id, params.enrollmentId),
eq(credentialGroupEnrollment.credentialGroupId, params.credentialGroupId),
eq(credentialGroupEnrollment.email, params.email),
- eq(credentialGroup.workspaceId, params.workspaceId),
- eq(mcpServers.workspaceId, params.workspaceId),
+ eq(credentialGroupEnrollment.userId, params.userId),
+ eq(credentialGroupEnrollment.invitationTokenHash, params.invitationTokenHash),
+ resourceScopeCondition(credentialGroup, scope),
+ resourceScopeCondition(mcpServers, scope),
+ eq(mcpServers.oauthConfigVersion, params.oauthConfigVersion),
eq(mcpServers.authType, 'oauth'),
eq(mcpServers.enabled, true),
isNull(mcpServers.deletedAt)
@@ -276,6 +337,7 @@ export async function persistManagedMcpCredential(params: {
.limit(1)
.for('update')
const values = {
+ mcpOauthConfigVersion: params.oauthConfigVersion,
displayName: params.mcpServerName,
managedOauthStatus: 'active' as const,
encryptedOauthTokenSet,
@@ -299,7 +361,7 @@ export async function persistManagedMcpCredential(params: {
const id = generateManagedMcpConnectionId()
const insert: typeof credential.$inferInsert = {
id,
- workspaceId: params.workspaceId,
+ ...resourceScopeColumns(scope),
type: 'managed_mcp',
createdBy: null,
credentialGroupEnrollmentId: params.enrollmentId,
@@ -331,7 +393,11 @@ export async function persistManagedMcpCredential(params: {
if (!updatedEnrollment) {
throw new ManagedMcpCredentialError('Managed MCP enrollment is no longer available', 404)
}
- return connectionId
+ return {
+ connectionId,
+ created: !existing,
+ enrollmentStatus: source.enrollmentStatus === 'completed' ? 'completed' : 'in_progress',
+ }
})
}
@@ -391,7 +457,9 @@ export async function saveManagedMcpRuntimeTokens(
/** Replaces the editor snapshot only after a complete live tools/list succeeds. */
export async function saveManagedMcpToolSnapshot(
credentialId: string,
- tools: ManagedMcpToolSnapshot[]
+ tools: ManagedMcpToolSnapshot[],
+ expectedConfigVersion: number,
+ expectedGrantedAt: Date
): Promise {
const updated = await db
.update(credential)
@@ -400,7 +468,9 @@ export async function saveManagedMcpToolSnapshot(
and(
eq(credential.id, credentialId),
eq(credential.type, 'managed_mcp'),
- eq(credential.managedOauthStatus, 'active')
+ eq(credential.managedOauthStatus, 'active'),
+ eq(credential.mcpOauthConfigVersion, expectedConfigVersion),
+ eq(credential.grantedAt, expectedGrantedAt)
)
)
.returning({ id: credential.id })
diff --git a/apps/sim/lib/credentials/managed-oauth.ts b/apps/sim/lib/credentials/managed-oauth.ts
index 5d8268a0714..ab4dcc65f21 100644
--- a/apps/sim/lib/credentials/managed-oauth.ts
+++ b/apps/sim/lib/credentials/managed-oauth.ts
@@ -73,6 +73,7 @@ interface ResolveManagedOAuthTokenParams {
}
export interface ManagedOAuthCredentialApplicationContext extends WorkspaceAuthorizationContext {
+ organizationId?: string
credentialId: string
credentialGroupId: string
credentialGroupEnrollmentId: string
@@ -180,15 +181,24 @@ async function getManagedCredential(exec: DbOrTx, credentialId: string, owner?:
/** Resolves the canonical workspace context for authorization without exposing token material. */
export async function loadManagedOAuthCredentialApplicationContext(
- credentialId: string
+ credentialId: string,
+ executingWorkspaceId?: string
): Promise {
const row = await getManagedCredential(db, credentialId)
- if (!row?.workspaceId) return null
-
- const workspaceContext = await loadActiveWorkspaceApplicationContext(row.workspaceId)
+ if (!row) return null
+ const workspaceId = executingWorkspaceId ?? row.workspaceId
+ if (!workspaceId) return null
+ const workspaceContext = await loadActiveWorkspaceApplicationContext(workspaceId)
if (!workspaceContext) return null
+ if (
+ row.organizationId
+ ? row.organizationId !== workspaceContext.workspaceOrganizationId
+ : row.workspaceId !== workspaceId
+ )
+ return null
return {
...workspaceContext,
+ ...(row.organizationId ? { organizationId: row.organizationId } : {}),
credentialId: row.id,
credentialGroupId: row.credentialGroupId,
credentialGroupEnrollmentId: row.credentialGroupEnrollmentId,
diff --git a/apps/sim/lib/knowledge/access/availability.test.ts b/apps/sim/lib/knowledge/access/availability.test.ts
index 3b0d77946f3..576966fdb67 100644
--- a/apps/sim/lib/knowledge/access/availability.test.ts
+++ b/apps/sim/lib/knowledge/access/availability.test.ts
@@ -28,7 +28,10 @@ vi.mock('@/lib/credential-groups/scoped-availability', () => ({
isScopedCredentialGroupsAvailable: mocks.scopedGroups,
}))
-import { resolveKnowledgeAccessAvailability } from '@/lib/knowledge/access/availability'
+import {
+ requireOrganizationSearchAvailable,
+ resolveKnowledgeAccessAvailability,
+} from '@/lib/knowledge/access/availability'
describe('knowledge access availability ownership', () => {
beforeEach(() => {
@@ -37,7 +40,7 @@ describe('knowledge access availability ownership', () => {
mocks.enterprise.mockResolvedValue(true)
mocks.scopedGroups.mockResolvedValue(true)
mocks.workspaceGroups.mockResolvedValue(true)
- mocks.workspaceBilling.mockResolvedValue({ isEnterprise: true })
+ mocks.workspaceBilling.mockResolvedValue({ isEnterprise: true, organizationId: 'org-parent' })
})
it('evaluates an organization flag and payer without consulting a workspace', async () => {
@@ -46,8 +49,6 @@ describe('knowledge access availability ownership', () => {
).resolves.toEqual({ sourceMirrored: true, memberScoped: true })
expect(mocks.featureEnabled).toHaveBeenCalledWith('knowledge-member-access', {
orgId: 'org-1',
- workspaceId: undefined,
- userId: 'viewer',
})
expect(mocks.enterprise).toHaveBeenCalledWith('org-1', 'throw')
expect(mocks.scopedGroups).toHaveBeenCalledWith({
@@ -82,13 +83,12 @@ describe('knowledge access availability ownership', () => {
).resolves.toEqual({ sourceMirrored: true, memberScoped: true })
expect(mocks.featureEnabled).toHaveBeenCalledWith('knowledge-member-access', {
workspaceId: 'workspace-1',
- orgId: undefined,
userId: undefined,
})
expect(mocks.workspaceBilling).toHaveBeenCalledWith('workspace-1')
expect(mocks.workspaceGroups).toHaveBeenCalledWith({
- workspaceId: 'workspace-1',
- ownerBilling: { isEnterprise: true },
+ organizationId: 'org-parent',
+ ownerBilling: { isEnterprise: true, organizationId: 'org-parent' },
})
expect(mocks.enterprise).not.toHaveBeenCalled()
expect(mocks.scopedGroups).not.toHaveBeenCalled()
@@ -101,4 +101,49 @@ describe('knowledge access availability ownership', () => {
expect(mocks.workspaceBilling).not.toHaveBeenCalled()
expect(mocks.enterprise).not.toHaveBeenCalled()
})
+
+ it('does not let a user-targeted rollout enable organization retrieval', async () => {
+ mocks.featureEnabled.mockImplementation(
+ async (_flag, context) => context.userId === 'platform-admin'
+ )
+ await expect(
+ resolveKnowledgeAccessAvailability({
+ organizationId: 'org-disabled',
+ userId: 'platform-admin',
+ })
+ ).resolves.toEqual({ sourceMirrored: false, memberScoped: false })
+ })
+
+ it.each([
+ { knowledge: false, groups: true },
+ { knowledge: true, groups: false },
+ { knowledge: false, groups: false },
+ ])(
+ 'denies organization Search when either required gate is off: %j',
+ async ({ knowledge, groups }) => {
+ mocks.featureEnabled.mockResolvedValue(knowledge)
+ mocks.scopedGroups.mockResolvedValue(groups)
+ await expect(requireOrganizationSearchAvailable('org-1')).rejects.toMatchObject({
+ code: 'forbidden',
+ message: 'Search is not enabled for this organization',
+ })
+ }
+ )
+
+ it('allows Search only for the organization enabled in the rollout', async () => {
+ mocks.featureEnabled.mockImplementation(
+ async (_flag, context) => context.orgId === 'org-enabled'
+ )
+ await expect(requireOrganizationSearchAvailable('org-enabled')).resolves.toBeUndefined()
+ await expect(requireOrganizationSearchAvailable('org-other')).rejects.toThrow(
+ 'Search is not enabled'
+ )
+ })
+
+ it('propagates a feature service failure instead of enabling Search', async () => {
+ mocks.featureEnabled.mockRejectedValue(new Error('Feature service unavailable'))
+ await expect(requireOrganizationSearchAvailable('org-1')).rejects.toThrow(
+ 'Feature service unavailable'
+ )
+ })
})
diff --git a/apps/sim/lib/knowledge/access/availability.ts b/apps/sim/lib/knowledge/access/availability.ts
index bd7b639e9bf..31c9d9722b6 100644
--- a/apps/sim/lib/knowledge/access/availability.ts
+++ b/apps/sim/lib/knowledge/access/availability.ts
@@ -13,9 +13,10 @@ import { isScopedCredentialGroupsAvailable } from '@/lib/credential-groups/scope
* Who is asking. Members mode — creating, switching, syncing, and honouring
* member tokens — is judged by the resource owner alone, because the member engine
* has no person to speak for and every gate must agree with it. Retrieval
- * defaults pass the signed-in user as well, so the flag's platform-admin
+ * defaults for workspaces pass the signed-in user as well, so the flag's platform-admin
* clause lets an admin try hybrid retrieval anywhere; an actorless caller
- * (a schedule, a cron, a workspace API key) passes none.
+ * (a schedule, a cron, a workspace API key) passes none. Organization access
+ * always uses the target organization alone, including retrieval.
*/
export interface KnowledgeMemberAccessContext {
workspaceId?: string
@@ -41,17 +42,19 @@ export interface KnowledgeAccessAvailability {
export async function resolveKnowledgeAccessAvailability(
context: KnowledgeMemberAccessContext
): Promise {
+ if (context.organizationId && context.workspaceId)
+ throw new Error('Knowledge access requires one resource owner')
if (
- !(await isFeatureEnabled('knowledge-member-access', {
- workspaceId: context.workspaceId,
- orgId: context.organizationId,
- userId: context.userId,
- }))
+ !(await isFeatureEnabled(
+ 'knowledge-member-access',
+ context.organizationId
+ ? { orgId: context.organizationId }
+ : { workspaceId: context.workspaceId, userId: context.userId }
+ ))
) {
return { sourceMirrored: false, memberScoped: false }
}
if (context.organizationId) {
- if (context.workspaceId) throw new Error('Knowledge access requires one resource owner')
return {
sourceMirrored:
!isHosted || (await isOrganizationOnEnterprisePlan(context.organizationId, 'throw')),
@@ -70,7 +73,7 @@ export async function resolveKnowledgeAccessAvailability(
return {
sourceMirrored,
memberScoped: await isCredentialGroupsAvailable({
- workspaceId: context.workspaceId,
+ organizationId: ownerBilling.organizationId,
ownerBilling,
}),
}
@@ -84,8 +87,9 @@ export async function resolveKnowledgeAccessAvailability(
* check this; the reader's tokens come from `resolveKnowledgeAccessAvailability`
* directly, which this is the `memberScoped` half of, so they can never
* disagree. When it turns off, member-scoped documents are hidden on the next
- * read, members-mode connectors wait rather than change anything, and search
- * returns to the semantic-only default; nothing is deleted.
+ * read and members-mode connectors wait rather than change anything. Organization
+ * Search is blocked; workspace search returns to the semantic-only default.
+ * Nothing is deleted.
*/
export async function isKnowledgeMemberAccessAvailable(
context: KnowledgeMemberAccessContext
@@ -93,6 +97,12 @@ export async function isKnowledgeMemberAccessAvailable(
return (await resolveKnowledgeAccessAvailability(context)).memberScoped
}
+/** Organization Search is available only when both owner-scoped rollout gates are enabled. */
+export async function requireOrganizationSearchAvailable(organizationId: string): Promise {
+ if (await isKnowledgeMemberAccessAvailable({ organizationId })) return
+ throw new OrchestrationError('forbidden', 'Search is not enabled for this organization')
+}
+
/** Refuses with the one message every source-mirroring gate uses when the feature is off. */
export async function requireSourceMirroredAccessAvailable(
context: KnowledgeMemberAccessContext
diff --git a/apps/sim/lib/knowledge/access/scope.test.ts b/apps/sim/lib/knowledge/access/scope.test.ts
index d26de3d84a0..57c9f882188 100644
--- a/apps/sim/lib/knowledge/access/scope.test.ts
+++ b/apps/sim/lib/knowledge/access/scope.test.ts
@@ -3,7 +3,7 @@
*/
import type { Principal } from '@sim/auth/principal'
import { dbChainMockFns, queueTableRows, resetDbChainMock, schemaMock } from '@sim/testing'
-import { inArray } from 'drizzle-orm'
+import { eq, inArray } from 'drizzle-orm'
import { beforeEach, describe, expect, it, vi } from 'vitest'
const { mockAvailability, mockCheckWorkspaceAccess } = vi.hoisted(() => ({
@@ -472,6 +472,12 @@ describe('organization document ACL scope', () => {
})
expect(mockCheckWorkspaceAccess).not.toHaveBeenCalled()
})
+ it('binds org indexing identities to the enrolled Sim user rather than a matching email', async () => {
+ queueTableRows(schemaMock.member, [{ id: 'membership-1' }])
+ queueSubjects([])
+ await resolveKnowledgeAccessScope(SESSION, organization)
+ expect(eq).toHaveBeenCalledWith(schemaMock.credentialGroupEnrollment.userId, schemaMock.user.id)
+ })
it('grants nothing after removal, even when stored provider grants remain', async () => {
queueTableRows(schemaMock.member, [])
queueSubjects([
diff --git a/apps/sim/lib/knowledge/access/scope.ts b/apps/sim/lib/knowledge/access/scope.ts
index bafcfc67f0d..159adc1661d 100644
--- a/apps/sim/lib/knowledge/access/scope.ts
+++ b/apps/sim/lib/knowledge/access/scope.ts
@@ -185,7 +185,9 @@ async function loadUserAccessTokens(
credentialGroupEnrollment,
and(
eq(credentialGroupEnrollment.credentialGroupId, credentialGroup.id),
- eq(credentialGroupEnrollment.email, foldedEmail(user.email)),
+ scope.kind === 'organization'
+ ? eq(credentialGroupEnrollment.userId, user.id)
+ : eq(credentialGroupEnrollment.email, foldedEmail(user.email)),
inArray(credentialGroupEnrollment.status, [...LIVE_ENROLLMENT_STATUSES])
)
)
diff --git a/apps/sim/lib/knowledge/application/operations.test.ts b/apps/sim/lib/knowledge/application/operations.test.ts
index f01c7fb11cb..f6e46310845 100644
--- a/apps/sim/lib/knowledge/application/operations.test.ts
+++ b/apps/sim/lib/knowledge/application/operations.test.ts
@@ -57,9 +57,12 @@ describe('knowledge operation registry', () => {
'knowledge.connectors.update',
'knowledge.connectors.access.update',
'knowledge.search.sources.list',
+ 'knowledge.search.integrations.list',
+ 'knowledge.search.integrations.approve',
'knowledge.connectors.members.list',
'knowledge.connectors.members.enroll',
'knowledge.simSearch.connect',
+ 'knowledge.search.sources.connectApproved',
'knowledge.search.index.read',
'knowledge.search.sources.prepare',
'knowledge.connectors.delete',
diff --git a/apps/sim/lib/knowledge/application/search.test.ts b/apps/sim/lib/knowledge/application/search.test.ts
index 9a7c81fd70c..5f28d2e8b8c 100644
--- a/apps/sim/lib/knowledge/application/search.test.ts
+++ b/apps/sim/lib/knowledge/application/search.test.ts
@@ -2,10 +2,15 @@
* @vitest-environment node
*/
+import { member } from '@sim/db/schema'
+import { queueTableRows, resetDbChainMock } from '@sim/testing'
import { beforeEach, describe, expect, it, vi } from 'vitest'
+import { OrchestrationError } from '@/lib/core/orchestration/types'
const mocks = vi.hoisted(() => ({
resolveWorkspace: vi.fn(),
+ resolveOrganization: vi.fn(),
+ requireOrganizationSearch: vi.fn(),
resolvePermission: vi.fn(),
getKnowledgeBase: vi.fn(),
resolveBilling: vi.fn(),
@@ -48,6 +53,7 @@ vi.mock('@/lib/billing/core/billing-attribution', () => ({
/** Retrieval defaults are the flag's concern; here the flag is off so the search stays as configured. */
vi.mock('@/lib/knowledge/access/availability', () => ({
isKnowledgeMemberAccessAvailable: async () => false,
+ requireOrganizationSearchAvailable: mocks.requireOrganizationSearch,
}))
vi.mock('@/lib/billing/calculations/usage-monitor', () => ({
@@ -56,6 +62,12 @@ vi.mock('@/lib/billing/calculations/usage-monitor', () => ({
vi.mock('@/lib/knowledge/application/contexts', () => ({
resolveKnowledgeWorkspaceContext: mocks.resolveWorkspace,
+ resolveKnowledgeOrganizationContext: mocks.resolveOrganization,
+}))
+
+vi.mock('@/lib/permission-groups/resolve.server', () => ({
+ getUserPermissionConfig: async () => null,
+ getUserPermissionConfigForOrganization: async () => null,
}))
vi.mock('@/lib/knowledge/service', () => ({
@@ -107,6 +119,12 @@ const knowledgeBase = {
describe('knowledge search application use case', () => {
beforeEach(() => {
vi.clearAllMocks()
+ resetDbChainMock()
+ mocks.requireOrganizationSearch.mockResolvedValue(undefined)
+ mocks.resolveOrganization.mockResolvedValue({
+ organizationId: 'org-canonical',
+ workspaceId: undefined,
+ })
mocks.resolveWorkspace.mockResolvedValue(workspace)
mocks.resolvePermission.mockResolvedValue('read')
mocks.getKnowledgeBase.mockResolvedValue(knowledgeBase)
@@ -167,6 +185,28 @@ describe('knowledge search application use case', () => {
expect(result.totalResults).toBe(0)
})
+ it('gates organization search using the persisted owner even when the request omits it', async () => {
+ mocks.getKnowledgeBase.mockResolvedValue({
+ ...knowledgeBase,
+ workspaceId: null,
+ organizationId: 'org-canonical',
+ })
+ queueTableRows(member, [{ role: 'member' }])
+ mocks.requireOrganizationSearch.mockRejectedValue(
+ new OrchestrationError('forbidden', 'Search is not enabled for this organization')
+ )
+ await expect(
+ searchKnowledge.execute({
+ principal: { kind: 'personal_api_key', userId: 'user-1', keyId: 'key-1' },
+ input: { knowledgeBaseIds: ['knowledge-1'], query: 'answer', topK: 5 },
+ })
+ ).rejects.toThrow('Search is not enabled for this organization')
+ expect(mocks.requireOrganizationSearch).toHaveBeenCalledExactlyOnceWith('org-canonical')
+ expect(mocks.resolveBilling).not.toHaveBeenCalled()
+ expect(mocks.generateEmbedding).not.toHaveBeenCalled()
+ expect(mocks.executeSearch).not.toHaveBeenCalled()
+ })
+
it('authorizes every canonical knowledge base before billing and search', async () => {
const result = await searchKnowledge.execute({
principal: { kind: 'session', userId: 'user-1', sessionId: 'session-1' },
diff --git a/apps/sim/lib/knowledge/application/search.ts b/apps/sim/lib/knowledge/application/search.ts
index 4cc80ecc374..75dfcd491b8 100644
--- a/apps/sim/lib/knowledge/application/search.ts
+++ b/apps/sim/lib/knowledge/application/search.ts
@@ -21,6 +21,7 @@ import {
isDurableSecretProvenanceEnforced,
reportUnrecordedDurableProvenance,
} from '@/lib/execution/durable-secret-provenance-enforcement'
+import { requireOrganizationSearchAvailable } from '@/lib/knowledge/access/availability'
import { createKnowledgeAccessProvider } from '@/lib/knowledge/access/scope'
import type { KnowledgeAccessProvider } from '@/lib/knowledge/access/types'
import { defineAuthorizedKnowledgeUseCase } from '@/lib/knowledge/application/authorized-knowledge-use-case'
@@ -263,6 +264,7 @@ export const searchKnowledge = defineAuthorizedKnowledgeUseCase({
resolveKnowledgeSearchContext(input, principal),
async execute({ principal, input, context }) {
input.signal?.throwIfAborted()
+ if (context.organizationId) await requireOrganizationSearchAvailable(context.organizationId)
const requestId = generateRequestId()
const hasQuery = Boolean(input.query?.trim())
const filters = input.tagFilters ?? []
diff --git a/apps/sim/lib/knowledge/application/sim-search.ts b/apps/sim/lib/knowledge/application/sim-search.ts
index c5f11dc5453..b2b2f8a9ade 100644
--- a/apps/sim/lib/knowledge/application/sim-search.ts
+++ b/apps/sim/lib/knowledge/application/sim-search.ts
@@ -24,6 +24,7 @@ import { generateRequestId } from '@/lib/core/utils/request'
import { ensureWorkspaceAccountsGroup } from '@/lib/credential-groups/service'
import {
requireKnowledgeMemberAccessAvailable,
+ requireOrganizationSearchAvailable,
requireSourceMirroredAccessAvailable,
} from '@/lib/knowledge/access/availability'
import { defineAuthorizedKnowledgeUseCase } from '@/lib/knowledge/application/authorized-knowledge-use-case'
@@ -121,6 +122,7 @@ export const readSearchIndex = defineAuthorizedKnowledgeUseCase({
operation: knowledgeOperations.readSearchIndex,
resolveContext: ({ input }: { input: ResourceOwner }) => resolveKnowledgeOwnerContext(input),
async execute({ context }) {
+ if (context.organizationId) await requireOrganizationSearchAvailable(context.organizationId)
return {
workspaceId: context.workspaceId,
knowledgeBaseId: (await findSearchIndex(resourceScopeFromOwner(context)))?.id ?? null,
diff --git a/apps/sim/lib/knowledge/connectors/member-access.test.ts b/apps/sim/lib/knowledge/connectors/member-access.test.ts
index 8d277bc3053..a56745e6fe0 100644
--- a/apps/sim/lib/knowledge/connectors/member-access.test.ts
+++ b/apps/sim/lib/knowledge/connectors/member-access.test.ts
@@ -679,6 +679,22 @@ describe('organization member credential binding', () => {
)
).toBe(true)
}
+ expect(
+ hasMockCondition(
+ predicate,
+ (condition) =>
+ condition.type === 'inArray' && condition.column === knowledgeConnector.status
+ )
+ ).toBe(true)
+ expect(
+ hasMockCondition(
+ predicate,
+ (condition) =>
+ condition.type === 'ne' &&
+ condition.left === knowledgeConnector.memberSyncStatus &&
+ condition.right === 'disabled'
+ )
+ ).toBe(true)
})
it('denies a connector whose current canonical owner or option no longer matches', async () => {
diff --git a/apps/sim/lib/knowledge/connectors/member-access.ts b/apps/sim/lib/knowledge/connectors/member-access.ts
index 7854e1ad939..005169b8407 100644
--- a/apps/sim/lib/knowledge/connectors/member-access.ts
+++ b/apps/sim/lib/knowledge/connectors/member-access.ts
@@ -7,7 +7,7 @@ import {
knowledgeConnector,
} from '@sim/db/schema'
import { createLogger } from '@sim/logger'
-import { and, eq, isNull } from 'drizzle-orm'
+import { and, eq, inArray, isNull, ne } from 'drizzle-orm'
import { OrchestrationError } from '@/lib/core/orchestration/types'
import {
type ResourceOwner,
@@ -42,6 +42,7 @@ import {
rejectManagedOAuthToken,
resolveManagedOAuthToken,
} from '@/lib/credentials/managed-oauth'
+import { MEMBER_LOCKABLE_CONNECTOR_STATUSES } from '@/lib/knowledge/connectors/sync-lock'
import {
CREDENTIAL_GROUP_CREDENTIAL_USE_ACTION,
type ResourcePolicyBindingFor,
@@ -279,6 +280,8 @@ export async function assertKnowledgeConnectorCredentialAccess(
and(
eq(knowledgeConnector.id, binding.connectorId),
eq(knowledgeConnector.accessMode, 'members'),
+ inArray(knowledgeConnector.status, MEMBER_LOCKABLE_CONNECTOR_STATUSES),
+ ne(knowledgeConnector.memberSyncStatus, 'disabled'),
eq(knowledgeConnector.credentialGroupId, binding.credentialGroupId),
eq(knowledgeConnector.credentialGroupOptionId, binding.credentialGroupOptionId),
resourceScopeCondition(knowledgeBase, scope),
diff --git a/apps/sim/lib/knowledge/connectors/member-provisioning.test.ts b/apps/sim/lib/knowledge/connectors/member-provisioning.test.ts
index c5dd93b14fc..12f729aaf89 100644
--- a/apps/sim/lib/knowledge/connectors/member-provisioning.test.ts
+++ b/apps/sim/lib/knowledge/connectors/member-provisioning.test.ts
@@ -30,9 +30,13 @@ vi.mock('@/lib/billing/core/billing-attribution', () => ({
vi.mock('@/lib/credential-groups/service', () => ({
ensureWorkspaceAccountsGroup: vi.fn(),
}))
+vi.mock('@/lib/credential-groups/organization-setup', () => ({
+ requireOrganizationAccountsSetup: vi.fn(),
+}))
import type { CredentialGroupCredentialListContext } from '@/lib/credential-groups/credentials'
import { inviteCredentialGroupEnrollment } from '@/lib/credential-groups/enrollments'
+import { requireOrganizationAccountsSetup } from '@/lib/credential-groups/organization-setup'
import { CredentialGroupProviderConfigurationError } from '@/lib/credential-groups/provider-adapter'
import { ensureWorkspaceAccountsGroup } from '@/lib/credential-groups/service'
import {
@@ -73,6 +77,7 @@ describe('provisionKnowledgeConnectorMembersBinding', () => {
})
beforeEach(() => {
vi.mocked(ensureWorkspaceAccountsGroup).mockReset()
+ vi.mocked(requireOrganizationAccountsSetup).mockReset()
})
it('reuses the configured Slack option in the workspace singleton', async () => {
@@ -141,6 +146,7 @@ describe('provisionKnowledgeConnectorMembersBinding', () => {
userId: 'user-1',
})
).resolves.toEqual({ credentialGroupId: 'accounts-1', credentialGroupOptionId: 'option-1' })
+ expect(requireOrganizationAccountsSetup).toHaveBeenCalledWith('org-1', 'accounts-1')
expect(ensureWorkspaceAccountsGroup).toHaveBeenCalledExactlyOnceWith(
{ kind: 'organization', organizationId: 'org-1' },
'user-1',
diff --git a/apps/sim/lib/knowledge/connectors/member-provisioning.ts b/apps/sim/lib/knowledge/connectors/member-provisioning.ts
index 60dbe8ebe33..5a791842bec 100644
--- a/apps/sim/lib/knowledge/connectors/member-provisioning.ts
+++ b/apps/sim/lib/knowledge/connectors/member-provisioning.ts
@@ -27,6 +27,7 @@ import {
loadScopedAccountsCredentialListContext,
} from '@/lib/credential-groups/credentials'
import { inviteCredentialGroupEnrollment } from '@/lib/credential-groups/enrollments'
+import { requireOrganizationAccountsSetup } from '@/lib/credential-groups/organization-setup'
import { CredentialGroupProviderConfigurationError } from '@/lib/credential-groups/provider-adapter'
import { getCredentialGroupProviderAdapter } from '@/lib/credential-groups/provider-registry'
import {
@@ -113,6 +114,7 @@ export async function provisionKnowledgeConnectorMembersBinding(input: {
option.status === 'active' &&
option.configurationStatus === 'ready'
)
+ if (input.organizationId) await requireOrganizationAccountsSetup(input.organizationId, group.id)
if (options.length !== 1) {
throw new OrchestrationError(
'validation',
@@ -362,7 +364,9 @@ export async function resolveViewerConnectorMemberships(input: {
and(
resourceScopeCondition(credentialGroup, resourceScopeFromOwner(input)),
eq(credentialGroup.id, group.credentialGroupId),
- eq(credentialGroupEnrollment.email, email)
+ input.organizationId
+ ? eq(credentialGroupEnrollment.userId, input.userId)
+ : eq(credentialGroupEnrollment.email, email)
)
)
diff --git a/apps/sim/lib/knowledge/connectors/organization-account-indexing.test.ts b/apps/sim/lib/knowledge/connectors/organization-account-indexing.test.ts
new file mode 100644
index 00000000000..ac54dd2f2bf
--- /dev/null
+++ b/apps/sim/lib/knowledge/connectors/organization-account-indexing.test.ts
@@ -0,0 +1,130 @@
+/** @vitest-environment node */
+import { credentialGroup, knowledgeBase, knowledgeConnector } from '@sim/db/schema'
+import {
+ dbChainMockFns,
+ flattenMockConditions,
+ queueTableRows,
+ resetDbChainMock,
+} from '@sim/testing'
+import { beforeEach, describe, expect, it, vi } from 'vitest'
+
+const { validateBinding } = vi.hoisted(() => ({ validateBinding: vi.fn() }))
+vi.mock('@/lib/knowledge/connectors/member-access', () => ({
+ validateKnowledgeConnectorMembersBinding: validateBinding,
+}))
+
+import { setOrganizationAccountIndexing } from '@/lib/knowledge/connectors/organization-account-indexing'
+
+const input = {
+ organizationId: 'org-1',
+ credentialGroupId: 'group-1',
+ optionId: 'gmail-option',
+ enabled: false,
+}
+const group = {
+ status: 'active',
+ options: [{ id: 'gmail-option', status: 'active', provider: 'gmail' }],
+}
+const source = {
+ id: 'source-1',
+ knowledgeBaseId: 'kb-1',
+ status: 'active',
+ memberSyncStatus: 'idle',
+ sourceConfig: {},
+}
+
+describe('organization provider indexing changes', () => {
+ beforeEach(() => {
+ vi.clearAllMocks()
+ resetDbChainMock()
+ validateBinding.mockReturnValue({ ok: true })
+ queueTableRows(credentialGroup, [group])
+ })
+
+ it('pauses all bound sources in one transaction, cancels queued work and retains documents', async () => {
+ queueTableRows(knowledgeConnector, [
+ source,
+ { ...source, id: 'source-2', memberSyncStatus: 'pending' },
+ ])
+ await expect(setOrganizationAccountIndexing(input)).resolves.toMatchObject({
+ enabled: false,
+ changed: true,
+ knowledgeBaseIds: ['kb-1'],
+ })
+ expect(dbChainMockFns.set).toHaveBeenCalledWith(
+ expect.objectContaining({
+ status: 'paused',
+ nextMemberSyncAt: null,
+ memberSyncLockToken: null,
+ memberSyncStatus: 'idle',
+ })
+ )
+ expect(dbChainMockFns.update).toHaveBeenCalledExactlyOnceWith(knowledgeConnector)
+ expect(dbChainMockFns.delete).not.toHaveBeenCalled()
+ const predicates = dbChainMockFns.where.mock.calls.flatMap(([condition]) =>
+ flattenMockConditions(condition)
+ )
+ for (const expected of [
+ { type: 'eq', left: knowledgeBase.organizationId, right: 'org-1' },
+ { type: 'eq', left: knowledgeBase.isSearchIndex, right: true },
+ { type: 'eq', left: knowledgeConnector.credentialGroupId, right: 'group-1' },
+ { type: 'eq', left: knowledgeConnector.credentialGroupOptionId, right: 'gmail-option' },
+ { type: 'eq', left: knowledgeConnector.accessMode, right: 'members' },
+ ])
+ expect(predicates).toContainEqual(expected)
+ })
+
+ it('resumes a paused source with a fresh schedule and revalidates its member binding', async () => {
+ queueTableRows(knowledgeConnector, [{ ...source, status: 'paused' }])
+ await setOrganizationAccountIndexing({ ...input, enabled: true })
+ expect(validateBinding).toHaveBeenCalledWith(
+ expect.objectContaining({ credentialGroupOptionId: 'gmail-option', group })
+ )
+ expect(dbChainMockFns.set).toHaveBeenCalledWith(
+ expect.objectContaining({
+ status: 'active',
+ nextMemberSyncAt: expect.any(Date),
+ lastMemberSyncError: null,
+ })
+ )
+ })
+
+ it('does not write when every source already has the requested state', async () => {
+ queueTableRows(knowledgeConnector, [{ ...source, status: 'paused' }])
+ await expect(setOrganizationAccountIndexing(input)).resolves.toMatchObject({ changed: false })
+ expect(dbChainMockFns.update).not.toHaveBeenCalled()
+ })
+
+ it('refuses the entire change when one source has an active run', async () => {
+ queueTableRows(knowledgeConnector, [
+ source,
+ { ...source, id: 'source-2', memberSyncStatus: 'running' },
+ ])
+ await expect(setOrganizationAccountIndexing(input)).rejects.toMatchObject({ code: 'conflict' })
+ expect(dbChainMockFns.update).not.toHaveBeenCalled()
+ })
+
+ it('refuses a stale or foreign option before touching any source', async () => {
+ await expect(
+ setOrganizationAccountIndexing({ ...input, optionId: 'foreign-option' })
+ ).rejects.toMatchObject({ code: 'not_found' })
+ expect(dbChainMockFns.update).not.toHaveBeenCalled()
+ })
+
+ it('requires setup instead of reporting indexing enabled without a source', async () => {
+ queueTableRows(knowledgeConnector, [])
+ await expect(setOrganizationAccountIndexing({ ...input, enabled: true })).rejects.toMatchObject(
+ { code: 'not_found' }
+ )
+ expect(dbChainMockFns.update).not.toHaveBeenCalled()
+ })
+
+ it('rejects re-enabling a source whose scopes no longer meet the ingestion requirements', async () => {
+ queueTableRows(knowledgeConnector, [{ ...source, status: 'paused' }])
+ validateBinding.mockReturnValue({ ok: false, message: 'Reconnect with the required scopes' })
+ await expect(setOrganizationAccountIndexing({ ...input, enabled: true })).rejects.toThrow(
+ 'Reconnect with the required scopes'
+ )
+ expect(dbChainMockFns.update).not.toHaveBeenCalled()
+ })
+})
diff --git a/apps/sim/lib/knowledge/connectors/organization-account-indexing.ts b/apps/sim/lib/knowledge/connectors/organization-account-indexing.ts
new file mode 100644
index 00000000000..a1e34897ede
--- /dev/null
+++ b/apps/sim/lib/knowledge/connectors/organization-account-indexing.ts
@@ -0,0 +1,136 @@
+import { db } from '@sim/db'
+import { credentialGroup, knowledgeBase, knowledgeConnector } from '@sim/db/schema'
+import { isPlainRecord } from '@sim/utils/object'
+import { and, asc, eq, inArray, isNull } from 'drizzle-orm'
+import { OrchestrationError } from '@/lib/core/orchestration/types'
+import { resourceScopeCondition } from '@/lib/core/resource-scope.server'
+import { getCredentialGroupIndexingConnector } from '@/lib/credential-groups/indexing'
+import { ORGANIZATION_ACCOUNT_INDEXING_SOURCE_LIMIT } from '@/lib/credential-groups/limits'
+import { isCredentialGroupProvider } from '@/lib/credential-groups/providers'
+import { validateKnowledgeConnectorMembersBinding } from '@/lib/knowledge/connectors/member-access'
+
+export interface SetOrganizationAccountIndexingInput {
+ organizationId: string
+ credentialGroupId: string
+ optionId: string
+ enabled: boolean
+}
+
+/** Changes every Search source bound to this org option atomically, respecting running sync leases. */
+export async function setOrganizationAccountIndexing(input: SetOrganizationAccountIndexingInput) {
+ const scope = { kind: 'organization' as const, organizationId: input.organizationId }
+ return db.transaction(async (tx) => {
+ const [group] = await tx
+ .select({ options: credentialGroup.options, status: credentialGroup.status })
+ .from(credentialGroup)
+ .where(
+ and(
+ eq(credentialGroup.id, input.credentialGroupId),
+ resourceScopeCondition(credentialGroup, scope)
+ )
+ )
+ .limit(1)
+ .for('update')
+ if (!group)
+ throw new OrchestrationError('not_found', 'Organization connected accounts were not found')
+ const option = group.options.find(
+ (candidate) => candidate.id === input.optionId && candidate.status === 'active'
+ )
+ if (!option || !isCredentialGroupProvider(option.provider))
+ throw new OrchestrationError('not_found', 'Connected account provider was not found')
+ const connector = getCredentialGroupIndexingConnector(option.provider)
+ if (!connector)
+ throw new OrchestrationError('validation', 'Indexing is not supported for this provider')
+ const sources = await tx
+ .select({
+ id: knowledgeConnector.id,
+ knowledgeBaseId: knowledgeBase.id,
+ sourceConfig: knowledgeConnector.sourceConfig,
+ status: knowledgeConnector.status,
+ memberSyncStatus: knowledgeConnector.memberSyncStatus,
+ })
+ .from(knowledgeConnector)
+ .innerJoin(knowledgeBase, eq(knowledgeBase.id, knowledgeConnector.knowledgeBaseId))
+ .where(
+ and(
+ resourceScopeCondition(knowledgeBase, scope),
+ eq(knowledgeBase.isSearchIndex, true),
+ isNull(knowledgeBase.deletedAt),
+ eq(knowledgeConnector.credentialGroupId, input.credentialGroupId),
+ eq(knowledgeConnector.credentialGroupOptionId, option.id),
+ eq(knowledgeConnector.connectorType, connector.type),
+ eq(knowledgeConnector.accessMode, 'members'),
+ isNull(knowledgeConnector.archivedAt),
+ isNull(knowledgeConnector.deletedAt)
+ )
+ )
+ .orderBy(asc(knowledgeConnector.id))
+ .limit(ORGANIZATION_ACCOUNT_INDEXING_SOURCE_LIMIT + 1)
+ .for('update')
+ if (sources.length > ORGANIZATION_ACCOUNT_INDEXING_SOURCE_LIMIT)
+ throw new OrchestrationError('validation', 'Too many indexing sources for one provider')
+ if (!sources.length)
+ throw new OrchestrationError('not_found', 'Set up an indexing source for this provider first')
+ const changed = sources.filter((source) =>
+ input.enabled
+ ? source.status === 'paused' ||
+ source.status === 'disabled' ||
+ source.memberSyncStatus === 'disabled'
+ : source.status !== 'paused'
+ )
+ if (
+ changed.some((source) => source.status === 'syncing' || source.memberSyncStatus === 'running')
+ )
+ throw new OrchestrationError(
+ 'conflict',
+ 'Indexing is running. Wait for the current sync to finish, then try again.'
+ )
+ if (input.enabled) {
+ for (const source of changed) {
+ if (!isPlainRecord(source.sourceConfig))
+ throw new OrchestrationError('validation', 'Indexing source settings are invalid')
+ const validation = validateKnowledgeConnectorMembersBinding({
+ connectorMeta: connector.meta,
+ group,
+ credentialGroupOptionId: option.id,
+ sourceConfig: source.sourceConfig,
+ })
+ if (!validation.ok) throw new OrchestrationError('validation', validation.message)
+ }
+ }
+ if (changed.length) {
+ const now = new Date()
+ await tx
+ .update(knowledgeConnector)
+ .set({
+ status: input.enabled ? 'active' : 'paused',
+ memberSyncStatus: 'idle',
+ memberSyncLockToken: null,
+ memberSyncLockLeaseAt: null,
+ syncLockToken: null,
+ syncLockLeaseAt: null,
+ nextMemberSyncAt: input.enabled ? now : null,
+ updatedAt: now,
+ ...(input.enabled
+ ? { consecutiveFailures: 0, lastSyncError: null, lastMemberSyncError: null }
+ : {}),
+ })
+ .where(
+ and(
+ inArray(
+ knowledgeConnector.id,
+ changed.map((source) => source.id)
+ ),
+ eq(knowledgeConnector.credentialGroupId, input.credentialGroupId),
+ eq(knowledgeConnector.credentialGroupOptionId, option.id)
+ )
+ )
+ }
+ return {
+ enabled: input.enabled,
+ changed: changed.length > 0,
+ providerName: connector.meta.name,
+ knowledgeBaseIds: [...new Set(sources.map((source) => source.knowledgeBaseId))],
+ }
+ })
+}
diff --git a/apps/sim/lib/knowledge/mcp/route-handler.test.ts b/apps/sim/lib/knowledge/mcp/route-handler.test.ts
index af9a5116d8d..4ce372558b5 100644
--- a/apps/sim/lib/knowledge/mcp/route-handler.test.ts
+++ b/apps/sim/lib/knowledge/mcp/route-handler.test.ts
@@ -12,6 +12,7 @@ const mocks = vi.hoisted(() => ({
handle: vi.fn(),
connect: vi.fn(),
close: vi.fn(),
+ requireSearch: vi.fn(),
}))
vi.mock('@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js', () => ({
WebStandardStreamableHTTPServerTransport: class {
@@ -63,6 +64,7 @@ vi.mock('@/lib/knowledge/application/connector-access', () => ({
vi.mock('@/lib/knowledge/access/availability', () => ({
requireKnowledgeMemberAccessAvailable: vi.fn(),
requireSourceMirroredAccessAvailable: vi.fn(),
+ requireOrganizationSearchAvailable: mocks.requireSearch,
}))
vi.mock('@/connectors/registry', () => ({ CONNECTOR_META_REGISTRY: {} }))
vi.mock('@/lib/sim-search/connectors', () => ({
@@ -72,6 +74,7 @@ vi.mock('@/lib/sim-search/connectors', () => ({
}))
import { OAUTH_ACCESS_TOKEN_PREFIX } from '@/lib/auth/oauth-provider'
+import { OrchestrationError } from '@/lib/core/orchestration/types'
import { createKnowledgeMcpHandlers } from '@/lib/knowledge/mcp/route-handler'
import { DEFAULT_PERMISSION_GROUP_CONFIG } from '@/lib/permission-groups/fields'
@@ -105,6 +108,7 @@ beforeEach(() => {
resetDbChainMock()
mocks.authenticate.mockResolvedValue(auth)
mocks.config.mockResolvedValue(null)
+ mocks.requireSearch.mockResolvedValue(undefined)
mocks.index.mockResolvedValue({ id: 'index-1' })
mocks.createServer.mockReturnValue({ connect: mocks.connect, close: mocks.close })
mocks.handle.mockImplementation(
@@ -122,6 +126,7 @@ describe('organization MCP request admission', () => {
expect(result.status).toBe(200)
expect(result.headers.get('Cache-Control')).toBe('private, no-store')
expect(mocks.index).toHaveBeenCalledWith({ kind: 'organization', organizationId: 'org-1' })
+ expect(mocks.requireSearch).toHaveBeenCalledExactlyOnceWith('org-1')
expect(mocks.createServer).toHaveBeenCalledWith(
expect.objectContaining({ auth, organizationId: 'org-1', searchIndexId: 'index-1' })
)
@@ -168,6 +173,17 @@ describe('organization MCP request admission', () => {
dbChainMockFns.limit.mockResolvedValue([])
expect((await post()).status).toBe(404)
expect(mocks.index).not.toHaveBeenCalled()
+ expect(mocks.requireSearch).not.toHaveBeenCalled()
+ })
+ it('rejects disabled organization Search before index lookup or MCP discovery', async () => {
+ mocks.requireSearch.mockRejectedValue(
+ new OrchestrationError('forbidden', 'Search is not enabled for this organization')
+ )
+ const response = await post()
+ expect(response.status).toBe(403)
+ expect(mocks.requireSearch).toHaveBeenCalledWith('org-1')
+ expect(mocks.index).not.toHaveBeenCalled()
+ expect(mocks.createServer).not.toHaveBeenCalled()
})
it.each(['disablePersonalApiKeys', 'hideKnowledgeBaseTab'])(
'enforces current organization policy: %s',
diff --git a/apps/sim/lib/mcp/application/execute-managed-tool.test.ts b/apps/sim/lib/mcp/application/execute-managed-tool.test.ts
index f543798bb50..e4d03b13e36 100644
--- a/apps/sim/lib/mcp/application/execute-managed-tool.test.ts
+++ b/apps/sim/lib/mcp/application/execute-managed-tool.test.ts
@@ -92,6 +92,9 @@ describe('executeManagedMcpToolUseCase', () => {
mcpServerId: context.mcpServerId,
mcpServerName: context.mcpServerName,
workspaceId: context.workspaceId,
+ scope: { kind: 'organization', organizationId: 'org-1' },
+ oauthConfigVersion: 2,
+ grantedAt: new Date('2026-09-01'),
tokenVersion: 'encrypted-token-version-1',
tokens: { access_token: 'access-token' },
tools: [],
@@ -185,27 +188,32 @@ describe('executeManagedMcpToolUseCase', () => {
expect(mocks.loadRuntime).toHaveBeenCalledWith(context.credentialId, context.workspaceId)
expect(mocks.discoverTools).toHaveBeenCalledWith(
context.mcpServerId,
- context.workspaceId,
+ { kind: 'organization', organizationId: 'org-1' },
{ credentialId: context.credentialId, loadProvider: expect.any(Function) },
signal,
{ requireComplete: true }
)
- expect(mocks.saveToolSnapshot).toHaveBeenCalledWith(context.credentialId, [
- {
- name: 'search_transcripts',
- description: 'Search Fireflies transcripts',
- inputSchema: {
- type: 'object',
- required: ['query'],
- properties: { query: { type: 'string' } },
+ expect(mocks.saveToolSnapshot).toHaveBeenCalledWith(
+ context.credentialId,
+ [
+ {
+ name: 'search_transcripts',
+ description: 'Search Fireflies transcripts',
+ inputSchema: {
+ type: 'object',
+ required: ['query'],
+ properties: { query: { type: 'string' } },
+ },
},
- },
- ])
+ ],
+ 2,
+ new Date('2026-09-01')
+ )
expect(mocks.executeTool).toHaveBeenCalledWith(
expect.objectContaining({
connectionId: context.credentialId,
serverId: context.mcpServerId,
- workspaceId: context.workspaceId,
+ scope: { kind: 'organization', organizationId: 'org-1' },
toolCall: {
name: 'search_transcripts',
arguments: { query: 'onboarding' },
diff --git a/apps/sim/lib/mcp/application/execute-managed-tool.ts b/apps/sim/lib/mcp/application/execute-managed-tool.ts
index 00ed0296fae..a75aad9ae00 100644
--- a/apps/sim/lib/mcp/application/execute-managed-tool.ts
+++ b/apps/sim/lib/mcp/application/execute-managed-tool.ts
@@ -40,7 +40,10 @@ function requireToolSchema(value: unknown): McpToolSchema {
export const executeManagedMcpToolUseCase = defineAuthorizedWorkspaceUseCase({
operation: credentialOperations.useManagedMcp,
resolveContext: async ({ input }: { input: ExecuteManagedMcpToolInput }) => {
- const context = await loadManagedMcpCredentialApplicationContext(input.credentialId)
+ const context = await loadManagedMcpCredentialApplicationContext(
+ input.credentialId,
+ input.workspaceId
+ )
if (!context) throw new OrchestrationError('not_found', 'Managed MCP connection not found')
if (context.workspaceId !== input.workspaceId) {
throw new OrchestrationError('not_found', 'Managed MCP connection not found')
@@ -56,7 +59,7 @@ export const executeManagedMcpToolUseCase = defineAuthorizedWorkspaceUseCase({
const runtime = await loadManagedMcpRuntimeCredential(context.credentialId, context.workspaceId)
const tools = await mcpService.discoverManagedMcpTools(
runtime.mcpServerId,
- runtime.workspaceId,
+ runtime.scope,
{
credentialId: runtime.credentialId,
loadProvider: () => loadManagedMcpAuthProvider(runtime.credentialId, runtime.workspaceId),
@@ -70,7 +73,9 @@ export const executeManagedMcpToolUseCase = defineAuthorizedWorkspaceUseCase({
name: tool.name,
...(tool.description ? { description: tool.description } : {}),
inputSchema: tool.inputSchema,
- }))
+ })),
+ runtime.oauthConfigVersion,
+ runtime.grantedAt
)
const discovered = tools.find((tool) => tool.name === input.toolName)
if (!discovered) {
@@ -93,7 +98,7 @@ export const executeManagedMcpToolUseCase = defineAuthorizedWorkspaceUseCase({
const providerResult = await mcpService.executeManagedMcpTool({
connectionId: runtime.credentialId,
serverId: runtime.mcpServerId,
- workspaceId: runtime.workspaceId,
+ scope: runtime.scope,
toolCall,
extraHeaders,
signal: input.signal,
diff --git a/apps/sim/lib/mcp/application/managed-auth-provider.ts b/apps/sim/lib/mcp/application/managed-auth-provider.ts
index 039062babfe..cf631231024 100644
--- a/apps/sim/lib/mcp/application/managed-auth-provider.ts
+++ b/apps/sim/lib/mcp/application/managed-auth-provider.ts
@@ -1,4 +1,6 @@
import type { OAuthClientProvider } from '@modelcontextprotocol/sdk/client/auth.js'
+import { resourceScopeFields } from '@/lib/core/resource-scope'
+import { requireOrganizationAccountsWorkspaceAccess } from '@/lib/credential-groups/application/organization-workspace-access'
import {
loadManagedMcpRuntimeCredential,
saveManagedMcpRuntimeTokens,
@@ -12,9 +14,17 @@ export async function loadManagedMcpAuthProvider(
workspaceId: string
): Promise {
const current = await loadManagedMcpRuntimeCredential(credentialId, workspaceId)
+ if (current.scope.kind === 'organization') {
+ await requireOrganizationAccountsWorkspaceAccess({
+ workspaceId,
+ workspaceOrganizationId: current.scope.organizationId,
+ organizationId: current.scope.organizationId,
+ credentialGroupId: current.credentialGroupId,
+ })
+ }
const clientRow = await getOrCreateOauthRow({
mcpServerId: current.mcpServerId,
- workspaceId: current.workspaceId,
+ ...resourceScopeFields(current.scope),
})
const preregistered = await loadPreregisteredClient(current.mcpServerId)
let tokenVersion: string | null = current.tokenVersion
diff --git a/apps/sim/lib/mcp/application/managed-connections.ts b/apps/sim/lib/mcp/application/managed-connections.ts
index 226d765fe15..36dbfb60d99 100644
--- a/apps/sim/lib/mcp/application/managed-connections.ts
+++ b/apps/sim/lib/mcp/application/managed-connections.ts
@@ -31,7 +31,12 @@ export const listManagedMcpConnectionsUseCase = defineAuthorizedWorkspaceUseCase
authorizationOptions: {},
async execute({ context }) {
const ownerBilling = await getWorkspaceOwnerSubscriptionAccess(context.workspaceId)
- if (!(await isCredentialGroupsAvailable({ workspaceId: context.workspaceId, ownerBilling }))) {
+ if (
+ !(await isCredentialGroupsAvailable({
+ organizationId: ownerBilling.organizationId,
+ ownerBilling,
+ }))
+ ) {
return { servers: [], tools: [] }
}
const managedCatalogScope = () =>
diff --git a/apps/sim/lib/mcp/oauth/storage.ts b/apps/sim/lib/mcp/oauth/storage.ts
index 3d5399e1b80..1e1f006b195 100644
--- a/apps/sim/lib/mcp/oauth/storage.ts
+++ b/apps/sim/lib/mcp/oauth/storage.ts
@@ -11,6 +11,11 @@ import { interruptibleSleep } from '@sim/utils/helpers'
import { generateId, generateShortId } from '@sim/utils/id'
import { and, eq, gt } from 'drizzle-orm'
import { acquireLock, extendLock, releaseLock } from '@/lib/core/config/redis'
+import {
+ resourceScopeColumns,
+ resourceScopeFromOwner,
+ sameResourceScope,
+} from '@/lib/core/resource-scope'
import { decryptSecret, encryptSecret } from '@/lib/core/security/encryption'
const logger = createLogger('McpOauthStorage')
@@ -25,7 +30,8 @@ export interface McpOauthRow {
id: string
mcpServerId: string
userId: string | null
- workspaceId: string
+ workspaceId: string | null
+ organizationId: string | null
clientInformation: OAuthClientInformationMixed | null
tokens: OAuthTokens | null
codeVerifier: string | null
@@ -72,10 +78,16 @@ async function safeDecrypt(
export async function getOrCreateOauthRow(params: {
mcpServerId: string
userId?: string | null
- workspaceId: string
+ workspaceId?: string
+ organizationId?: string
}): Promise {
+ const scope = resourceScopeFromOwner(params)
const existing = await loadOauthRow(params)
- if (existing) return existing
+ if (existing) {
+ if (!sameResourceScope(scope, resourceScopeFromOwner(existing)))
+ throw new Error('MCP OAuth client belongs to another scope')
+ return existing
+ }
const id = generateId()
try {
@@ -83,11 +95,11 @@ export async function getOrCreateOauthRow(params: {
id,
mcpServerId: params.mcpServerId,
userId: params.userId ?? null,
- workspaceId: params.workspaceId,
+ ...resourceScopeColumns(scope),
})
} catch (error) {
const winner = await loadOauthRow(params)
- if (winner) return winner
+ if (winner && sameResourceScope(scope, resourceScopeFromOwner(winner))) return winner
throw error
}
@@ -95,7 +107,7 @@ export async function getOrCreateOauthRow(params: {
id,
mcpServerId: params.mcpServerId,
userId: params.userId ?? null,
- workspaceId: params.workspaceId,
+ ...resourceScopeColumns(scope),
clientInformation: null,
tokens: null,
codeVerifier: null,
@@ -113,6 +125,7 @@ async function mapOauthRow(row: RawOauthRow): Promise {
mcpServerId: row.mcpServerId,
userId: row.userId,
workspaceId: row.workspaceId,
+ organizationId: row.organizationId,
clientInformation: row.clientInformation
? await safeDecrypt(
row.id,
diff --git a/apps/sim/lib/mcp/service-pool.test.ts b/apps/sim/lib/mcp/service-pool.test.ts
index 17125bbd3a6..e9a7006d8ff 100644
--- a/apps/sim/lib/mcp/service-pool.test.ts
+++ b/apps/sim/lib/mcp/service-pool.test.ts
@@ -149,7 +149,7 @@ describe('McpService connection reuse wiring', () => {
mcpService.executeManagedMcpTool({
connectionId: 'disabled-grant',
serverId: SERVER_ROW.id,
- workspaceId: WORKSPACE_ID,
+ scope: { kind: 'workspace', workspaceId: WORKSPACE_ID },
toolCall: { name: 'slow', arguments: {} },
loadAuthProvider: vi.fn().mockRejectedValue(error),
})
@@ -165,7 +165,7 @@ describe('McpService connection reuse wiring', () => {
await mcpService.executeManagedMcpTool({
connectionId: 'personal-grant',
serverId: SERVER_ROW.id,
- workspaceId: WORKSPACE_ID,
+ scope: { kind: 'workspace', workspaceId: WORKSPACE_ID },
toolCall: { name: 'slow', arguments: {} },
loadAuthProvider,
signal,
diff --git a/apps/sim/lib/mcp/service.ts b/apps/sim/lib/mcp/service.ts
index f2c27a5bf78..74ce71f6d66 100644
--- a/apps/sim/lib/mcp/service.ts
+++ b/apps/sim/lib/mcp/service.ts
@@ -12,6 +12,8 @@ import { interruptibleSleep } from '@sim/utils/helpers'
import { backoffWithJitter } from '@sim/utils/retry'
import { truncate } from '@sim/utils/string'
import { and, eq, isNull, lte, or, sql } from 'drizzle-orm'
+import { type ResourceScope, resourceScopeFields } from '@/lib/core/resource-scope'
+import { resourceScopeCondition } from '@/lib/core/resource-scope.server'
import { generateRequestId } from '@/lib/core/utils/request'
import { McpClient } from '@/lib/mcp/client'
import { mcpConnectionManager } from '@/lib/mcp/connection-manager'
@@ -326,15 +328,17 @@ class McpService {
private async getServerConfig(
serverId: string,
- workspaceId: string
+ scopeInput: string | ResourceScope
): Promise {
+ const scope: ResourceScope =
+ typeof scopeInput === 'string' ? { kind: 'workspace', workspaceId: scopeInput } : scopeInput
const [server] = await db
.select()
.from(mcpServers)
.where(
and(
eq(mcpServers.id, serverId),
- eq(mcpServers.workspaceId, workspaceId),
+ resourceScopeCondition(mcpServers, scope),
eq(mcpServers.enabled, true),
isNull(mcpServers.deletedAt)
)
@@ -356,7 +360,7 @@ class McpService {
transport: 'streamable-http' as const,
url: server.url || undefined,
authType: (server.authType as McpServerConfig['authType']) ?? 'headers',
- workspaceId: server.workspaceId,
+ ...resourceScopeFields(scope),
headers: (server.headers as Record) || {},
timeout: server.timeout || 30000,
retries: server.retries || 3,
@@ -386,7 +390,7 @@ class McpService {
transport: server.transport as McpTransport,
url: server.url || undefined,
authType: (server.authType as McpServerConfig['authType']) ?? 'headers',
- workspaceId: server.workspaceId,
+ workspaceId,
headers: (server.headers as Record) || {},
timeout: server.timeout || 30000,
retries: server.retries || 3,
@@ -490,12 +494,12 @@ class McpService {
async discoverManagedMcpTools(
serverId: string,
- workspaceId: string,
+ scope: string | ResourceScope,
auth: OAuthClientProvider | McpOauthCredentials,
signal?: AbortSignal,
options: { requireComplete?: boolean } = {}
): Promise {
- const config = await this.getServerConfig(serverId, workspaceId)
+ const config = await this.getServerConfig(serverId, scope)
if (!config) throw new Error('Managed MCP server is unavailable')
return this.withServerClient(
{ key: '', serverId, allowPool: false },
@@ -510,14 +514,14 @@ class McpService {
async executeManagedMcpTool(params: {
connectionId: string
serverId: string
- workspaceId: string
+ scope: ResourceScope
toolCall: McpToolCall
loadAuthProvider: () => Promise
extraHeaders?: Record
signal?: AbortSignal
timeoutMs?: number
}): Promise {
- const config = await this.getServerConfig(params.serverId, params.workspaceId)
+ const config = await this.getServerConfig(params.serverId, params.scope)
if (!config) throw new Error('Managed MCP server is unavailable')
const effectiveConfig = params.extraHeaders
? { ...config, headers: { ...config.headers, ...params.extraHeaders } }
diff --git a/apps/sim/lib/mcp/types.ts b/apps/sim/lib/mcp/types.ts
index d2341ca8118..e2675394b61 100644
--- a/apps/sim/lib/mcp/types.ts
+++ b/apps/sim/lib/mcp/types.ts
@@ -25,6 +25,7 @@ export interface McpServerConfig {
*/
userId?: string
workspaceId?: string
+ organizationId?: string
headers?: Record
timeout?: number
retries?: number
diff --git a/apps/sim/lib/navigation/paths.ts b/apps/sim/lib/navigation/paths.ts
index 841fa7bb250..b11070e5e5d 100644
--- a/apps/sim/lib/navigation/paths.ts
+++ b/apps/sim/lib/navigation/paths.ts
@@ -19,6 +19,9 @@ export const APP_ENTRY_PATH = '/home'
*/
export const WORKSPACES_PATH = '/workspace'
+/** Opens full settings in the viewer's most recent accessible workspace. */
+export const WORKSPACE_SETTINGS_PATH = `${WORKSPACES_PATH}?redirect=settings`
+
/** Root of the organization surface; `/o` alone resolves like {@link APP_ENTRY_PATH}. */
const ORGANIZATIONS_PATH = '/o'
diff --git a/apps/sim/lib/navigation/resolve-app-entry.test.ts b/apps/sim/lib/navigation/resolve-app-entry.test.ts
index 0936817d91c..c34fd1f27f6 100644
--- a/apps/sim/lib/navigation/resolve-app-entry.test.ts
+++ b/apps/sim/lib/navigation/resolve-app-entry.test.ts
@@ -3,8 +3,13 @@
*/
import { beforeEach, describe, expect, it, vi } from 'vitest'
-const { mockResolveOrganizationLanding } = vi.hoisted(() => ({
+const { mockResolveOrganizationLanding, mockSearchAvailable } = vi.hoisted(() => ({
mockResolveOrganizationLanding: vi.fn(),
+ mockSearchAvailable: vi.fn(),
+}))
+
+vi.mock('@/lib/knowledge/access/availability', () => ({
+ isKnowledgeMemberAccessAvailable: mockSearchAvailable,
}))
vi.mock('@/lib/organizations/surface', () => ({
@@ -16,6 +21,7 @@ import { resolveAppEntryPath } from '@/lib/navigation/resolve-app-entry'
describe('resolveAppEntryPath', () => {
beforeEach(() => {
vi.clearAllMocks()
+ mockSearchAvailable.mockResolvedValue(true)
})
it('lands an organization member on that organization home', async () => {
@@ -28,6 +34,15 @@ describe('resolveAppEntryPath', () => {
})
).resolves.toBe('/o/org-2/home')
expect(mockResolveOrganizationLanding).toHaveBeenCalledWith('viewer', 'org-2')
+ expect(mockSearchAvailable).toHaveBeenCalledWith({ organizationId: 'org-2' })
+ })
+
+ it('opens full workspace settings when Search is disabled', async () => {
+ mockResolveOrganizationLanding.mockResolvedValue('org-2')
+ mockSearchAvailable.mockResolvedValue(false)
+ await expect(resolveAppEntryPath({ user: { id: 'viewer' } })).resolves.toBe(
+ '/workspace?redirect=settings'
+ )
})
it('lands a viewer with no organization on the workspace picker', async () => {
@@ -35,5 +50,6 @@ describe('resolveAppEntryPath', () => {
await expect(resolveAppEntryPath({ user: { id: 'viewer' } })).resolves.toBe('/workspace')
expect(mockResolveOrganizationLanding).toHaveBeenCalledWith('viewer', null)
+ expect(mockSearchAvailable).not.toHaveBeenCalled()
})
})
diff --git a/apps/sim/lib/navigation/resolve-app-entry.ts b/apps/sim/lib/navigation/resolve-app-entry.ts
index b71dda9168b..afa62a109ca 100644
--- a/apps/sim/lib/navigation/resolve-app-entry.ts
+++ b/apps/sim/lib/navigation/resolve-app-entry.ts
@@ -1,5 +1,10 @@
import { getActiveOrganizationId } from '@/lib/auth/session-response'
-import { organizationRoutes, WORKSPACES_PATH } from '@/lib/navigation/paths'
+import { isKnowledgeMemberAccessAvailable } from '@/lib/knowledge/access/availability'
+import {
+ organizationRoutes,
+ WORKSPACE_SETTINGS_PATH,
+ WORKSPACES_PATH,
+} from '@/lib/navigation/paths'
import { resolveOrganizationLanding } from '@/lib/organizations/surface'
interface EntrySession {
@@ -7,14 +12,17 @@ interface EntrySession {
}
/**
- * Where an authenticated viewer lands by default: the home of their organization
- * (the session's active one when they belong to it, otherwise their first), or the
- * workspace picker when they belong to no organization.
+ * Routes organization members to Home when Search is enabled and workspace settings otherwise.
+ * Viewers without an organization land on the workspace picker.
*/
export async function resolveAppEntryPath(session: EntrySession): Promise {
const organizationId = await resolveOrganizationLanding(
session.user.id,
getActiveOrganizationId(session)
)
- return organizationId ? organizationRoutes(organizationId).home : WORKSPACES_PATH
+ if (!organizationId) return WORKSPACES_PATH
+ const routes = organizationRoutes(organizationId)
+ return (await isKnowledgeMemberAccessAvailable({ organizationId }))
+ ? routes.home
+ : WORKSPACE_SETTINGS_PATH
}
diff --git a/apps/sim/lib/organizations/surface.test.ts b/apps/sim/lib/organizations/surface.test.ts
index daf1c12efcd..e30cfbd2d0f 100644
--- a/apps/sim/lib/organizations/surface.test.ts
+++ b/apps/sim/lib/organizations/surface.test.ts
@@ -10,6 +10,10 @@ const { mockSearchAccess, mockPermissionConfig, featureFlags } = vi.hoisted(() =
mockPermissionConfig: vi.fn(),
featureFlags: { invitationsDisabled: false },
}))
+vi.mock('@/lib/credential-groups/scoped-availability', () => ({
+ isScopedCredentialGroupsAvailable: vi.fn().mockResolvedValue(true),
+}))
+
vi.mock('@/lib/permission-groups/resolve.server', () => ({
getUserPermissionConfigForOrganization: mockPermissionConfig,
}))
@@ -60,6 +64,7 @@ describe('getOrganizationSurfaceContext', () => {
canInviteMembers: true,
canUsePersonalApiKeys: true,
},
+ connectedAccountsAvailable: true,
searchAccess: { memberScoped: true, sourceMirrored: false },
})
expect(mockSearchAccess).toHaveBeenCalledWith({ organizationId: 'org-1' })
diff --git a/apps/sim/lib/organizations/surface.ts b/apps/sim/lib/organizations/surface.ts
index 7f662bd0fec..6b5f95e8495 100644
--- a/apps/sim/lib/organizations/surface.ts
+++ b/apps/sim/lib/organizations/surface.ts
@@ -4,6 +4,7 @@ import { member, organization } from '@sim/db/schema'
import { asc, count, eq } from 'drizzle-orm'
import type { OrganizationRole } from '@/lib/api/contracts/primitives'
import { isInvitationsDisabled } from '@/lib/core/config/env-flags'
+import { isScopedCredentialGroupsAvailable } from '@/lib/credential-groups/scoped-availability'
import {
type KnowledgeAccessAvailability,
resolveKnowledgeAccessAvailability,
@@ -36,6 +37,7 @@ interface OrganizationSurfaceViewer {
export interface OrganizationSurfaceContext {
organization: OrganizationSurfaceOrganization
viewer: OrganizationSurfaceViewer
+ connectedAccountsAvailable: boolean
searchAccess: KnowledgeAccessAvailability
}
@@ -87,6 +89,10 @@ async function resolveOrganizationSurfaceContext(
!capabilityDeniedBy('personal_api_key.use', config) &&
!capabilityDeniedBy('api_keys.manage', config),
},
+ connectedAccountsAvailable: await isScopedCredentialGroupsAvailable({
+ kind: 'organization',
+ organizationId,
+ }),
searchAccess: await resolveKnowledgeAccessAvailability({ organizationId }),
}
}
diff --git a/apps/sim/lib/resource-policies/principals/registry.ts b/apps/sim/lib/resource-policies/principals/registry.ts
index 923490c8bf1..ff737830218 100644
--- a/apps/sim/lib/resource-policies/principals/registry.ts
+++ b/apps/sim/lib/resource-policies/principals/registry.ts
@@ -7,11 +7,13 @@ import type {
ResourcePolicyPrincipalType,
} from '@/lib/resource-policies/principals/types'
import { workflowResourcePolicyPrincipalDefinition } from '@/lib/resource-policies/principals/workflow'
+import { workspaceResourcePolicyPrincipalDefinition } from '@/lib/resource-policies/principals/workspace'
export const RESOURCE_POLICY_PRINCIPAL_DEFINITIONS = Object.freeze({
credential_group_actor: credentialGroupActorResourcePolicyPrincipalDefinition,
knowledge_connector: knowledgeConnectorResourcePolicyPrincipalDefinition,
workflow: workflowResourcePolicyPrincipalDefinition,
+ workspace: workspaceResourcePolicyPrincipalDefinition,
} as const satisfies Record)
export function getResourcePolicyPrincipalDefinition(
diff --git a/apps/sim/lib/resource-policies/principals/types.ts b/apps/sim/lib/resource-policies/principals/types.ts
index eb68f056736..3addc7b6e66 100644
--- a/apps/sim/lib/resource-policies/principals/types.ts
+++ b/apps/sim/lib/resource-policies/principals/types.ts
@@ -5,6 +5,11 @@ export interface WorkflowResourcePolicyPrincipal {
workflowId: string
}
+export interface WorkspaceResourcePolicyPrincipal {
+ type: 'workspace'
+ workspaceId: string
+}
+
export interface CredentialGroupActorResourcePolicyPrincipal {
type: 'credential_group_actor'
}
@@ -15,12 +20,14 @@ export interface KnowledgeConnectorResourcePolicyPrincipal {
}
export type ResourcePolicyPrincipal =
+ | WorkspaceResourcePolicyPrincipal
| WorkflowResourcePolicyPrincipal
| CredentialGroupActorResourcePolicyPrincipal
| KnowledgeConnectorResourcePolicyPrincipal
export type ResourcePolicyPrincipalType = ResourcePolicyPrincipal['type']
export interface ResourcePolicyPrincipalEvaluationFacts {
+ currentWorkspaceId?: string
credentialGroupActorEnrollmentId?: string
currentWorkflow?: {
workflowId: string
@@ -32,7 +39,7 @@ export interface ResourcePolicyPrincipalEvaluationFacts {
}
export type ResourcePolicyPrincipalSelector =
- | { type: 'catalog'; catalog: 'workflows' }
+ | { type: 'catalog'; catalog: 'workflows' | 'workspaces' }
| { type: 'internal' }
export interface ResourcePolicyPrincipalDefinition<
diff --git a/apps/sim/lib/resource-policies/principals/workspace.ts b/apps/sim/lib/resource-policies/principals/workspace.ts
new file mode 100644
index 00000000000..46e1c3043db
--- /dev/null
+++ b/apps/sim/lib/resource-policies/principals/workspace.ts
@@ -0,0 +1,23 @@
+import { z } from 'zod'
+import { defineResourcePolicyPrincipal } from '@/lib/resource-policies/principals/types'
+
+export const workspaceResourcePolicyPrincipalSchema = z
+ .object({
+ type: z.literal('workspace'),
+ workspaceId: z
+ .string()
+ .min(1)
+ .max(128)
+ .refine((id) => id === id.trim(), {
+ message: 'Workspace ID must be canonical',
+ }),
+ })
+ .strict()
+
+export const workspaceResourcePolicyPrincipalDefinition = defineResourcePolicyPrincipal({
+ type: 'workspace',
+ schema: workspaceResourcePolicyPrincipalSchema,
+ label: 'Workspace',
+ selector: { type: 'catalog', catalog: 'workspaces' },
+ matches: (principal, facts) => principal.workspaceId === facts.currentWorkspaceId,
+})
diff --git a/apps/sim/lib/resource-policies/registry.ts b/apps/sim/lib/resource-policies/registry.ts
index 2ab5f068614..25070bcd4ab 100644
--- a/apps/sim/lib/resource-policies/registry.ts
+++ b/apps/sim/lib/resource-policies/registry.ts
@@ -17,7 +17,7 @@ interface ResourcePolicyResourceDefinition {
export const RESOURCE_POLICY_DEFINITIONS = Object.freeze({
credential_group: {
actions: RESOURCE_POLICY_ACTIONS,
- principalTypes: ['credential_group_actor', 'knowledge_connector', 'workflow'],
+ principalTypes: ['credential_group_actor', 'knowledge_connector', 'workflow', 'workspace'],
conditionKeys: [
'credential_group:ActorOwnsCredential',
'credential_group:OptionId',
diff --git a/apps/sim/lib/resource-policies/repository.ts b/apps/sim/lib/resource-policies/repository.ts
index 511edcf8368..e67bfbba3f1 100644
--- a/apps/sim/lib/resource-policies/repository.ts
+++ b/apps/sim/lib/resource-policies/repository.ts
@@ -1,6 +1,8 @@
import { db } from '@sim/db'
import { resourcePolicy } from '@sim/db/schema'
import { and, eq } from 'drizzle-orm'
+import { resourceScopeFromOwner } from '@/lib/core/resource-scope'
+import { resourceScopeCondition } from '@/lib/core/resource-scope.server'
import type { DbOrTx } from '@/lib/db/types'
import type {
ResourcePolicyCodec,
@@ -14,7 +16,8 @@ export interface StoredResourcePolicy<
Document extends ResourcePolicyDocument,
> {
id: string
- workspaceId: string
+ workspaceId: string | null
+ organizationId: string | null
revision: number
document: Document
createdAt: Date
@@ -67,7 +70,7 @@ async function loadResourcePolicyWithExecutor<
.from(resourcePolicy)
.where(
and(
- eq(resourcePolicy.workspaceId, input.workspaceId),
+ resourceScopeCondition(resourcePolicy, resourceScopeFromOwner(input)),
eq(resourcePolicy.resourceType, input.resourceType),
eq(resourcePolicy.resourceId, input.resourceId)
)
@@ -79,6 +82,7 @@ async function loadResourcePolicyWithExecutor<
return {
id: row.id,
workspaceId: row.workspaceId,
+ organizationId: row.organizationId,
revision: row.revision,
document: input.codec.parse(row.document, {
type: input.resourceType,
@@ -142,6 +146,7 @@ export async function writeResourcePolicy<
return {
id: updated.id,
workspaceId: updated.workspaceId,
+ organizationId: updated.organizationId,
revision: updated.revision,
document,
createdAt: updated.createdAt,
@@ -157,7 +162,7 @@ export async function deleteResourcePolicyForResource<
.delete(resourcePolicy)
.where(
and(
- eq(resourcePolicy.workspaceId, input.workspaceId),
+ resourceScopeCondition(resourcePolicy, resourceScopeFromOwner(input)),
eq(resourcePolicy.resourceType, input.resourceType),
eq(resourcePolicy.resourceId, input.resourceId)
)
diff --git a/apps/sim/lib/resource-policies/types.ts b/apps/sim/lib/resource-policies/types.ts
index 7daed5be6ae..16fbfa99f76 100644
--- a/apps/sim/lib/resource-policies/types.ts
+++ b/apps/sim/lib/resource-policies/types.ts
@@ -29,8 +29,10 @@ export interface ResourcePolicyStatement {
condition?: ResourcePolicyCondition
}
-export interface ResourcePolicyTarget {
- workspaceId: string
+export type ResourcePolicyTarget = (
+ | { workspaceId: string; organizationId?: never }
+ | { organizationId: string; workspaceId?: never }
+) & {
resourceType: ResourceType
resourceId: string
}
diff --git a/apps/sim/lib/selectors/manifest.test.ts b/apps/sim/lib/selectors/manifest.test.ts
index df9322d33e3..599b78e1866 100644
--- a/apps/sim/lib/selectors/manifest.test.ts
+++ b/apps/sim/lib/selectors/manifest.test.ts
@@ -9,9 +9,9 @@ describe('selector manifest', () => {
const count = (classification: (typeof classifications)[number]) =>
classifications.filter((value) => value === classification).length
- expect(Object.keys(selectorManifest)).toHaveLength(94)
+ expect(Object.keys(selectorManifest)).toHaveLength(95)
expect(count('provider-server')).toBe(82)
- expect(count('internal-server')).toBe(11)
+ expect(count('internal-server')).toBe(12)
expect(count('local')).toBe(1)
expect(classifications).not.toContain('provider-legacy')
})
diff --git a/apps/sim/lib/selectors/manifest.ts b/apps/sim/lib/selectors/manifest.ts
index 26634cdebfe..fbafd6df703 100644
--- a/apps/sim/lib/selectors/manifest.ts
+++ b/apps/sim/lib/selectors/manifest.ts
@@ -368,6 +368,7 @@ export const selectorManifest = {
staleTime: 0,
}),
'workspace.credentialProviders': internalSelector([], { detail: true }),
+ 'workspace.organizationMcpProviders': internalSelector([], { detail: true }),
'workspace.credentialGroupProviders': internalSelector([], {
detail: true,
}),
diff --git a/apps/sim/lib/selectors/server/internal.test.ts b/apps/sim/lib/selectors/server/internal.test.ts
index a779c18b928..3ab88055c18 100644
--- a/apps/sim/lib/selectors/server/internal.test.ts
+++ b/apps/sim/lib/selectors/server/internal.test.ts
@@ -6,10 +6,10 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
const mockListWorkflows = vi.hoisted(() => vi.fn())
const mockFetchOpenRouterEmbeddingModelCatalog = vi.hoisted(() => vi.fn())
-const mockGetWorkspaceAccountsSettings = vi.hoisted(() => vi.fn())
+const mockGetWorkspaceOrganizationAccounts = vi.hoisted(() => vi.fn())
-vi.mock('@/lib/credential-groups/application/manage-groups', () => ({
- getWorkspaceAccountsSettings: { execute: mockGetWorkspaceAccountsSettings },
+vi.mock('@/lib/credential-groups/application/workspace-organization-accounts', () => ({
+ getWorkspaceOrganizationAccounts: { execute: mockGetWorkspaceOrganizationAccounts },
}))
vi.mock('@/lib/workflows/application/list-workflows', () => ({
@@ -39,42 +39,46 @@ function workflowArgs(): ExecuteServerSelectorArgs {
}
}
-describe('workspace.credentialGroupProviders selector', () => {
+describe.each([
+ {
+ key: 'workspace.credentialGroupProviders',
+ field: 'providers',
+ option: { id: 'google-email', label: 'Gmail' },
+ },
+ {
+ key: 'workspace.organizationMcpProviders',
+ field: 'mcpProviders',
+ option: { id: 'fireflies', label: 'Fireflies' },
+ },
+] as const)('$key selector', ({ key, field, option }) => {
beforeEach(() => vi.clearAllMocks())
-
- it('uses active options from the workspace container without a group selection', async () => {
- mockGetWorkspaceAccountsSettings.mockResolvedValue({
- credentialGroup: {
- options: [
- { provider: 'gmail', status: 'active' },
- { provider: 'slack', status: 'disabled' },
- ],
- },
- })
- const args: ExecuteServerSelectorArgs = {
- ...workflowArgs(),
- selectorKey: 'workspace.credentialGroupProviders',
- }
- await expect(
- internalSelectorAttachments['workspace.credentialGroupProviders'].execute(args)
- ).resolves.toEqual({
+ it('uses the authorized organization provider projection', async () => {
+ mockGetWorkspaceOrganizationAccounts.mockResolvedValue({ allowed: true, [field]: [option] })
+ const args: ExecuteServerSelectorArgs = { ...workflowArgs(), selectorKey: key }
+ await expect(internalSelectorAttachments[key].execute(args)).resolves.toEqual({
kind: 'list',
- items: [{ id: 'google-email', label: 'Gmail' }],
+ items: [option],
})
- expect(mockGetWorkspaceAccountsSettings).toHaveBeenCalledWith({
+ expect(mockGetWorkspaceOrganizationAccounts).toHaveBeenCalledWith({
principal: args.principal,
input: { workspaceId: 'workspace-1' },
})
})
-
- it('returns an empty list when the workspace has no container', async () => {
- mockGetWorkspaceAccountsSettings.mockResolvedValue({ credentialGroup: null })
+ it('refuses providers when workspace access is not granted', async () => {
+ mockGetWorkspaceOrganizationAccounts.mockResolvedValue({ allowed: false, [field]: [option] })
+ await expect(
+ internalSelectorAttachments[key].execute({ ...workflowArgs(), selectorKey: key })
+ ).rejects.toBeInstanceOf(SelectorOptionsUnavailableError)
+ })
+ it('resolves a selected provider by ID', async () => {
+ mockGetWorkspaceOrganizationAccounts.mockResolvedValue({ allowed: true, [field]: [option] })
await expect(
- internalSelectorAttachments['workspace.credentialGroupProviders'].execute({
+ internalSelectorAttachments[key].execute({
...workflowArgs(),
- selectorKey: 'workspace.credentialGroupProviders',
+ selectorKey: key,
+ request: { kind: 'detail', id: option.id },
})
- ).resolves.toEqual({ kind: 'list', items: [] })
+ ).resolves.toEqual({ kind: 'detail', item: option })
})
})
diff --git a/apps/sim/lib/selectors/server/internal.ts b/apps/sim/lib/selectors/server/internal.ts
index be5970fe957..c7f7fde32c9 100644
--- a/apps/sim/lib/selectors/server/internal.ts
+++ b/apps/sim/lib/selectors/server/internal.ts
@@ -1,5 +1,4 @@
-import { getWorkspaceAccountsSettings } from '@/lib/credential-groups/application/manage-groups'
-import { getCredentialGroupProviderService } from '@/lib/credential-groups/providers'
+import { getWorkspaceOrganizationAccounts } from '@/lib/credential-groups/application/workspace-organization-accounts'
import { listInternalCredentials } from '@/lib/credentials/application/credential-crud'
import { fetchOllamaEmbeddingModelCatalog } from '@/lib/embeddings/ollama-model-catalog.server'
import { fetchOpenRouterEmbeddingModelCatalog } from '@/lib/embeddings/openrouter-model-catalog.server'
@@ -214,20 +213,32 @@ export const internalSelectorAttachments = {
destination: 'fixed',
async execute(args: ExecuteServerSelectorArgs) {
if (!args.workspaceId) throw new SelectorContextUnavailableError()
- const { credentialGroup: group } = await getWorkspaceAccountsSettings.execute({
+ const result = await getWorkspaceOrganizationAccounts.execute({
principal: args.principal,
input: { workspaceId: args.workspaceId },
})
- const options = (group?.options ?? [])
- .filter((option) => option.status === 'active')
- .map((option) => {
- const service = getCredentialGroupProviderService(option.provider)
- return { id: service.providerId, label: service.name }
- })
- .sort((left, right) => left.label.localeCompare(right.label))
+ if (!result.allowed) throw new SelectorOptionsUnavailableError()
+ const options = result.providers
if (args.request.kind === 'detail') {
- const detailId = args.request.id
- return detailSelectorResult(options.find((option) => option.id === detailId) ?? null)
+ const id = args.request.id
+ return detailSelectorResult(options.find((option) => option.id === id) ?? null)
+ }
+ return listSelectorResult(options)
+ },
+ },
+ 'workspace.organizationMcpProviders': {
+ destination: 'fixed',
+ async execute(args: ExecuteServerSelectorArgs) {
+ if (!args.workspaceId) throw new SelectorContextUnavailableError()
+ const result = await getWorkspaceOrganizationAccounts.execute({
+ principal: args.principal,
+ input: { workspaceId: args.workspaceId },
+ })
+ if (!result.allowed) throw new SelectorOptionsUnavailableError()
+ const options = result.mcpProviders
+ if (args.request.kind === 'detail') {
+ const id = args.request.id
+ return detailSelectorResult(options.find((option) => option.id === id) ?? null)
}
return listSelectorResult(options)
},
diff --git a/apps/sim/lib/settings/application/organization-section-access.test.ts b/apps/sim/lib/settings/application/organization-section-access.test.ts
index ec7d4931d62..0e81e192caa 100644
--- a/apps/sim/lib/settings/application/organization-section-access.test.ts
+++ b/apps/sim/lib/settings/application/organization-section-access.test.ts
@@ -4,7 +4,18 @@
import { resetEnvFlagsMock, setEnvFlags } from '@sim/testing'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
-const mocks = vi.hoisted(() => ({ canOpen: vi.fn(), enterprise: vi.fn() }))
+const mocks = vi.hoisted(() => ({
+ canOpen: vi.fn(),
+ enterprise: vi.fn(),
+ groups: vi.fn(),
+ search: vi.fn(),
+}))
+vi.mock('@/lib/credential-groups/scoped-availability', () => ({
+ isScopedCredentialGroupsAvailable: mocks.groups,
+}))
+vi.mock('@/lib/knowledge/access/availability', () => ({
+ isKnowledgeMemberAccessAvailable: mocks.search,
+}))
vi.mock('@/lib/organizations/settings-access', () => ({
canOpenOrganizationSettingsSection: mocks.canOpen,
}))
@@ -20,9 +31,78 @@ describe('organization settings authorization', () => {
setEnvFlags({ isHosted: true, isBillingEnabled: true })
mocks.canOpen.mockResolvedValue(true)
mocks.enterprise.mockResolvedValue(true)
+ mocks.groups.mockResolvedValue(true)
+ mocks.search.mockResolvedValue(true)
})
afterEach(resetEnvFlagsMock)
+ it.each(['connected-accounts', 'search-mcp', 'integrations'] as const)(
+ 'gates direct %s settings links using the target org',
+ async (section) => {
+ const gate = section === 'connected-accounts' ? mocks.groups : mocks.search
+ gate.mockResolvedValue(false)
+ await expect(
+ authorizeOrganizationSettingsSection({
+ organizationId: 'target',
+ userId: 'viewer',
+ section,
+ })
+ ).resolves.toBe(false)
+ expect(gate).toHaveBeenCalledExactlyOnceWith(
+ section === 'connected-accounts'
+ ? { kind: 'organization', organizationId: 'target' }
+ : { organizationId: 'target' }
+ )
+ expect(mocks.enterprise).not.toHaveBeenCalled()
+ }
+ )
+
+ it.each([
+ { groups: false, search: false, connectedAccounts: false, integrations: false },
+ { groups: true, search: false, connectedAccounts: true, integrations: false },
+ { groups: true, search: true, connectedAccounts: false, integrations: true },
+ ])(
+ 'selects the setup page with groups=$groups and search=$search',
+ async ({ groups, search, connectedAccounts, integrations }) => {
+ mocks.groups.mockResolvedValue(groups)
+ mocks.search.mockResolvedValue(search)
+ const input = { organizationId: 'target', userId: 'admin' }
+ await expect(
+ authorizeOrganizationSettingsSection({ ...input, section: 'connected-accounts' })
+ ).resolves.toBe(connectedAccounts)
+ await expect(
+ authorizeOrganizationSettingsSection({ ...input, section: 'integrations' })
+ ).resolves.toBe(integrations)
+ }
+ )
+
+ it.each(['connected-accounts', 'integrations'] as const)(
+ 'checks role access before selecting the %s UI',
+ async (section) => {
+ mocks.canOpen.mockResolvedValue(false)
+ await expect(
+ authorizeOrganizationSettingsSection({
+ organizationId: 'target',
+ userId: 'member',
+ section,
+ })
+ ).resolves.toBe(false)
+ expect(mocks.groups).not.toHaveBeenCalled()
+ expect(mocks.search).not.toHaveBeenCalled()
+ }
+ )
+
+ it('propagates Search availability failures instead of selecting the old UI', async () => {
+ mocks.search.mockRejectedValue(new Error('Feature configuration unavailable'))
+ await expect(
+ authorizeOrganizationSettingsSection({
+ organizationId: 'target',
+ userId: 'admin',
+ section: 'connected-accounts',
+ })
+ ).rejects.toThrow('Feature configuration unavailable')
+ })
+
it('checks current target organization membership before billing reads', async () => {
mocks.canOpen.mockResolvedValue(false)
expect(
@@ -58,25 +138,6 @@ describe('organization settings authorization', () => {
).toBe(false)
})
- it('gates Sim Search source setup on the enterprise plan when hosted', async () => {
- mocks.enterprise.mockResolvedValue(false)
- expect(
- await authorizeOrganizationSettingsSection({
- organizationId: 'target',
- userId: 'admin',
- section: 'integrations',
- })
- ).toBe(false)
- mocks.enterprise.mockResolvedValue(true)
- expect(
- await authorizeOrganizationSettingsSection({
- organizationId: 'target',
- userId: 'admin',
- section: 'integrations',
- })
- ).toBe(true)
- })
-
it('does not turn authorization infrastructure failures into empty settings', async () => {
mocks.canOpen.mockRejectedValue(new Error('Membership database unavailable'))
await expect(
diff --git a/apps/sim/lib/settings/application/organization-section-access.ts b/apps/sim/lib/settings/application/organization-section-access.ts
index ab13f44c8af..968872e0207 100644
--- a/apps/sim/lib/settings/application/organization-section-access.ts
+++ b/apps/sim/lib/settings/application/organization-section-access.ts
@@ -5,6 +5,8 @@ import {
} from '@/components/settings/navigation'
import { isOrganizationOnEnterprisePlan } from '@/lib/billing/core/subscription'
import { getDeploymentShape } from '@/lib/core/config/deployment-shape'
+import { isScopedCredentialGroupsAvailable } from '@/lib/credential-groups/scoped-availability'
+import { isKnowledgeMemberAccessAvailable } from '@/lib/knowledge/access/availability'
import { canOpenOrganizationSettingsSection } from '@/lib/organizations/settings-access'
interface AuthorizeOrganizationSettingsSectionInput {
@@ -21,9 +23,16 @@ export async function authorizeOrganizationSettingsSection({
}: AuthorizeOrganizationSettingsSectionInput): Promise {
if (!(await canOpenOrganizationSettingsSection(organizationId, userId, section))) return false
+ if (section === 'connected-accounts') {
+ if (!(await isScopedCredentialGroupsAvailable({ kind: 'organization', organizationId })))
+ return false
+ return !(await isKnowledgeMemberAccessAvailable({ organizationId }))
+ }
+ if (section === 'search-mcp' || section === 'integrations')
+ return isKnowledgeMemberAccessAvailable({ organizationId })
+
const deployment = getDeploymentShape()
- const needsEnterprisePlan =
- deployment.hosted && section !== 'members' && section !== 'billing' && section !== 'search-mcp'
+ const needsEnterprisePlan = deployment.hosted && section !== 'members' && section !== 'billing'
const hasEnterprisePlan = needsEnterprisePlan
? await isOrganizationOnEnterprisePlan(organizationId)
: false
diff --git a/apps/sim/lib/settings/application/workspace-section-access.test.ts b/apps/sim/lib/settings/application/workspace-section-access.test.ts
index 9e94ffb2828..8c2e4f8a05f 100644
--- a/apps/sim/lib/settings/application/workspace-section-access.test.ts
+++ b/apps/sim/lib/settings/application/workspace-section-access.test.ts
@@ -27,8 +27,6 @@ const mocks = vi.hoisted(() => ({
},
},
getOrganizationSettingsFeatures: vi.fn((hasEnterprisePlan: boolean) => ({ hasEnterprisePlan })),
- getWorkspaceOwnerSubscriptionAccess: vi.fn(),
- isCredentialGroupsAvailable: vi.fn(),
isCustomBlocksEligibleForOrganization: vi.fn(),
isForkingAvailableForWorkspace: vi.fn(),
isOrganizationOnEnterprisePlan: vi.fn(),
@@ -49,7 +47,6 @@ vi.mock('@/components/settings/navigation', () => ({
},
UNIFIED_TO_WORKSPACE_SECTION: {
secrets: 'secrets',
- 'credential-groups': 'credential-groups',
forks: 'forks',
'custom-blocks': 'custom-blocks',
},
@@ -57,15 +54,9 @@ vi.mock('@/components/settings/navigation', () => ({
['secrets', 'api-keys', 'inbox', 'mcp', 'custom-tools'].includes(section)
),
}))
-vi.mock('@/lib/billing/core/workspace-access', () => ({
- getWorkspaceOwnerSubscriptionAccess: mocks.getWorkspaceOwnerSubscriptionAccess,
-}))
vi.mock('@/lib/billing/core/subscription', () => ({
isOrganizationOnEnterprisePlan: mocks.isOrganizationOnEnterprisePlan,
}))
-vi.mock('@/lib/credential-groups/availability', () => ({
- isCredentialGroupsAvailable: mocks.isCredentialGroupsAvailable,
-}))
vi.mock('@/lib/core/config/deployment-shape', () => ({
getDeploymentShape: () => mocks.deploymentShape,
}))
@@ -119,8 +110,6 @@ describe('authorizeWorkspaceSettingsSection', () => {
beforeEach(() => {
vi.clearAllMocks()
mocks.checkWorkspaceAccess.mockResolvedValue(PERSONAL_ACCESS)
- mocks.getWorkspaceOwnerSubscriptionAccess.mockResolvedValue({ isEnterprise: true })
- mocks.isCredentialGroupsAvailable.mockResolvedValue(true)
mocks.isCustomBlocksEligibleForOrganization.mockResolvedValue(true)
mocks.isForkingAvailableForWorkspace.mockResolvedValue(true)
mocks.isOrganizationOnEnterprisePlan.mockResolvedValue(true)
@@ -144,13 +133,11 @@ describe('authorizeWorkspaceSettingsSection', () => {
disposition: 'not-found',
})
expect(mocks.canOpenOrganizationSettingsSection).not.toHaveBeenCalled()
- expect(mocks.getWorkspaceOwnerSubscriptionAccess).not.toHaveBeenCalled()
})
it('opens ordinary sections from workspace access alone', async () => {
await expect(authorize('general')).resolves.toEqual({ allowed: true })
- expect(mocks.getWorkspaceOwnerSubscriptionAccess).not.toHaveBeenCalled()
expect(mocks.canOpenOrganizationSettingsSection).not.toHaveBeenCalled()
expect(mocks.resolveVerifiedUserAccessControlContext).not.toHaveBeenCalled()
expect(mocks.isPlatformAdmin).not.toHaveBeenCalled()
@@ -177,7 +164,6 @@ describe('authorizeWorkspaceSettingsSection', () => {
allowed: false,
disposition: 'redirect-general',
})
- expect(mocks.getWorkspaceOwnerSubscriptionAccess).not.toHaveBeenCalled()
expect(mocks.resolveVerifiedUserAccessControlContext).toHaveBeenCalledWith(
'viewer-1',
'workspace-1',
@@ -191,7 +177,6 @@ describe('authorizeWorkspaceSettingsSection', () => {
it('resolves environment access-control policy for the same section in a personal workspace', async () => {
await authorize('secrets')
- expect(mocks.getWorkspaceOwnerSubscriptionAccess).not.toHaveBeenCalled()
expect(mocks.resolveVerifiedUserAccessControlContext).toHaveBeenCalledWith(
'viewer-1',
'workspace-1',
@@ -201,7 +186,6 @@ describe('authorizeWorkspaceSettingsSection', () => {
it('enforces canonical permission config independently of billing subscription state', async () => {
mocks.checkWorkspaceAccess.mockResolvedValue(ORGANIZATION_ACCESS)
- mocks.getWorkspaceOwnerSubscriptionAccess.mockResolvedValue({ isEnterprise: false })
mocks.resolveVerifiedUserAccessControlContext.mockResolvedValue({
entitled: true,
config: { hideSecretsTab: true },
@@ -212,7 +196,6 @@ describe('authorizeWorkspaceSettingsSection', () => {
allowed: false,
disposition: 'redirect-general',
})
- expect(mocks.getWorkspaceOwnerSubscriptionAccess).not.toHaveBeenCalled()
})
it('passes the server-resolved deployment shape to both navigation gates', async () => {
@@ -230,14 +213,7 @@ describe('authorizeWorkspaceSettingsSection', () => {
})
it('resolves the exact entitlement source only for gated workspace sections', async () => {
- mocks.resolveWorkspaceNavigation.mockReturnValue([{ id: 'credential-groups' }])
- await authorize('credential-groups')
- expect(mocks.isCredentialGroupsAvailable).toHaveBeenCalledWith({
- workspaceId: 'workspace-1',
- ownerBilling: { isEnterprise: true },
- })
- expect(mocks.isForkingAvailableForWorkspace).not.toHaveBeenCalled()
-
+ mocks.checkWorkspaceAccess.mockResolvedValue(PERSONAL_ACCESS)
mocks.resolveWorkspaceNavigation.mockReturnValue([{ id: 'forks' }])
await authorize('forks')
expect(mocks.isForkingAvailableForWorkspace).toHaveBeenCalledWith(null, 'viewer-1')
diff --git a/apps/sim/lib/settings/application/workspace-section-access.ts b/apps/sim/lib/settings/application/workspace-section-access.ts
index 8f2194a6e2f..632370d2815 100644
--- a/apps/sim/lib/settings/application/workspace-section-access.ts
+++ b/apps/sim/lib/settings/application/workspace-section-access.ts
@@ -9,9 +9,7 @@ import {
workspaceSectionUsesPermissionConfig,
} from '@/components/settings/navigation'
import { isOrganizationOnEnterprisePlan } from '@/lib/billing/core/subscription'
-import { getWorkspaceOwnerSubscriptionAccess } from '@/lib/billing/core/workspace-access'
import { getDeploymentShape } from '@/lib/core/config/deployment-shape'
-import { isCredentialGroupsAvailable } from '@/lib/credential-groups/availability'
import { canOpenOrganizationSettingsSection } from '@/lib/organizations/settings-access'
import { isPlatformAdmin } from '@/lib/permissions/super-user'
import { isCustomBlocksEligibleForOrganization } from '@/lib/workflows/custom-blocks/operations'
@@ -37,30 +35,21 @@ async function canOpenWorkspaceSection(
},
permission: NonNullable>['permission']>
): Promise {
- const needsOwnerBilling = section === 'credential-groups'
- const ownerBilling = needsOwnerBilling
- ? await getWorkspaceOwnerSubscriptionAccess(input.workspaceId)
- : null
-
- const [accessControl, credentialGroupsAvailable, forksAvailable, customBlocksAvailable] =
- await Promise.all([
- workspaceSectionUsesPermissionConfig(section)
- ? resolveVerifiedUserAccessControlContext(
- input.userId,
- input.workspaceId,
- workspace.organizationId
- )
- : null,
- section === 'credential-groups' && ownerBilling
- ? isCredentialGroupsAvailable({ workspaceId: input.workspaceId, ownerBilling })
- : false,
- section === 'forks'
- ? isForkingAvailableForWorkspace(workspace.organizationId, input.userId)
- : false,
- section === 'custom-blocks' && workspace.organizationId
- ? isCustomBlocksEligibleForOrganization(workspace.organizationId)
- : false,
- ])
+ const [accessControl, forksAvailable, customBlocksAvailable] = await Promise.all([
+ workspaceSectionUsesPermissionConfig(section)
+ ? resolveVerifiedUserAccessControlContext(
+ input.userId,
+ input.workspaceId,
+ workspace.organizationId
+ )
+ : null,
+ section === 'forks'
+ ? isForkingAvailableForWorkspace(workspace.organizationId, input.userId)
+ : false,
+ section === 'custom-blocks' && workspace.organizationId
+ ? isCustomBlocksEligibleForOrganization(workspace.organizationId)
+ : false,
+ ])
const deployment = getDeploymentShape()
const navigation = resolveWorkspaceNavigation({
@@ -68,7 +57,6 @@ async function canOpenWorkspaceSection(
permissionConfig: accessControl?.config ?? {},
deployment,
entitlements: {
- credentialGroups: credentialGroupsAvailable,
inbox: true,
customBlocks: customBlocksAvailable,
forks: forksAvailable,
diff --git a/apps/sim/lib/workspaces/admin-move-source-impact.ts b/apps/sim/lib/workspaces/admin-move-source-impact.ts
index 76a76e44716..e2c41e838bc 100644
--- a/apps/sim/lib/workspaces/admin-move-source-impact.ts
+++ b/apps/sim/lib/workspaces/admin-move-source-impact.ts
@@ -57,6 +57,7 @@ import { getCustomBlockUsageCounts } from '@/lib/workflows/custom-blocks/operati
const ENTERPRISE_GATED_SECTION_LABELS: Record = {
integrations: 'Sim Search source setup',
'search-mcp': null,
+ 'connected-accounts': 'organization connected accounts',
members: null,
billing: null,
usage: 'organization usage monitoring',
diff --git a/apps/sim/lib/workspaces/host-context.test.ts b/apps/sim/lib/workspaces/host-context.test.ts
index db13e2865cf..c118a73b33c 100644
--- a/apps/sim/lib/workspaces/host-context.test.ts
+++ b/apps/sim/lib/workspaces/host-context.test.ts
@@ -13,6 +13,10 @@ const {
mockGetOrganizationSettingsAccess: vi.fn(),
}))
+vi.mock('@/lib/credential-groups/scoped-availability', () => ({
+ isScopedCredentialGroupsAvailable: vi.fn().mockResolvedValue(true),
+}))
+
vi.mock('@/lib/workspaces/permissions/utils', () => ({
checkWorkspaceAccess: mockCheckWorkspaceAccess,
}))
diff --git a/apps/sim/lib/workspaces/host-context.ts b/apps/sim/lib/workspaces/host-context.ts
index 0742770648d..e60716ca9ef 100644
--- a/apps/sim/lib/workspaces/host-context.ts
+++ b/apps/sim/lib/workspaces/host-context.ts
@@ -2,7 +2,7 @@ import { cache } from 'react'
import type { WorkspaceHostContext } from '@/lib/api/contracts/workspaces'
import { getWorkspaceOwnerSubscriptionAccess } from '@/lib/billing/core/workspace-access'
import { resolveDeploymentShape } from '@/lib/core/config/deployment-shape'
-import { isCredentialGroupsAvailable } from '@/lib/credential-groups/availability'
+import { isScopedCredentialGroupsAvailable } from '@/lib/credential-groups/scoped-availability'
import { resolveKnowledgeAccessAvailability } from '@/lib/knowledge/access/availability'
import { getOrganizationSettingsAccess } from '@/lib/organizations/settings-access'
import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
@@ -31,7 +31,12 @@ async function resolveWorkspaceHostContextForViewer(
: Promise.resolve({ role: null, isMember: false, isAdmin: false }),
])
const [credentialGroupsAvailable, knowledgeAccess] = await Promise.all([
- isCredentialGroupsAvailable({ workspaceId, ownerBilling }),
+ hostOrganizationId
+ ? isScopedCredentialGroupsAvailable({
+ kind: 'organization',
+ organizationId: hostOrganizationId,
+ })
+ : Promise.resolve(false),
resolveKnowledgeAccessAvailability({ workspaceId, ownerBilling }),
])
diff --git a/apps/sim/triggers/credential-group/event.ts b/apps/sim/triggers/credential-group/event.ts
index d985a7a92b5..85ddc1e4be3 100644
--- a/apps/sim/triggers/credential-group/event.ts
+++ b/apps/sim/triggers/credential-group/event.ts
@@ -37,7 +37,7 @@ export const credentialGroupEventTrigger: TriggerConfig = {
type: 'text',
defaultValue: [
'Choose whether to trigger on a new credential, a reconnection, or a submitted form',
- 'Grant this workflow access in Connected accounts settings',
+ 'Ask an organization admin to allow this workspace in Connected accounts settings',
'Deploy the workflow to start receiving events',
]
.map(
@@ -60,11 +60,11 @@ export const credentialGroupEventTrigger: TriggerConfig = {
},
credentialGroupId: {
type: 'string',
- description: 'Workspace accounts container ID',
+ description: 'Organization accounts container ID',
},
credentialGroupName: {
type: 'string',
- description: 'Workspace accounts container name',
+ description: 'Organization accounts container name',
},
enrollmentId: {
type: 'string',
@@ -88,6 +88,11 @@ export const credentialGroupEventTrigger: TriggerConfig = {
description: 'Connected account option ID',
condition: { field: 'eventType', value: [...CREDENTIAL_GROUP_CREDENTIAL_EVENT_TYPES] },
},
+ mcpServerId: {
+ type: 'string',
+ description: 'Managed MCP server configuration ID, or null for an OAuth account',
+ condition: { field: 'eventType', value: [...CREDENTIAL_GROUP_CREDENTIAL_EVENT_TYPES] },
+ },
provider: {
type: 'string',
description: 'Account provider',
diff --git a/packages/auth/src/principal.ts b/packages/auth/src/principal.ts
index 077c5797e0f..746a955f748 100644
--- a/packages/auth/src/principal.ts
+++ b/packages/auth/src/principal.ts
@@ -179,6 +179,7 @@ export interface OrganizationDelegatedPrincipal {
/** Bearer identity established by a currently valid Credential Group invitation. */
interface CredentialGroupEnrollmentIdentity {
kind: 'credential_group_enrollment'
+ userId: string
credentialGroupId: string
enrollmentId: string
email: string
diff --git a/packages/db/credential-group-resource-policies.ts b/packages/db/credential-group-resource-policies.ts
index a8be90034e6..28d70c986fd 100644
--- a/packages/db/credential-group-resource-policies.ts
+++ b/packages/db/credential-group-resource-policies.ts
@@ -3,6 +3,7 @@ import type { Sql } from 'postgres'
export const CREDENTIAL_GROUP_POLICY_BATCH_SIZE = 500
export const CREDENTIAL_GROUP_WORKFLOW_ACCESS_LIMIT = 50
export const CREDENTIAL_GROUP_POLICY_DOCUMENT_MAX_BYTES = 32 * 1024
+export const ORGANIZATION_ACCOUNT_POLICY_DOCUMENT_MAX_BYTES = 256 * 1024
const ACTOR_ACCESS_SID = 'CredentialGroupActorCredentialAccess'
const WORKFLOW_ACCESS_SID = 'WorkflowCredentialAccess'
@@ -69,7 +70,8 @@ export interface MissingCredentialGroupPolicyRow {
export interface StoredCredentialGroupPolicyRow {
id: string
- workspaceId: string
+ organizationId?: string | null
+ workspaceId: string | null
resourceId: string
revision: number
documentBytes: number
@@ -353,6 +355,58 @@ export function parseCredentialGroupPolicyDocument(
}
}
+/** Validates the org-only workspace sharing document without importing application code. */
+export function validateOrganizationAccountPolicyDocument(
+ value: unknown,
+ expectedResourceId: string
+): void {
+ const document = requireRecord(value, 'Organization account policy')
+ requireExactKeys(document, ['version', 'resource', 'statements'], 'Organization account policy')
+ if (document.version !== 2) throw new Error('Organization account policy version must be 2')
+ const resource = requireRecord(document.resource, 'Organization account resource')
+ requireExactKeys(resource, ['type', 'id'], 'Organization account resource')
+ if (
+ resource.type !== 'credential_group' ||
+ requireCanonicalId(resource.id, 'Organization account resource ID') !== expectedResourceId
+ )
+ throw new Error('Organization account policy resource does not match its canonical resource')
+ if (!Array.isArray(document.statements) || document.statements.length > 1)
+ throw new Error('Organization account policy supports only workspace access')
+ if (document.statements.length === 0) return
+ const statement = requireRecord(
+ document.statements[0],
+ 'Organization account workspace statement'
+ )
+ requireExactKeys(
+ statement,
+ ['sid', 'effect', 'actions', 'principals'],
+ 'Organization account workspace statement'
+ )
+ if (
+ statement.sid !== 'WorkspaceCredentialAccess' ||
+ statement.effect !== 'allow' ||
+ !Array.isArray(statement.actions) ||
+ statement.actions.length !== 1 ||
+ statement.actions[0] !== CREDENTIAL_USE_ACTION
+ )
+ throw new Error('Organization account workspace statement is invalid')
+ if (
+ !Array.isArray(statement.principals) ||
+ statement.principals.length < 1 ||
+ statement.principals.length > 1000
+ )
+ throw new Error('Organization account policy supports 1-1000 workspaces')
+ let previous = ''
+ for (const value of statement.principals) {
+ const principal = requireRecord(value, 'Organization account workspace principal')
+ requireExactKeys(principal, ['type', 'workspaceId'], 'Organization account workspace principal')
+ const id = requireCanonicalId(principal.workspaceId, 'Organization account workspace ID')
+ if (principal.type !== 'workspace' || id <= previous)
+ throw new Error('Organization account workspace principals must be unique and sorted')
+ previous = id
+ }
+}
+
function assertPage(
rows: T[],
afterId: string,
@@ -414,16 +468,24 @@ export async function reconcileCredentialGroupResourcePolicies(
if (!Number.isInteger(row.revision) || row.revision < 1) {
throw new Error(`Credential Group policy ${row.id} has an invalid revision`)
}
+ const maxBytes = row.organizationId
+ ? ORGANIZATION_ACCOUNT_POLICY_DOCUMENT_MAX_BYTES
+ : CREDENTIAL_GROUP_POLICY_DOCUMENT_MAX_BYTES
if (
!Number.isInteger(row.documentBytes) ||
row.documentBytes < 0 ||
- row.documentBytes > CREDENTIAL_GROUP_POLICY_DOCUMENT_MAX_BYTES
+ row.documentBytes > maxBytes
) {
- throw new Error(
- `Credential Group policy ${row.id} exceeds the ${CREDENTIAL_GROUP_POLICY_DOCUMENT_MAX_BYTES}-byte limit`
- )
+ throw new Error(`Credential Group policy ${row.id} exceeds the ${maxBytes}-byte limit`)
+ }
+ if (row.organizationId) {
+ if (row.workspaceId)
+ throw new Error('Organization account policy cannot also belong to a workspace')
+ validateOrganizationAccountPolicyDocument(row.document, row.resourceId)
+ } else {
+ if (!row.workspaceId) throw new Error('Credential group policy has no owner')
+ parseCredentialGroupPolicyDocument(row.document, row.resourceId)
}
- parseCredentialGroupPolicyDocument(row.document, row.resourceId)
}
result.validated += rows.length
afterId = lastId
@@ -491,7 +553,7 @@ export function createPostgresCredentialGroupPolicyLifecycleStore(
END IF;
DELETE FROM "public"."resource_policy"
- WHERE "workspace_id" = OLD."workspace_id"
+ WHERE ("workspace_id" = OLD."workspace_id" OR "organization_id" = OLD."organization_id")
AND "resource_type" = 'credential_group'
AND "resource_id" = OLD."id";
RETURN OLD;
@@ -598,9 +660,9 @@ export function createPostgresCredentialGroupPolicyLifecycleStore(
LEFT JOIN resource_policy rp
ON rp.resource_type = 'credential_group'
AND rp.resource_id = cg.id
- WHERE cg.workspace_id IS NOT NULL
- AND (rp.resource_id IS NULL
- OR rp.workspace_id IS DISTINCT FROM cg.workspace_id)
+ WHERE rp.resource_id IS NULL
+ OR rp.workspace_id IS DISTINCT FROM cg.workspace_id
+ OR rp.organization_id IS DISTINCT FROM cg.organization_id
UNION ALL
@@ -608,7 +670,7 @@ export function createPostgresCredentialGroupPolicyLifecycleStore(
FROM resource_policy rp
LEFT JOIN credential_group cg ON cg.id = rp.resource_id
WHERE rp.resource_type = 'credential_group'
- AND (cg.id IS NULL OR cg.workspace_id IS NULL)
+ AND cg.id IS NULL
) violations
ORDER BY resource_id
LIMIT 1
@@ -621,11 +683,15 @@ export function createPostgresCredentialGroupPolicyLifecycleStore(
SELECT
id,
workspace_id AS "workspaceId",
+ organization_id AS "organizationId",
resource_id AS "resourceId",
revision,
octet_length(document::text)::integer AS "documentBytes",
CASE
- WHEN octet_length(document::text) <= ${CREDENTIAL_GROUP_POLICY_DOCUMENT_MAX_BYTES}
+ WHEN octet_length(document::text) <= CASE
+ WHEN organization_id IS NULL THEN ${CREDENTIAL_GROUP_POLICY_DOCUMENT_MAX_BYTES}::integer
+ ELSE ${ORGANIZATION_ACCOUNT_POLICY_DOCUMENT_MAX_BYTES}::integer
+ END
THEN document
ELSE NULL
END AS document
diff --git a/packages/db/migrations/0327_organization_connected_accounts.sql b/packages/db/migrations/0327_organization_connected_accounts.sql
new file mode 100644
index 00000000000..cc70640a95d
--- /dev/null
+++ b/packages/db/migrations/0327_organization_connected_accounts.sql
@@ -0,0 +1,191 @@
+SET LOCAL lock_timeout = '5s';
+--> statement-breakpoint
+-- migration-safe: Replaces the organization credential type check with a superset including managed_mcp; old writers remain valid and no data is removed.
+ALTER TABLE "credential" DROP CONSTRAINT IF EXISTS "credential_organization_type_check";
+--> statement-breakpoint
+ALTER TABLE "mcp_server_oauth" ALTER COLUMN "workspace_id" DROP NOT NULL;
+--> statement-breakpoint
+ALTER TABLE "mcp_servers" ALTER COLUMN "workspace_id" DROP NOT NULL;
+--> statement-breakpoint
+ALTER TABLE "resource_policy" ALTER COLUMN "workspace_id" DROP NOT NULL;
+--> statement-breakpoint
+ALTER TABLE "credential" ADD COLUMN IF NOT EXISTS "mcp_oauth_config_version" integer;
+--> statement-breakpoint
+ALTER TABLE "credential_group_enrollment" ADD COLUMN IF NOT EXISTS "user_id" text;
+--> statement-breakpoint
+ALTER TABLE "mcp_server_oauth" ADD COLUMN IF NOT EXISTS "organization_id" text;
+--> statement-breakpoint
+ALTER TABLE "mcp_servers" ADD COLUMN IF NOT EXISTS "organization_id" text;
+--> statement-breakpoint
+ALTER TABLE "mcp_servers" ADD COLUMN IF NOT EXISTS "oauth_config_version" integer DEFAULT 1 NOT NULL;
+--> statement-breakpoint
+ALTER TABLE "resource_policy" ADD COLUMN IF NOT EXISTS "organization_id" text;
+--> statement-breakpoint
+DO $$
+BEGIN
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'credential_group_enrollment_user_id_user_id_fk' AND conrelid = '"public"."credential_group_enrollment"'::regclass) THEN
+ ALTER TABLE "credential_group_enrollment" ADD CONSTRAINT "credential_group_enrollment_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action NOT VALID;
+ END IF;
+END;
+$$;
+--> statement-breakpoint
+DO $$
+BEGIN
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'mcp_server_oauth_organization_id_organization_id_fk' AND conrelid = '"public"."mcp_server_oauth"'::regclass) THEN
+ ALTER TABLE "mcp_server_oauth" ADD CONSTRAINT "mcp_server_oauth_organization_id_organization_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action NOT VALID;
+ END IF;
+END;
+$$;
+--> statement-breakpoint
+DO $$
+BEGIN
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'mcp_servers_organization_id_organization_id_fk' AND conrelid = '"public"."mcp_servers"'::regclass) THEN
+ ALTER TABLE "mcp_servers" ADD CONSTRAINT "mcp_servers_organization_id_organization_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action NOT VALID;
+ END IF;
+END;
+$$;
+--> statement-breakpoint
+DO $$
+BEGIN
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'resource_policy_organization_id_organization_id_fk' AND conrelid = '"public"."resource_policy"'::regclass) THEN
+ ALTER TABLE "resource_policy" ADD CONSTRAINT "resource_policy_organization_id_organization_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action NOT VALID;
+ END IF;
+END;
+$$;
+--> statement-breakpoint
+DO $$
+BEGIN
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'credential_organization_type_check' AND conrelid = '"public"."credential"'::regclass) THEN
+ ALTER TABLE "credential" ADD CONSTRAINT "credential_organization_type_check" CHECK ("credential"."organization_id" IS NULL OR "credential"."type" IN ('oauth', 'managed_oauth', 'managed_mcp', 'service_account', 'personal_token')) NOT VALID;
+ END IF;
+END;
+$$;
+--> statement-breakpoint
+DO $$
+BEGIN
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'mcp_server_oauth_owner_check' AND conrelid = '"public"."mcp_server_oauth"'::regclass) THEN
+ ALTER TABLE "mcp_server_oauth" ADD CONSTRAINT "mcp_server_oauth_owner_check" CHECK (num_nonnulls("mcp_server_oauth"."workspace_id", "mcp_server_oauth"."organization_id") = 1) NOT VALID;
+ END IF;
+END;
+$$;
+--> statement-breakpoint
+DO $$
+BEGIN
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'mcp_servers_owner_check' AND conrelid = '"public"."mcp_servers"'::regclass) THEN
+ ALTER TABLE "mcp_servers" ADD CONSTRAINT "mcp_servers_owner_check" CHECK (num_nonnulls("mcp_servers"."workspace_id", "mcp_servers"."organization_id") = 1) NOT VALID;
+ END IF;
+END;
+$$;
+--> statement-breakpoint
+DO $$
+BEGIN
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'mcp_servers_organization_managed_check' AND conrelid = '"public"."mcp_servers"'::regclass) THEN
+ ALTER TABLE "mcp_servers" ADD CONSTRAINT "mcp_servers_organization_managed_check" CHECK ("mcp_servers"."organization_id" IS NULL OR "mcp_servers"."credential_group_id" IS NOT NULL) NOT VALID;
+ END IF;
+END;
+$$;
+--> statement-breakpoint
+DO $$
+BEGIN
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'resource_policy_owner_check' AND conrelid = '"public"."resource_policy"'::regclass) THEN
+ ALTER TABLE "resource_policy" ADD CONSTRAINT "resource_policy_owner_check" CHECK (num_nonnulls("resource_policy"."workspace_id", "resource_policy"."organization_id") = 1) NOT VALID;
+ END IF;
+END;
+$$;
+--> statement-breakpoint
+CREATE OR REPLACE FUNCTION "public"."sync_credential_group_resource_policy"()
+RETURNS trigger
+LANGUAGE plpgsql
+SET search_path = pg_catalog, public
+AS $$
+BEGIN
+ IF TG_OP = 'INSERT' THEN
+ IF NEW."workspace_id" IS NULL THEN
+ RETURN NEW;
+ END IF;
+ INSERT INTO "public"."resource_policy" (
+ "id",
+ "workspace_id",
+ "resource_type",
+ "resource_id",
+ "revision",
+ "document",
+ "created_by",
+ "updated_by"
+ )
+ VALUES (
+ gen_random_uuid()::text,
+ NEW."workspace_id",
+ 'credential_group',
+ NEW."id",
+ 1,
+ jsonb_build_object(
+ 'version', 1,
+ 'resource', jsonb_build_object('type', 'credential_group', 'id', NEW."id"),
+ 'statements', jsonb_build_array(
+ jsonb_build_object(
+ 'sid', 'CredentialGroupActorCredentialAccess',
+ 'effect', 'allow',
+ 'actions', jsonb_build_array('credential_groups.credentials.use'),
+ 'principals', jsonb_build_array(
+ jsonb_build_object('type', 'credential_group_actor')
+ ),
+ 'condition', jsonb_build_object(
+ 'Bool', jsonb_build_object(
+ 'credential_group:ActorOwnsCredential', true
+ )
+ )
+ )
+ )
+ ),
+ NEW."created_by",
+ NEW."created_by"
+ );
+ RETURN NEW;
+ END IF;
+
+ DELETE FROM "public"."resource_policy"
+ WHERE ("workspace_id" = OLD."workspace_id" OR "organization_id" = OLD."organization_id")
+ AND "resource_type" = 'credential_group'
+ AND "resource_id" = OLD."id";
+ RETURN OLD;
+END;
+$$;
+
+--> statement-breakpoint
+COMMIT;
+--> statement-breakpoint
+SET lock_timeout = 0;
+--> statement-breakpoint
+CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS "credential_group_enrollment_group_user_unique" ON "credential_group_enrollment" USING btree ("credential_group_id","user_id") WHERE "credential_group_enrollment"."user_id" IS NOT NULL;
+--> statement-breakpoint
+CREATE INDEX CONCURRENTLY IF NOT EXISTS "credential_group_enrollment_user_id_idx" ON "credential_group_enrollment" USING btree ("user_id");
+--> statement-breakpoint
+CREATE INDEX CONCURRENTLY IF NOT EXISTS "mcp_servers_organization_id_idx" ON "mcp_servers" USING btree ("organization_id");
+--> statement-breakpoint
+CREATE INDEX CONCURRENTLY IF NOT EXISTS "resource_policy_organization_id_idx" ON "resource_policy" USING btree ("organization_id");
+--> statement-breakpoint
+RESET lock_timeout;
+--> statement-breakpoint
+BEGIN;
+--> statement-breakpoint
+SET LOCAL lock_timeout = '5s';
+--> statement-breakpoint
+DO $$
+DECLARE invalid_indexes text;
+BEGIN
+ SELECT string_agg(required.index_name, ', ' ORDER BY required.index_name) INTO invalid_indexes
+ FROM (VALUES
+ ('"public"."credential_group_enrollment_group_user_unique"', '"public"."credential_group_enrollment"'),
+ ('"public"."credential_group_enrollment_user_id_idx"', '"public"."credential_group_enrollment"'),
+ ('"public"."mcp_servers_organization_id_idx"', '"public"."mcp_servers"'),
+ ('"public"."resource_policy_organization_id_idx"', '"public"."resource_policy"')
+ ) AS required(index_name, table_name)
+ LEFT JOIN pg_index AS actual ON actual.indexrelid = to_regclass(required.index_name) AND actual.indrelid = to_regclass(required.table_name)
+ WHERE NOT COALESCE(actual.indisvalid AND actual.indisready, false);
+ IF invalid_indexes IS NOT NULL THEN
+ RAISE EXCEPTION 'Connected accounts migration requires valid indexes: %', invalid_indexes
+ USING HINT = 'Repair listed indexes with DROP INDEX CONCURRENTLY and CREATE INDEX CONCURRENTLY, then rerun.';
+ END IF;
+END;
+$$;
diff --git a/packages/db/migrations/meta/0327_snapshot.json b/packages/db/migrations/meta/0327_snapshot.json
new file mode 100644
index 00000000000..8642152425c
--- /dev/null
+++ b/packages/db/migrations/meta/0327_snapshot.json
@@ -0,0 +1,25266 @@
+{
+ "id": "1c843e33-9afa-45ab-9790-f0f825110b57",
+ "prevId": "621e13c3-aa09-4312-b403-8bf9edae0689",
+ "version": "7",
+ "dialect": "postgresql",
+ "tables": {
+ "public.academy_certificate": {
+ "name": "academy_certificate",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "course_id": {
+ "name": "course_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "academy_cert_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'active'"
+ },
+ "issued_at": {
+ "name": "issued_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "certificate_number": {
+ "name": "certificate_number",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "academy_certificate_user_id_idx": {
+ "name": "academy_certificate_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "academy_certificate_course_id_idx": {
+ "name": "academy_certificate_course_id_idx",
+ "columns": [
+ {
+ "expression": "course_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "academy_certificate_user_course_unique": {
+ "name": "academy_certificate_user_course_unique",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "course_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "academy_certificate_status_idx": {
+ "name": "academy_certificate_status_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "academy_certificate_user_id_user_id_fk": {
+ "name": "academy_certificate_user_id_user_id_fk",
+ "tableFrom": "academy_certificate",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "academy_certificate_certificate_number_unique": {
+ "name": "academy_certificate_certificate_number_unique",
+ "nullsNotDistinct": false,
+ "columns": ["certificate_number"]
+ }
+ },
+ "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
+ },
+ "oauth_config": {
+ "name": "oauth_config",
+ "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
+ },
+ "key_hash": {
+ "name": "key_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "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": {}
+ },
+ "api_key_key_hash_idx": {
+ "name": "api_key_key_hash_idx",
+ "columns": [
+ {
+ "expression": "key_hash",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "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": {}
+ },
+ "async_jobs_schedule_pending_run_at_idx": {
+ "name": "async_jobs_schedule_pending_run_at_idx",
+ "columns": [
+ {
+ "expression": "run_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"async_jobs\".\"type\" = 'schedule-execution' AND \"async_jobs\".\"status\" = 'pending'",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "async_jobs_schedule_processing_started_at_idx": {
+ "name": "async_jobs_schedule_processing_started_at_idx",
+ "columns": [
+ {
+ "expression": "started_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"async_jobs\".\"type\" = 'schedule-execution' AND \"async_jobs\".\"status\" = 'processing'",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "async_jobs_schedule_unreconciled_terminal_idx": {
+ "name": "async_jobs_schedule_unreconciled_terminal_idx",
+ "columns": [
+ {
+ "expression": "updated_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"async_jobs\".\"type\" = 'schedule-execution' AND \"async_jobs\".\"status\" IN ('completed', 'failed', 'cancelled') AND COALESCE(\"async_jobs\".\"metadata\" ->> 'scheduleReconciled', 'false') <> 'true'",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.audit_log": {
+ "name": "audit_log",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "actor_id": {
+ "name": "actor_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "action": {
+ "name": "action",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "resource_type": {
+ "name": "resource_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "resource_id": {
+ "name": "resource_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "actor_name": {
+ "name": "actor_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "actor_email": {
+ "name": "actor_email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "resource_name": {
+ "name": "resource_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'{}'"
+ },
+ "ip_address": {
+ "name": "ip_address",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_agent": {
+ "name": "user_agent",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "audit_log_workspace_created_idx": {
+ "name": "audit_log_workspace_created_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "audit_log_workspace_created_at_id_idx": {
+ "name": "audit_log_workspace_created_at_id_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "date_trunc('milliseconds', \"created_at\")",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "audit_log_actor_created_idx": {
+ "name": "audit_log_actor_created_idx",
+ "columns": [
+ {
+ "expression": "actor_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "audit_log_resource_idx": {
+ "name": "audit_log_resource_idx",
+ "columns": [
+ {
+ "expression": "resource_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "resource_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "audit_log_action_idx": {
+ "name": "audit_log_action_idx",
+ "columns": [
+ {
+ "expression": "action",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "audit_log_workspace_id_workspace_id_fk": {
+ "name": "audit_log_workspace_id_workspace_id_fk",
+ "tableFrom": "audit_log",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "audit_log_actor_id_user_id_fk": {
+ "name": "audit_log_actor_id_user_id_fk",
+ "tableFrom": "audit_log",
+ "tableTo": "user",
+ "columnsFrom": ["actor_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.background_work_status": {
+ "name": "background_work_status",
+ "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": false
+ },
+ "kind": {
+ "name": "kind",
+ "type": "background_work_kind",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "background_work_status_value",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "message": {
+ "name": "message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "error": {
+ "name": "error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "started_at": {
+ "name": "started_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "completed_at": {
+ "name": "completed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "background_work_status_workspace_status_idx": {
+ "name": "background_work_status_workspace_status_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "background_work_status_workflow_status_idx": {
+ "name": "background_work_status_workflow_status_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "background_work_status_meta_child_ws_idx": {
+ "name": "background_work_status_meta_child_ws_idx",
+ "columns": [
+ {
+ "expression": "(\"metadata\" ->> 'childWorkspaceId')",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "background_work_status_meta_other_ws_idx": {
+ "name": "background_work_status_meta_other_ws_idx",
+ "columns": [
+ {
+ "expression": "(\"metadata\" ->> 'otherWorkspaceId')",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "background_work_status_workspace_id_workspace_id_fk": {
+ "name": "background_work_status_workspace_id_workspace_id_fk",
+ "tableFrom": "background_work_status",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "background_work_status_workflow_id_workflow_id_fk": {
+ "name": "background_work_status_workflow_id_workflow_id_fk",
+ "tableFrom": "background_work_status",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "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": "'[]'"
+ },
+ "include_thinking": {
+ "name": "include_thinking",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "include_tool_calls": {
+ "name": "include_tool_calls",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "archived_at": {
+ "name": "archived_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": {
+ "identifier_idx": {
+ "name": "identifier_idx",
+ "columns": [
+ {
+ "expression": "identifier",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"chat\".\"archived_at\" IS NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "chat_archived_at_partial_idx": {
+ "name": "chat_archived_at_partial_idx",
+ "columns": [
+ {
+ "expression": "archived_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"chat\".\"archived_at\" IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_chat_on_workflow_id_archived_at": {
+ "name": "idx_chat_on_workflow_id_archived_at",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "archived_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "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_async_tool_calls": {
+ "name": "copilot_async_tool_calls",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "run_id": {
+ "name": "run_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "checkpoint_id": {
+ "name": "checkpoint_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tool_call_id": {
+ "name": "tool_call_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "tool_name": {
+ "name": "tool_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "args": {
+ "name": "args",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'"
+ },
+ "status": {
+ "name": "status",
+ "type": "copilot_async_tool_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "result": {
+ "name": "result",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "error": {
+ "name": "error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "permission_decision": {
+ "name": "permission_decision",
+ "type": "copilot_tool_permission_decision",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "permission_decided_at": {
+ "name": "permission_decided_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "claimed_at": {
+ "name": "claimed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "claimed_by": {
+ "name": "claimed_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "completed_at": {
+ "name": "completed_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": {
+ "copilot_async_tool_calls_run_id_idx": {
+ "name": "copilot_async_tool_calls_run_id_idx",
+ "columns": [
+ {
+ "expression": "run_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_async_tool_calls_checkpoint_id_idx": {
+ "name": "copilot_async_tool_calls_checkpoint_id_idx",
+ "columns": [
+ {
+ "expression": "checkpoint_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_async_tool_calls_status_idx": {
+ "name": "copilot_async_tool_calls_status_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_async_tool_calls_run_status_idx": {
+ "name": "copilot_async_tool_calls_run_status_idx",
+ "columns": [
+ {
+ "expression": "run_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_async_tool_calls_tool_call_id_unique": {
+ "name": "copilot_async_tool_calls_tool_call_id_unique",
+ "columns": [
+ {
+ "expression": "tool_call_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "copilot_async_tool_calls_run_id_copilot_runs_id_fk": {
+ "name": "copilot_async_tool_calls_run_id_copilot_runs_id_fk",
+ "tableFrom": "copilot_async_tool_calls",
+ "tableTo": "copilot_runs",
+ "columnsFrom": ["run_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "copilot_async_tool_calls_checkpoint_id_copilot_run_checkpoints_id_fk": {
+ "name": "copilot_async_tool_calls_checkpoint_id_copilot_run_checkpoints_id_fk",
+ "tableFrom": "copilot_async_tool_calls",
+ "tableTo": "copilot_run_checkpoints",
+ "columnsFrom": ["checkpoint_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": false
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "type": {
+ "name": "type",
+ "type": "chat_type",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'copilot'"
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "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
+ },
+ "resources": {
+ "name": "resources",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'"
+ },
+ "auto_allowed_tools": {
+ "name": "auto_allowed_tools",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'"
+ },
+ "last_seen_at": {
+ "name": "last_seen_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "pinned": {
+ "name": "pinned",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 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": {
+ "copilot_chats_organization_id_idx": {
+ "name": "copilot_chats_organization_id_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_chats_user_org_created_idx": {
+ "name": "copilot_chats_user_org_created_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "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_user_workspace_idx": {
+ "name": "copilot_chats_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": {}
+ },
+ "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": {}
+ },
+ "copilot_chats_workspace_created_at_id_idx": {
+ "name": "copilot_chats_workspace_created_at_id_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "date_trunc('milliseconds', \"created_at\")",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_chats_user_workspace_deleted_partial_idx": {
+ "name": "copilot_chats_user_workspace_deleted_partial_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"copilot_chats\".\"deleted_at\" IS NOT NULL",
+ "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"
+ },
+ "copilot_chats_workspace_id_workspace_id_fk": {
+ "name": "copilot_chats_workspace_id_workspace_id_fk",
+ "tableFrom": "copilot_chats",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "copilot_chats_organization_id_organization_id_fk": {
+ "name": "copilot_chats_organization_id_organization_id_fk",
+ "tableFrom": "copilot_chats",
+ "tableTo": "organization",
+ "columnsFrom": ["organization_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "copilot_chats_owner_check": {
+ "name": "copilot_chats_owner_check",
+ "value": "num_nonnulls(\"copilot_chats\".\"workspace_id\", \"copilot_chats\".\"organization_id\") <= 1"
+ },
+ "copilot_chats_organization_workflow_check": {
+ "name": "copilot_chats_organization_workflow_check",
+ "value": "\"copilot_chats\".\"organization_id\" IS NULL OR \"copilot_chats\".\"workflow_id\" IS NULL"
+ }
+ },
+ "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.copilot_messages": {
+ "name": "copilot_messages",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "chat_id": {
+ "name": "chat_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "message_id": {
+ "name": "message_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "role": {
+ "name": "role",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "content": {
+ "name": "content",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "stream_id": {
+ "name": "stream_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "parent_message_id": {
+ "name": "parent_message_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "model": {
+ "name": "model",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tokens_in": {
+ "name": "tokens_in",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tokens_out": {
+ "name": "tokens_out",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "seq": {
+ "name": "seq",
+ "type": "integer",
+ "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": {
+ "copilot_messages_chat_message_unique": {
+ "name": "copilot_messages_chat_message_unique",
+ "columns": [
+ {
+ "expression": "chat_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "message_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_messages_chat_created_at_idx": {
+ "name": "copilot_messages_chat_created_at_idx",
+ "columns": [
+ {
+ "expression": "chat_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"copilot_messages\".\"deleted_at\" IS NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_messages_chat_seq_idx": {
+ "name": "copilot_messages_chat_seq_idx",
+ "columns": [
+ {
+ "expression": "chat_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "seq",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"copilot_messages\".\"deleted_at\" IS NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_messages_chat_stream_idx": {
+ "name": "copilot_messages_chat_stream_idx",
+ "columns": [
+ {
+ "expression": "chat_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "stream_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"copilot_messages\".\"stream_id\" IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_messages_user_created_at_idx": {
+ "name": "copilot_messages_user_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "chat_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "message_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"copilot_messages\".\"role\" = 'user' AND \"copilot_messages\".\"deleted_at\" IS NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "copilot_messages_chat_id_copilot_chats_id_fk": {
+ "name": "copilot_messages_chat_id_copilot_chats_id_fk",
+ "tableFrom": "copilot_messages",
+ "tableTo": "copilot_chats",
+ "columnsFrom": ["chat_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.copilot_run_checkpoints": {
+ "name": "copilot_run_checkpoints",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "run_id": {
+ "name": "run_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "pending_tool_call_id": {
+ "name": "pending_tool_call_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "conversation_snapshot": {
+ "name": "conversation_snapshot",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'"
+ },
+ "agent_state": {
+ "name": "agent_state",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'"
+ },
+ "provider_request": {
+ "name": "provider_request",
+ "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": {
+ "copilot_run_checkpoints_run_id_idx": {
+ "name": "copilot_run_checkpoints_run_id_idx",
+ "columns": [
+ {
+ "expression": "run_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_run_checkpoints_pending_tool_call_id_idx": {
+ "name": "copilot_run_checkpoints_pending_tool_call_id_idx",
+ "columns": [
+ {
+ "expression": "pending_tool_call_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_run_checkpoints_run_pending_tool_unique": {
+ "name": "copilot_run_checkpoints_run_pending_tool_unique",
+ "columns": [
+ {
+ "expression": "run_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "pending_tool_call_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "copilot_run_checkpoints_run_id_copilot_runs_id_fk": {
+ "name": "copilot_run_checkpoints_run_id_copilot_runs_id_fk",
+ "tableFrom": "copilot_run_checkpoints",
+ "tableTo": "copilot_runs",
+ "columnsFrom": ["run_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.copilot_runs": {
+ "name": "copilot_runs",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "execution_id": {
+ "name": "execution_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "parent_run_id": {
+ "name": "parent_run_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "chat_id": {
+ "name": "chat_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "stream_id": {
+ "name": "stream_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "agent": {
+ "name": "agent",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "model": {
+ "name": "model",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "provider": {
+ "name": "provider",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "copilot_run_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'active'"
+ },
+ "request_context": {
+ "name": "request_context",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'"
+ },
+ "started_at": {
+ "name": "started_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "completed_at": {
+ "name": "completed_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()"
+ },
+ "error": {
+ "name": "error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "copilot_runs_execution_id_idx": {
+ "name": "copilot_runs_execution_id_idx",
+ "columns": [
+ {
+ "expression": "execution_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_runs_parent_run_id_idx": {
+ "name": "copilot_runs_parent_run_id_idx",
+ "columns": [
+ {
+ "expression": "parent_run_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_runs_chat_id_idx": {
+ "name": "copilot_runs_chat_id_idx",
+ "columns": [
+ {
+ "expression": "chat_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_runs_user_id_idx": {
+ "name": "copilot_runs_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_runs_workflow_id_idx": {
+ "name": "copilot_runs_workflow_id_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_runs_workspace_id_idx": {
+ "name": "copilot_runs_workspace_id_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_runs_status_idx": {
+ "name": "copilot_runs_status_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_runs_chat_execution_idx": {
+ "name": "copilot_runs_chat_execution_idx",
+ "columns": [
+ {
+ "expression": "chat_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "execution_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_runs_execution_started_at_idx": {
+ "name": "copilot_runs_execution_started_at_idx",
+ "columns": [
+ {
+ "expression": "execution_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "started_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_runs_workspace_completed_at_id_idx": {
+ "name": "copilot_runs_workspace_completed_at_id_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "date_trunc('milliseconds', \"completed_at\")",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_runs_stream_id_unique": {
+ "name": "copilot_runs_stream_id_unique",
+ "columns": [
+ {
+ "expression": "stream_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "copilot_runs_chat_id_copilot_chats_id_fk": {
+ "name": "copilot_runs_chat_id_copilot_chats_id_fk",
+ "tableFrom": "copilot_runs",
+ "tableTo": "copilot_chats",
+ "columnsFrom": ["chat_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "copilot_runs_user_id_user_id_fk": {
+ "name": "copilot_runs_user_id_user_id_fk",
+ "tableFrom": "copilot_runs",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "copilot_runs_workflow_id_workflow_id_fk": {
+ "name": "copilot_runs_workflow_id_workflow_id_fk",
+ "tableFrom": "copilot_runs",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "copilot_runs_workspace_id_workspace_id_fk": {
+ "name": "copilot_runs_workspace_id_workspace_id_fk",
+ "tableFrom": "copilot_runs",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.copilot_workflow_read_hashes": {
+ "name": "copilot_workflow_read_hashes",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "chat_id": {
+ "name": "chat_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "hash": {
+ "name": "hash",
+ "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": {
+ "copilot_workflow_read_hashes_chat_id_idx": {
+ "name": "copilot_workflow_read_hashes_chat_id_idx",
+ "columns": [
+ {
+ "expression": "chat_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_workflow_read_hashes_workflow_id_idx": {
+ "name": "copilot_workflow_read_hashes_workflow_id_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "copilot_workflow_read_hashes_chat_workflow_unique": {
+ "name": "copilot_workflow_read_hashes_chat_workflow_unique",
+ "columns": [
+ {
+ "expression": "chat_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "copilot_workflow_read_hashes_chat_id_copilot_chats_id_fk": {
+ "name": "copilot_workflow_read_hashes_chat_id_copilot_chats_id_fk",
+ "tableFrom": "copilot_workflow_read_hashes",
+ "tableTo": "copilot_chats",
+ "columnsFrom": ["chat_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "copilot_workflow_read_hashes_workflow_id_workflow_id_fk": {
+ "name": "copilot_workflow_read_hashes_workflow_id_workflow_id_fk",
+ "tableFrom": "copilot_workflow_read_hashes",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_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": false
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "type": {
+ "name": "type",
+ "type": "credential_type",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "display_name": {
+ "name": "display_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "unredacted": {
+ "name": "unredacted",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "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
+ },
+ "encrypted_service_account_key": {
+ "name": "encrypted_service_account_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "encrypted_personal_token": {
+ "name": "encrypted_personal_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "authorization_app_id": {
+ "name": "authorization_app_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "credential_group_enrollment_id": {
+ "name": "credential_group_enrollment_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "credential_group_option_id": {
+ "name": "credential_group_option_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "mcp_server_id": {
+ "name": "mcp_server_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "mcp_oauth_config_version": {
+ "name": "mcp_oauth_config_version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "managed_oauth_scope_version": {
+ "name": "managed_oauth_scope_version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "provider_subject_id": {
+ "name": "provider_subject_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "provider_tenant_id": {
+ "name": "provider_tenant_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "managed_oauth_status": {
+ "name": "managed_oauth_status",
+ "type": "managed_oauth_credential_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "granted_scopes": {
+ "name": "granted_scopes",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "provider_metadata": {
+ "name": "provider_metadata",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "encrypted_oauth_token_set": {
+ "name": "encrypted_oauth_token_set",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "mcp_tools": {
+ "name": "mcp_tools",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "mcp_tools_refreshed_at": {
+ "name": "mcp_tools_refreshed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "granted_at": {
+ "name": "granted_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "revoked_at": {
+ "name": "revoked_at",
+ "type": "timestamp",
+ "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
+ },
+ "last_refreshed_at": {
+ "name": "last_refreshed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": 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": {
+ "credential_organization_id_idx": {
+ "name": "credential_organization_id_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "credential_organization_account_unique": {
+ "name": "credential_organization_account_unique",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "account_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"credential\".\"account_id\" IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "credential_org_personal_token_unique": {
+ "name": "credential_org_personal_token_unique",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_by",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "provider_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "provider_tenant_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "provider_subject_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"credential\".\"type\" = 'personal_token'",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "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_group_enrollment_idx": {
+ "name": "credential_group_enrollment_idx",
+ "columns": [
+ {
+ "expression": "credential_group_enrollment_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "credential_mcp_server_idx": {
+ "name": "credential_mcp_server_idx",
+ "columns": [
+ {
+ "expression": "mcp_server_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "credential_group_option_unique": {
+ "name": "credential_group_option_unique",
+ "columns": [
+ {
+ "expression": "credential_group_enrollment_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "credential_group_option_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"credential\".\"type\" = 'managed_oauth'",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "credential_managed_mcp_enrollment_server_unique": {
+ "name": "credential_managed_mcp_enrollment_server_unique",
+ "columns": [
+ {
+ "expression": "credential_group_enrollment_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "mcp_server_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"credential\".\"type\" = 'managed_mcp'",
+ "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": {}
+ },
+ "credential_personal_token_identity_unique": {
+ "name": "credential_personal_token_identity_unique",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_by",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "provider_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "provider_tenant_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "provider_subject_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "type = 'personal_token'",
+ "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_organization_id_organization_id_fk": {
+ "name": "credential_organization_id_organization_id_fk",
+ "tableFrom": "credential",
+ "tableTo": "organization",
+ "columnsFrom": ["organization_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_credential_group_enrollment_id_credential_group_enrollment_id_fk": {
+ "name": "credential_credential_group_enrollment_id_credential_group_enrollment_id_fk",
+ "tableFrom": "credential",
+ "tableTo": "credential_group_enrollment",
+ "columnsFrom": ["credential_group_enrollment_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "credential_mcp_server_id_mcp_servers_id_fk": {
+ "name": "credential_mcp_server_id_mcp_servers_id_fk",
+ "tableFrom": "credential",
+ "tableTo": "mcp_servers",
+ "columnsFrom": ["mcp_server_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_owner_check": {
+ "name": "credential_owner_check",
+ "value": "num_nonnulls(\"credential\".\"workspace_id\", \"credential\".\"organization_id\") = 1"
+ },
+ "credential_organization_type_check": {
+ "name": "credential_organization_type_check",
+ "value": "\"credential\".\"organization_id\" IS NULL OR \"credential\".\"type\" IN ('oauth', 'managed_oauth', 'managed_mcp', 'service_account', 'personal_token')"
+ },
+ "credential_personal_token_source_check": {
+ "name": "credential_personal_token_source_check",
+ "value": "(type::text <> 'personal_token') OR (\n created_by IS NOT NULL\n AND provider_id IS NOT NULL\n AND provider_id = 'gitlab'\n AND provider_subject_id IS NOT NULL\n AND provider_tenant_id IS NOT NULL\n AND encrypted_personal_token IS NOT NULL\n AND granted_scopes IS NOT NULL\n AND cardinality(granted_scopes) > 0\n AND account_id IS NULL\n AND env_key IS NULL\n AND env_owner_user_id IS NULL\n AND authorization_app_id IS NULL\n AND encrypted_oauth_token_set IS NULL\n AND encrypted_service_account_key IS NULL\n AND unredacted = false\n )"
+ },
+ "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_managed_oauth_source_check": {
+ "name": "credential_managed_oauth_source_check",
+ "value": "(type::text <> 'managed_oauth') OR (\n account_id IS NULL\n AND provider_id IS NOT NULL\n AND authorization_app_id IS NOT NULL\n AND provider_subject_id IS NOT NULL\n AND managed_oauth_status IS NOT NULL\n AND granted_scopes IS NOT NULL\n AND encrypted_oauth_token_set IS NOT NULL\n AND granted_at IS NOT NULL\n )"
+ },
+ "credential_managed_oauth_group_binding_check": {
+ "name": "credential_managed_oauth_group_binding_check",
+ "value": "(type::text <> 'managed_oauth') OR (\n credential_group_enrollment_id IS NOT NULL\n AND credential_group_option_id IS NOT NULL\n AND managed_oauth_scope_version IS NOT NULL\n AND managed_oauth_scope_version > 0\n )"
+ },
+ "credential_managed_mcp_source_check": {
+ "name": "credential_managed_mcp_source_check",
+ "value": "(type::text <> 'managed_mcp') OR (\n id LIKE 'mcp-cg-%'\n AND account_id IS NULL\n AND provider_id IS NULL\n AND authorization_app_id IS NULL\n AND credential_group_enrollment_id IS NOT NULL\n AND credential_group_option_id IS NULL\n AND mcp_server_id IS NOT NULL\n AND managed_oauth_status IS NOT NULL\n AND (managed_oauth_status <> 'active' OR (\n encrypted_oauth_token_set IS NOT NULL\n AND mcp_tools IS NOT NULL\n ))\n AND granted_at IS NOT NULL\n AND managed_oauth_scope_version IS NULL\n AND provider_subject_id IS NULL\n AND provider_tenant_id IS NULL\n AND granted_scopes IS NULL\n AND provider_metadata IS NULL\n AND created_by IS NULL\n AND env_key IS NULL\n AND env_owner_user_id IS NULL\n AND encrypted_service_account_key IS NULL\n AND unredacted = false\n )"
+ },
+ "credential_creator_source_check": {
+ "name": "credential_creator_source_check",
+ "value": "(type::text = 'managed_mcp') OR created_by 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_group": {
+ "name": "credential_group",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "public_id": {
+ "name": "public_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
+ },
+ "options": {
+ "name": "options",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "encrypted_provider_configuration": {
+ "name": "encrypted_provider_configuration",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "credential_group_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'active'"
+ },
+ "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": {
+ "credential_group_organization_id_idx": {
+ "name": "credential_group_organization_id_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "credential_group_organization_unique": {
+ "name": "credential_group_organization_unique",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "credential_group_public_id_unique": {
+ "name": "credential_group_public_id_unique",
+ "columns": [
+ {
+ "expression": "public_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "credential_group_workspace_unique": {
+ "name": "credential_group_workspace_unique",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "credential_group_workspace_id_workspace_id_fk": {
+ "name": "credential_group_workspace_id_workspace_id_fk",
+ "tableFrom": "credential_group",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "credential_group_organization_id_organization_id_fk": {
+ "name": "credential_group_organization_id_organization_id_fk",
+ "tableFrom": "credential_group",
+ "tableTo": "organization",
+ "columnsFrom": ["organization_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "credential_group_created_by_user_id_fk": {
+ "name": "credential_group_created_by_user_id_fk",
+ "tableFrom": "credential_group",
+ "tableTo": "user",
+ "columnsFrom": ["created_by"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "credential_group_owner_check": {
+ "name": "credential_group_owner_check",
+ "value": "num_nonnulls(\"credential_group\".\"workspace_id\", \"credential_group\".\"organization_id\") = 1"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.credential_group_enrollment": {
+ "name": "credential_group_enrollment",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "credential_group_id": {
+ "name": "credential_group_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "credential_group_enrollment_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'invited'"
+ },
+ "invitation_token_hash": {
+ "name": "invitation_token_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "invitation_expires_at": {
+ "name": "invitation_expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "invited_at": {
+ "name": "invited_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "sent_at": {
+ "name": "sent_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "completed_at": {
+ "name": "completed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "revoked_at": {
+ "name": "revoked_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_delivery_error": {
+ "name": "last_delivery_error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": 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": {
+ "credential_group_enrollment_group_user_unique": {
+ "name": "credential_group_enrollment_group_user_unique",
+ "columns": [
+ {
+ "expression": "credential_group_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"credential_group_enrollment\".\"user_id\" IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "credential_group_enrollment_user_id_idx": {
+ "name": "credential_group_enrollment_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "credential_group_enrollment_group_email_unique": {
+ "name": "credential_group_enrollment_group_email_unique",
+ "columns": [
+ {
+ "expression": "credential_group_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "email",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "credential_group_enrollment_invitation_token_hash_unique": {
+ "name": "credential_group_enrollment_invitation_token_hash_unique",
+ "columns": [
+ {
+ "expression": "invitation_token_hash",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "credential_group_enrollment_group_status_idx": {
+ "name": "credential_group_enrollment_group_status_idx",
+ "columns": [
+ {
+ "expression": "credential_group_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "credential_group_enrollment_group_invited_at_id_idx": {
+ "name": "credential_group_enrollment_group_invited_at_id_idx",
+ "columns": [
+ {
+ "expression": "credential_group_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "invited_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "credential_group_enrollment_credential_group_id_credential_group_id_fk": {
+ "name": "credential_group_enrollment_credential_group_id_credential_group_id_fk",
+ "tableFrom": "credential_group_enrollment",
+ "tableTo": "credential_group",
+ "columnsFrom": ["credential_group_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "credential_group_enrollment_user_id_user_id_fk": {
+ "name": "credential_group_enrollment_user_id_user_id_fk",
+ "tableFrom": "credential_group_enrollment",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "credential_group_enrollment_created_by_user_id_fk": {
+ "name": "credential_group_enrollment_created_by_user_id_fk",
+ "tableFrom": "credential_group_enrollment",
+ "tableTo": "user",
+ "columnsFrom": ["created_by"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "credential_group_enrollment_normalized_email_check": {
+ "name": "credential_group_enrollment_normalized_email_check",
+ "value": "\"credential_group_enrollment\".\"email\" = lower(btrim(\"credential_group_enrollment\".\"email\")) AND length(\"credential_group_enrollment\".\"email\") BETWEEN 3 AND 320"
+ },
+ "credential_group_enrollment_invitation_token_hash_length_check": {
+ "name": "credential_group_enrollment_invitation_token_hash_length_check",
+ "value": "length(\"credential_group_enrollment\".\"invitation_token_hash\") = 64"
+ }
+ },
+ "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_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.custom_block": {
+ "name": "custom_block",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "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
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "''"
+ },
+ "icon_url": {
+ "name": "icon_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "inputs": {
+ "name": "inputs",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "outputs": {
+ "name": "outputs",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "trace_child_runs": {
+ "name": "trace_child_runs",
+ "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": {
+ "custom_block_organization_id_idx": {
+ "name": "custom_block_organization_id_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "custom_block_workflow_id_idx": {
+ "name": "custom_block_workflow_id_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "custom_block_organization_type_unique": {
+ "name": "custom_block_organization_type_unique",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "custom_block_organization_id_organization_id_fk": {
+ "name": "custom_block_organization_id_organization_id_fk",
+ "tableFrom": "custom_block",
+ "tableTo": "organization",
+ "columnsFrom": ["organization_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "custom_block_workflow_id_workflow_id_fk": {
+ "name": "custom_block_workflow_id_workflow_id_fk",
+ "tableFrom": "custom_block",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "custom_block_created_by_user_id_fk": {
+ "name": "custom_block_created_by_user_id_fk",
+ "tableFrom": "custom_block",
+ "tableTo": "user",
+ "columnsFrom": ["created_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.data_drain_runs": {
+ "name": "data_drain_runs",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "drain_id": {
+ "name": "drain_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "data_drain_run_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "trigger": {
+ "name": "trigger",
+ "type": "data_drain_run_trigger",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "started_at": {
+ "name": "started_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "finished_at": {
+ "name": "finished_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "rows_exported": {
+ "name": "rows_exported",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "bytes_written": {
+ "name": "bytes_written",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "cursor_before": {
+ "name": "cursor_before",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cursor_after": {
+ "name": "cursor_after",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "error": {
+ "name": "error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "locators": {
+ "name": "locators",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ }
+ },
+ "indexes": {
+ "data_drain_runs_drain_started_idx": {
+ "name": "data_drain_runs_drain_started_idx",
+ "columns": [
+ {
+ "expression": "drain_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "started_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "data_drain_runs_drain_id_data_drains_id_fk": {
+ "name": "data_drain_runs_drain_id_data_drains_id_fk",
+ "tableFrom": "data_drain_runs",
+ "tableTo": "data_drains",
+ "columnsFrom": ["drain_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.data_drains": {
+ "name": "data_drains",
+ "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
+ },
+ "source": {
+ "name": "source",
+ "type": "data_drain_source",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "destination_type": {
+ "name": "destination_type",
+ "type": "data_drain_destination",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "destination_config": {
+ "name": "destination_config",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "destination_credentials": {
+ "name": "destination_credentials",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "schedule_cadence": {
+ "name": "schedule_cadence",
+ "type": "data_drain_cadence",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "cursor": {
+ "name": "cursor",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_run_at": {
+ "name": "last_run_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_success_at": {
+ "name": "last_success_at",
+ "type": "timestamp",
+ "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": {
+ "data_drains_org_idx": {
+ "name": "data_drains_org_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "data_drains_due_idx": {
+ "name": "data_drains_due_idx",
+ "columns": [
+ {
+ "expression": "enabled",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "last_run_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "data_drains_org_name_unique": {
+ "name": "data_drains_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": {}
+ }
+ },
+ "foreignKeys": {
+ "data_drains_organization_id_organization_id_fk": {
+ "name": "data_drains_organization_id_organization_id_fk",
+ "tableFrom": "data_drains",
+ "tableTo": "organization",
+ "columnsFrom": ["organization_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "data_drains_created_by_user_id_fk": {
+ "name": "data_drains_created_by_user_id_fk",
+ "tableFrom": "data_drains",
+ "tableTo": "user",
+ "columnsFrom": ["created_by"],
+ "columnsTo": ["id"],
+ "onDelete": "no action",
+ "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
+ },
+ "storage_key": {
+ "name": "storage_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "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_attempts": {
+ "name": "processing_attempts",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "processing_queued_at": {
+ "name": "processing_queued_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "processing_queue_token": {
+ "name": "processing_queue_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "processing_started_at": {
+ "name": "processing_started_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "processing_deferred_until": {
+ "name": "processing_deferred_until",
+ "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
+ },
+ "archived_at": {
+ "name": "archived_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_excluded": {
+ "name": "user_excluded",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 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
+ },
+ "connector_id": {
+ "name": "connector_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "external_id": {
+ "name": "external_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "content_hash": {
+ "name": "content_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "source_url": {
+ "name": "source_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "secret_provenance_version": {
+ "name": "secret_provenance_version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "uploaded_by": {
+ "name": "uploaded_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "acl": {
+ "name": "acl",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{ws}'::text[]"
+ },
+ "acl_requirements": {
+ "name": "acl_requirements",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "acl_verified_at": {
+ "name": "acl_verified_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "source_modified_at": {
+ "name": "source_modified_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "source_seen_at": {
+ "name": "source_seen_at",
+ "type": "timestamp",
+ "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_acl_gin_idx": {
+ "name": "doc_acl_gin_idx",
+ "columns": [
+ {
+ "expression": "acl",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last",
+ "opclass": "array_ops"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"document\".\"deleted_at\" IS NULL",
+ "concurrently": false,
+ "method": "gin",
+ "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_connector_external_id_idx": {
+ "name": "doc_connector_external_id_idx",
+ "columns": [
+ {
+ "expression": "connector_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "external_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"document\".\"deleted_at\" IS NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_connector_source_lookup_idx": {
+ "name": "doc_connector_source_lookup_idx",
+ "columns": [
+ {
+ "expression": "connector_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "external_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_connector_reconciliation_idx": {
+ "name": "doc_connector_reconciliation_idx",
+ "columns": [
+ {
+ "expression": "connector_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "COALESCE(\"source_seen_at\", '-infinity'::timestamp)",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"document\".\"user_excluded\" = false AND \"document\".\"archived_at\" IS NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_active_kb_token_count_idx": {
+ "name": "doc_active_kb_token_count_idx",
+ "columns": [
+ {
+ "expression": "knowledge_base_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "token_count",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"document\".\"user_excluded\" = false AND \"document\".\"archived_at\" IS NULL AND \"document\".\"deleted_at\" IS NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_storage_key_idx": {
+ "name": "doc_storage_key_idx",
+ "columns": [
+ {
+ "expression": "storage_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"document\".\"storage_key\" IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_archived_at_partial_idx": {
+ "name": "doc_archived_at_partial_idx",
+ "columns": [
+ {
+ "expression": "archived_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"document\".\"archived_at\" IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_deleted_at_partial_idx": {
+ "name": "doc_deleted_at_partial_idx",
+ "columns": [
+ {
+ "expression": "deleted_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"document\".\"deleted_at\" IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_kb_tag1_lower_idx": {
+ "name": "doc_kb_tag1_lower_idx",
+ "columns": [
+ {
+ "expression": "knowledge_base_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "lower(\"tag1\")",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_kb_tag2_lower_idx": {
+ "name": "doc_kb_tag2_lower_idx",
+ "columns": [
+ {
+ "expression": "knowledge_base_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "lower(\"tag2\")",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_kb_tag3_lower_idx": {
+ "name": "doc_kb_tag3_lower_idx",
+ "columns": [
+ {
+ "expression": "knowledge_base_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "lower(\"tag3\")",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_kb_tag4_lower_idx": {
+ "name": "doc_kb_tag4_lower_idx",
+ "columns": [
+ {
+ "expression": "knowledge_base_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "lower(\"tag4\")",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_kb_tag5_lower_idx": {
+ "name": "doc_kb_tag5_lower_idx",
+ "columns": [
+ {
+ "expression": "knowledge_base_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "lower(\"tag5\")",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_kb_tag6_lower_idx": {
+ "name": "doc_kb_tag6_lower_idx",
+ "columns": [
+ {
+ "expression": "knowledge_base_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "lower(\"tag6\")",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "doc_kb_tag7_lower_idx": {
+ "name": "doc_kb_tag7_lower_idx",
+ "columns": [
+ {
+ "expression": "knowledge_base_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "lower(\"tag7\")",
+ "asc": true,
+ "isExpression": 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"
+ },
+ "document_connector_id_knowledge_connector_id_fk": {
+ "name": "document_connector_id_knowledge_connector_id_fk",
+ "tableFrom": "document",
+ "tableTo": "knowledge_connector",
+ "columnsFrom": ["connector_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "document_uploaded_by_user_id_fk": {
+ "name": "document_uploaded_by_user_id_fk",
+ "tableFrom": "document",
+ "tableTo": "user",
+ "columnsFrom": ["uploaded_by"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "doc_acl_token_shape_check": {
+ "name": "doc_acl_token_shape_check",
+ "value": "array_position(\"document\".\"acl\", NULL) IS NULL AND (cardinality(\"document\".\"acl\") = 0 OR (cardinality(\"document\".\"acl\") = array_length(string_to_array(array_to_string(\"document\".\"acl\", E'\\n'), E'\\n'), 1) AND array_to_string(\"document\".\"acl\", E'\\n') ~ '^((ws|pub|link|u:[^\\nA-Z]+@[^\\nA-Z]+|[gs]:[^\\n:]+:[^\\n:]+:[^\\n]+)(\\n(ws|pub|link|u:[^\\nA-Z]+@[^\\nA-Z]+|[gs]:[^\\n:]+:[^\\n:]+:[^\\n]+))*)$'))"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.document_secret_provenance": {
+ "name": "document_secret_provenance",
+ "schema": "",
+ "columns": {
+ "document_id": {
+ "name": "document_id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "source_hash": {
+ "name": "source_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "entries": {
+ "name": "entries",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "document_secret_provenance_document_id_document_id_fk": {
+ "name": "document_secret_provenance_document_id_document_id_fk",
+ "tableFrom": "document_secret_provenance",
+ "tableTo": "document",
+ "columnsFrom": ["document_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "document_secret_provenance_status_check": {
+ "name": "document_secret_provenance_status_check",
+ "value": "\"document_secret_provenance\".\"status\" IN ('exact', 'unknown')"
+ }
+ },
+ "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
+ },
+ "secret_provenance_version": {
+ "name": "secret_provenance_version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "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_384": {
+ "name": "embedding_384",
+ "type": "vector(384)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "embedding_768": {
+ "name": "embedding_768",
+ "type": "vector(768)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "embedding_1024": {
+ "name": "embedding_1024",
+ "type": "vector(1024)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "embedding_3072": {
+ "name": "embedding_3072",
+ "type": "vector(3072)",
+ "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
+ }
+ },
+ "embedding_384_vector_hnsw_idx": {
+ "name": "embedding_384_vector_hnsw_idx",
+ "columns": [
+ {
+ "expression": "embedding_384",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last",
+ "opclass": "vector_cosine_ops"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "hnsw",
+ "with": {
+ "m": 16,
+ "ef_construction": 64
+ }
+ },
+ "embedding_768_vector_hnsw_idx": {
+ "name": "embedding_768_vector_hnsw_idx",
+ "columns": [
+ {
+ "expression": "embedding_768",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last",
+ "opclass": "vector_cosine_ops"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "hnsw",
+ "with": {
+ "m": 16,
+ "ef_construction": 64
+ }
+ },
+ "embedding_1024_vector_hnsw_idx": {
+ "name": "embedding_1024_vector_hnsw_idx",
+ "columns": [
+ {
+ "expression": "embedding_1024",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last",
+ "opclass": "vector_cosine_ops"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "hnsw",
+ "with": {
+ "m": 16,
+ "ef_construction": 64
+ }
+ },
+ "embedding_3072_vector_hnsw_idx": {
+ "name": "embedding_3072_vector_hnsw_idx",
+ "columns": [
+ {
+ "expression": "(\"embedding_3072\"::halfvec(3072)) halfvec_cosine_ops",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "hnsw",
+ "with": {
+ "m": 16,
+ "ef_construction": 64
+ }
+ },
+ "emb_kb_tag1_lower_idx": {
+ "name": "emb_kb_tag1_lower_idx",
+ "columns": [
+ {
+ "expression": "knowledge_base_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "lower(\"tag1\")",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_kb_tag2_lower_idx": {
+ "name": "emb_kb_tag2_lower_idx",
+ "columns": [
+ {
+ "expression": "knowledge_base_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "lower(\"tag2\")",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_kb_tag3_lower_idx": {
+ "name": "emb_kb_tag3_lower_idx",
+ "columns": [
+ {
+ "expression": "knowledge_base_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "lower(\"tag3\")",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_kb_tag4_lower_idx": {
+ "name": "emb_kb_tag4_lower_idx",
+ "columns": [
+ {
+ "expression": "knowledge_base_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "lower(\"tag4\")",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_kb_tag5_lower_idx": {
+ "name": "emb_kb_tag5_lower_idx",
+ "columns": [
+ {
+ "expression": "knowledge_base_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "lower(\"tag5\")",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_kb_tag6_lower_idx": {
+ "name": "emb_kb_tag6_lower_idx",
+ "columns": [
+ {
+ "expression": "knowledge_base_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "lower(\"tag6\")",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "emb_kb_tag7_lower_idx": {
+ "name": "emb_kb_tag7_lower_idx",
+ "columns": [
+ {
+ "expression": "knowledge_base_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "lower(\"tag7\")",
+ "asc": true,
+ "isExpression": 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_width_check": {
+ "name": "embedding_width_check",
+ "value": "num_nonnulls(\"embedding\", \"embedding_384\", \"embedding_768\", \"embedding_1024\", \"embedding_3072\") = 1"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.embedding_secret_provenance": {
+ "name": "embedding_secret_provenance",
+ "schema": "",
+ "columns": {
+ "embedding_id": {
+ "name": "embedding_id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "content_hash": {
+ "name": "content_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "entries": {
+ "name": "entries",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "embedding_secret_provenance_embedding_id_embedding_id_fk": {
+ "name": "embedding_secret_provenance_embedding_id_embedding_id_fk",
+ "tableFrom": "embedding_secret_provenance",
+ "tableTo": "embedding",
+ "columnsFrom": ["embedding_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "embedding_secret_provenance_status_check": {
+ "name": "embedding_secret_provenance_status_check",
+ "value": "\"embedding_secret_provenance\".\"status\" IN ('exact', 'unknown')"
+ }
+ },
+ "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.execution_large_value_dependencies": {
+ "name": "execution_large_value_dependencies",
+ "schema": "",
+ "columns": {
+ "parent_key": {
+ "name": "parent_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "child_key": {
+ "name": "child_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "execution_large_value_dependencies_workspace_parent_key_idx": {
+ "name": "execution_large_value_dependencies_workspace_parent_key_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "parent_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "execution_large_value_dependencies_workspace_child_key_idx": {
+ "name": "execution_large_value_dependencies_workspace_child_key_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "child_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "execution_large_value_dependencies_workspace_id_workspace_id_fk": {
+ "name": "execution_large_value_dependencies_workspace_id_workspace_id_fk",
+ "tableFrom": "execution_large_value_dependencies",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "execution_large_value_dependencies_parent_key_child_key_pk": {
+ "name": "execution_large_value_dependencies_parent_key_child_key_pk",
+ "columns": ["parent_key", "child_key"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.execution_large_value_references": {
+ "name": "execution_large_value_references",
+ "schema": "",
+ "columns": {
+ "key": {
+ "name": "key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "execution_id": {
+ "name": "execution_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source": {
+ "name": "source",
+ "type": "execution_large_value_reference_source",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "execution_large_value_references_workspace_execution_source_idx": {
+ "name": "execution_large_value_references_workspace_execution_source_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "execution_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "source",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "execution_large_value_references_workflow_id_idx": {
+ "name": "execution_large_value_references_workflow_id_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "execution_large_value_references_workspace_id_workspace_id_fk": {
+ "name": "execution_large_value_references_workspace_id_workspace_id_fk",
+ "tableFrom": "execution_large_value_references",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "execution_large_value_references_workflow_id_workflow_id_fk": {
+ "name": "execution_large_value_references_workflow_id_workflow_id_fk",
+ "tableFrom": "execution_large_value_references",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "execution_large_value_references_key_execution_id_source_pk": {
+ "name": "execution_large_value_references_key_execution_id_source_pk",
+ "columns": ["key", "execution_id", "source"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.execution_large_values": {
+ "name": "execution_large_values",
+ "schema": "",
+ "columns": {
+ "key": {
+ "name": "key",
+ "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": false
+ },
+ "owner_execution_id": {
+ "name": "owner_execution_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "size": {
+ "name": "size",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "execution_large_values_owner_execution_id_idx": {
+ "name": "execution_large_values_owner_execution_id_idx",
+ "columns": [
+ {
+ "expression": "owner_execution_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "execution_large_values_cleanup_idx": {
+ "name": "execution_large_values_cleanup_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"execution_large_values\".\"deleted_at\" IS NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "execution_large_values_tombstone_cleanup_idx": {
+ "name": "execution_large_values_tombstone_cleanup_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "deleted_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"execution_large_values\".\"deleted_at\" IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "execution_large_values_workflow_id_idx": {
+ "name": "execution_large_values_workflow_id_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "execution_large_values_workspace_id_workspace_id_fk": {
+ "name": "execution_large_values_workspace_id_workspace_id_fk",
+ "tableFrom": "execution_large_values",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "execution_large_values_workflow_id_workflow_id_fk": {
+ "name": "execution_large_values_workflow_id_workflow_id_fk",
+ "tableFrom": "execution_large_values",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.folder": {
+ "name": "folder",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "resource_type": {
+ "name": "resource_type",
+ "type": "folder_resource_type",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "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
+ },
+ "locked": {
+ "name": "locked",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "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()"
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "folder_user_idx": {
+ "name": "folder_user_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "folder_workspace_resource_parent_idx": {
+ "name": "folder_workspace_resource_parent_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "resource_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "parent_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "folder_parent_sort_idx": {
+ "name": "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": {}
+ },
+ "folder_deleted_at_idx": {
+ "name": "folder_deleted_at_idx",
+ "columns": [
+ {
+ "expression": "deleted_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "folder_workspace_deleted_partial_idx": {
+ "name": "folder_workspace_deleted_partial_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "deleted_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"folder\".\"deleted_at\" IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "folder_workspace_resource_parent_name_active_unique": {
+ "name": "folder_workspace_resource_parent_name_active_unique",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "resource_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "coalesce(\"parent_id\", '')",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"folder\".\"deleted_at\" IS NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "folder_user_id_user_id_fk": {
+ "name": "folder_user_id_user_id_fk",
+ "tableFrom": "folder",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "folder_workspace_id_workspace_id_fk": {
+ "name": "folder_workspace_id_workspace_id_fk",
+ "tableFrom": "folder",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "folder_parent_id_folder_id_fk": {
+ "name": "folder_parent_id_folder_id_fk",
+ "tableFrom": "folder",
+ "tableTo": "folder",
+ "columnsFrom": ["parent_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "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
+ },
+ "kind": {
+ "name": "kind",
+ "type": "invitation_kind",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'organization'"
+ },
+ "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": false
+ },
+ "membership_intent": {
+ "name": "membership_intent",
+ "type": "invitation_membership_intent",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'internal'"
+ },
+ "role": {
+ "name": "role",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "invitation_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "token": {
+ "name": "token",
+ "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()"
+ },
+ "updated_at": {
+ "name": "updated_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": {}
+ },
+ "invitation_status_idx": {
+ "name": "invitation_status_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "invitation_pending_email_org_unique": {
+ "name": "invitation_pending_email_org_unique",
+ "columns": [
+ {
+ "expression": "email",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"invitation\".\"status\" = 'pending' AND \"invitation\".\"organization_id\" IS NOT NULL",
+ "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": {
+ "invitation_token_unique": {
+ "name": "invitation_token_unique",
+ "nullsNotDistinct": false,
+ "columns": ["token"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.invitation_workspace_grant": {
+ "name": "invitation_workspace_grant",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "invitation_id": {
+ "name": "invitation_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "permission": {
+ "name": "permission",
+ "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": {
+ "invitation_workspace_grant_unique": {
+ "name": "invitation_workspace_grant_unique",
+ "columns": [
+ {
+ "expression": "invitation_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "invitation_workspace_grant_workspace_id_idx": {
+ "name": "invitation_workspace_grant_workspace_id_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "invitation_workspace_grant_invitation_id_invitation_id_fk": {
+ "name": "invitation_workspace_grant_invitation_id_invitation_id_fk",
+ "tableFrom": "invitation_workspace_grant",
+ "tableTo": "invitation",
+ "columnsFrom": ["invitation_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "invitation_workspace_grant_workspace_id_workspace_id_fk": {
+ "name": "invitation_workspace_grant_workspace_id_workspace_id_fk",
+ "tableFrom": "invitation_workspace_grant",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.job_execution_logs": {
+ "name": "job_execution_logs",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "schedule_id": {
+ "name": "schedule_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
+ },
+ "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
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "job_execution_logs_schedule_id_idx": {
+ "name": "job_execution_logs_schedule_id_idx",
+ "columns": [
+ {
+ "expression": "schedule_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "job_execution_logs_workspace_started_at_idx": {
+ "name": "job_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": {}
+ },
+ "job_execution_logs_workspace_ended_at_id_idx": {
+ "name": "job_execution_logs_workspace_ended_at_id_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "date_trunc('milliseconds', \"ended_at\")",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "job_execution_logs_execution_id_unique": {
+ "name": "job_execution_logs_execution_id_unique",
+ "columns": [
+ {
+ "expression": "execution_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "job_execution_logs_trigger_idx": {
+ "name": "job_execution_logs_trigger_idx",
+ "columns": [
+ {
+ "expression": "trigger",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "job_execution_logs_schedule_id_workflow_schedule_id_fk": {
+ "name": "job_execution_logs_schedule_id_workflow_schedule_id_fk",
+ "tableFrom": "job_execution_logs",
+ "tableTo": "workflow_schedule",
+ "columnsFrom": ["schedule_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "job_execution_logs_workspace_id_workspace_id_fk": {
+ "name": "job_execution_logs_workspace_id_workspace_id_fk",
+ "tableFrom": "job_execution_logs",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_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
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "folder_id": {
+ "name": "folder_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "is_search_index": {
+ "name": "is_search_index",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 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_organization_id_idx": {
+ "name": "kb_organization_id_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "kb_organization_search_index_unique": {
+ "name": "kb_organization_search_index_unique",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"knowledge_base\".\"is_search_index\" = true AND \"knowledge_base\".\"deleted_at\" IS NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "kb_organization_name_active_unique": {
+ "name": "kb_organization_name_active_unique",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"knowledge_base\".\"deleted_at\" IS NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "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_folder_id_idx": {
+ "name": "kb_folder_id_idx",
+ "columns": [
+ {
+ "expression": "folder_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": {}
+ },
+ "kb_workspace_deleted_partial_idx": {
+ "name": "kb_workspace_deleted_partial_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "deleted_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"knowledge_base\".\"deleted_at\" IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "kb_workspace_name_active_unique": {
+ "name": "kb_workspace_name_active_unique",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"knowledge_base\".\"deleted_at\" IS NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "kb_workspace_search_index_unique": {
+ "name": "kb_workspace_search_index_unique",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"knowledge_base\".\"is_search_index\" = true AND \"knowledge_base\".\"deleted_at\" IS NULL",
+ "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": "cascade",
+ "onUpdate": "no action"
+ },
+ "knowledge_base_organization_id_organization_id_fk": {
+ "name": "knowledge_base_organization_id_organization_id_fk",
+ "tableFrom": "knowledge_base",
+ "tableTo": "organization",
+ "columnsFrom": ["organization_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "knowledge_base_folder_id_folder_id_fk": {
+ "name": "knowledge_base_folder_id_folder_id_fk",
+ "tableFrom": "knowledge_base",
+ "tableTo": "folder",
+ "columnsFrom": ["folder_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "kb_owner_check": {
+ "name": "kb_owner_check",
+ "value": "num_nonnulls(\"knowledge_base\".\"workspace_id\", \"knowledge_base\".\"organization_id\") <= 1"
+ },
+ "kb_organization_folder_check": {
+ "name": "kb_organization_folder_check",
+ "value": "\"knowledge_base\".\"organization_id\" IS NULL OR \"knowledge_base\".\"folder_id\" IS NULL"
+ }
+ },
+ "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.knowledge_connector": {
+ "name": "knowledge_connector",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "knowledge_base_id": {
+ "name": "knowledge_base_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "connector_type": {
+ "name": "connector_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "credential_id": {
+ "name": "credential_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "encrypted_api_key": {
+ "name": "encrypted_api_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "source_config": {
+ "name": "source_config",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "sync_mode": {
+ "name": "sync_mode",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'full'"
+ },
+ "sync_interval_minutes": {
+ "name": "sync_interval_minutes",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 1440
+ },
+ "access_mode": {
+ "name": "access_mode",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'workspace'"
+ },
+ "credential_group_id": {
+ "name": "credential_group_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "credential_group_option_id": {
+ "name": "credential_group_option_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "member_sync_status": {
+ "name": "member_sync_status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'idle'"
+ },
+ "member_sync_lock_token": {
+ "name": "member_sync_lock_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "member_sync_lock_lease_at": {
+ "name": "member_sync_lock_lease_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "next_member_sync_at": {
+ "name": "next_member_sync_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_member_sync_at": {
+ "name": "last_member_sync_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_member_sync_error": {
+ "name": "last_member_sync_error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "member_sync_consecutive_failures": {
+ "name": "member_sync_consecutive_failures",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "access_rewrite_pending": {
+ "name": "access_rewrite_pending",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'active'"
+ },
+ "last_sync_at": {
+ "name": "last_sync_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_sync_error": {
+ "name": "last_sync_error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_sync_doc_count": {
+ "name": "last_sync_doc_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "listing_checkpoint": {
+ "name": "listing_checkpoint",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "directory_checkpoint": {
+ "name": "directory_checkpoint",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "next_sync_at": {
+ "name": "next_sync_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "next_directory_sync_at": {
+ "name": "next_directory_sync_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "consecutive_failures": {
+ "name": "consecutive_failures",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "sync_lock_token": {
+ "name": "sync_lock_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sync_lock_lease_at": {
+ "name": "sync_lock_lease_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()"
+ },
+ "archived_at": {
+ "name": "archived_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "kc_knowledge_base_id_idx": {
+ "name": "kc_knowledge_base_id_idx",
+ "columns": [
+ {
+ "expression": "knowledge_base_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "kc_status_next_sync_idx": {
+ "name": "kc_status_next_sync_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "next_sync_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "kc_archived_at_partial_idx": {
+ "name": "kc_archived_at_partial_idx",
+ "columns": [
+ {
+ "expression": "archived_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"knowledge_connector\".\"archived_at\" IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "kc_deleted_at_partial_idx": {
+ "name": "kc_deleted_at_partial_idx",
+ "columns": [
+ {
+ "expression": "deleted_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"knowledge_connector\".\"deleted_at\" IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "kc_member_sync_due_idx": {
+ "name": "kc_member_sync_due_idx",
+ "columns": [
+ {
+ "expression": "member_sync_status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "next_member_sync_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"knowledge_connector\".\"access_mode\" = 'members' AND \"knowledge_connector\".\"deleted_at\" IS NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "kc_directory_sync_due_idx": {
+ "name": "kc_directory_sync_due_idx",
+ "columns": [
+ {
+ "expression": "next_directory_sync_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"knowledge_connector\".\"access_mode\" = 'admin' AND \"knowledge_connector\".\"deleted_at\" IS NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "knowledge_connector_knowledge_base_id_knowledge_base_id_fk": {
+ "name": "knowledge_connector_knowledge_base_id_knowledge_base_id_fk",
+ "tableFrom": "knowledge_connector",
+ "tableTo": "knowledge_base",
+ "columnsFrom": ["knowledge_base_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "knowledge_connector_credential_group_id_credential_group_id_fk": {
+ "name": "knowledge_connector_credential_group_id_credential_group_id_fk",
+ "tableFrom": "knowledge_connector",
+ "tableTo": "credential_group",
+ "columnsFrom": ["credential_group_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "kc_access_mode_check": {
+ "name": "kc_access_mode_check",
+ "value": "\"knowledge_connector\".\"access_mode\" IN ('workspace', 'members', 'admin')"
+ },
+ "kc_member_sync_status_check": {
+ "name": "kc_member_sync_status_check",
+ "value": "\"knowledge_connector\".\"member_sync_status\" IN ('idle', 'pending', 'running', 'error', 'disabled')"
+ },
+ "kc_sync_lock_exclusive_check": {
+ "name": "kc_sync_lock_exclusive_check",
+ "value": "NOT (\"knowledge_connector\".\"sync_lock_token\" IS NOT NULL AND \"knowledge_connector\".\"member_sync_lock_token\" IS NOT NULL)"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.knowledge_connector_member": {
+ "name": "knowledge_connector_member",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "connector_id": {
+ "name": "connector_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "credential_id": {
+ "name": "credential_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "subject_token": {
+ "name": "subject_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'active'"
+ },
+ "consecutive_failures": {
+ "name": "consecutive_failures",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "next_attempt_at": {
+ "name": "next_attempt_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_started_at": {
+ "name": "last_started_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_complete_listing_at": {
+ "name": "last_complete_listing_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_listed_count": {
+ "name": "last_listed_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_error": {
+ "name": "last_error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "member_synced_through": {
+ "name": "member_synced_through",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "change_cursor": {
+ "name": "change_cursor",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "listing_checkpoint": {
+ "name": "listing_checkpoint",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "suspended_at": {
+ "name": "suspended_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": {
+ "kcm_organization_id_idx": {
+ "name": "kcm_organization_id_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "kcm_connector_credential_unique": {
+ "name": "kcm_connector_credential_unique",
+ "columns": [
+ {
+ "expression": "connector_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "credential_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "kcm_connector_queue_idx": {
+ "name": "kcm_connector_queue_idx",
+ "columns": [
+ {
+ "expression": "connector_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "next_attempt_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "first"
+ },
+ {
+ "expression": "last_started_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "first"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "kcm_credential_idx": {
+ "name": "kcm_credential_idx",
+ "columns": [
+ {
+ "expression": "credential_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "knowledge_connector_member_workspace_id_workspace_id_fk": {
+ "name": "knowledge_connector_member_workspace_id_workspace_id_fk",
+ "tableFrom": "knowledge_connector_member",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "knowledge_connector_member_organization_id_organization_id_fk": {
+ "name": "knowledge_connector_member_organization_id_organization_id_fk",
+ "tableFrom": "knowledge_connector_member",
+ "tableTo": "organization",
+ "columnsFrom": ["organization_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "knowledge_connector_member_connector_id_knowledge_connector_id_fk": {
+ "name": "knowledge_connector_member_connector_id_knowledge_connector_id_fk",
+ "tableFrom": "knowledge_connector_member",
+ "tableTo": "knowledge_connector",
+ "columnsFrom": ["connector_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "knowledge_connector_member_credential_id_credential_id_fk": {
+ "name": "knowledge_connector_member_credential_id_credential_id_fk",
+ "tableFrom": "knowledge_connector_member",
+ "tableTo": "credential",
+ "columnsFrom": ["credential_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "kcm_owner_check": {
+ "name": "kcm_owner_check",
+ "value": "num_nonnulls(\"knowledge_connector_member\".\"workspace_id\", \"knowledge_connector_member\".\"organization_id\") = 1"
+ },
+ "kcm_status_check": {
+ "name": "kcm_status_check",
+ "value": "\"knowledge_connector_member\".\"status\" IN ('active', 'suspended', 'disabled')"
+ },
+ "kcm_subject_token_shape_check": {
+ "name": "kcm_subject_token_shape_check",
+ "value": "\"knowledge_connector_member\".\"subject_token\" ~ '^s:[^:]+:[^:]+:.+$'"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.knowledge_connector_member_sync_log": {
+ "name": "knowledge_connector_member_sync_log",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "connector_id": {
+ "name": "connector_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "started_at": {
+ "name": "started_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "completed_at": {
+ "name": "completed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "members_claimed": {
+ "name": "members_claimed",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "members_completed": {
+ "name": "members_completed",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "members_incomplete": {
+ "name": "members_incomplete",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "members_failed": {
+ "name": "members_failed",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "docs_listed": {
+ "name": "docs_listed",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "docs_added": {
+ "name": "docs_added",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "docs_updated": {
+ "name": "docs_updated",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "docs_unchanged": {
+ "name": "docs_unchanged",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "docs_hydrated_once": {
+ "name": "docs_hydrated_once",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "observations_added": {
+ "name": "observations_added",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "observations_removed": {
+ "name": "observations_removed",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "docs_tombstoned": {
+ "name": "docs_tombstoned",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "docs_resurrected": {
+ "name": "docs_resurrected",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "docs_purged": {
+ "name": "docs_purged",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "credentials_audited": {
+ "name": "credentials_audited",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "error_message": {
+ "name": "error_message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "kcmsl_connector_started_at_idx": {
+ "name": "kcmsl_connector_started_at_idx",
+ "columns": [
+ {
+ "expression": "connector_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "\"started_at\" DESC",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "kcmsl_started_at_partial_idx": {
+ "name": "kcmsl_started_at_partial_idx",
+ "columns": [
+ {
+ "expression": "started_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"knowledge_connector_member_sync_log\".\"status\" = 'started'",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "knowledge_connector_member_sync_log_connector_id_knowledge_connector_id_fk": {
+ "name": "knowledge_connector_member_sync_log_connector_id_knowledge_connector_id_fk",
+ "tableFrom": "knowledge_connector_member_sync_log",
+ "tableTo": "knowledge_connector",
+ "columnsFrom": ["connector_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "kcmsl_status_check": {
+ "name": "kcmsl_status_check",
+ "value": "\"knowledge_connector_member_sync_log\".\"status\" IN ('started', 'partial', 'completed', 'failed')"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.knowledge_connector_sync_log": {
+ "name": "knowledge_connector_sync_log",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "connector_id": {
+ "name": "connector_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "started_at": {
+ "name": "started_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "completed_at": {
+ "name": "completed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "docs_added": {
+ "name": "docs_added",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "docs_updated": {
+ "name": "docs_updated",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "docs_deleted": {
+ "name": "docs_deleted",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "docs_unchanged": {
+ "name": "docs_unchanged",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "docs_skipped": {
+ "name": "docs_skipped",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "docs_failed": {
+ "name": "docs_failed",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "listed_count": {
+ "name": "listed_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "error_message": {
+ "name": "error_message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "kcsl_connector_started_at_idx": {
+ "name": "kcsl_connector_started_at_idx",
+ "columns": [
+ {
+ "expression": "connector_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "\"started_at\" DESC",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "kcsl_started_at_partial_idx": {
+ "name": "kcsl_started_at_partial_idx",
+ "columns": [
+ {
+ "expression": "started_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"knowledge_connector_sync_log\".\"status\" = 'started'",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "knowledge_connector_sync_log_connector_id_knowledge_connector_id_fk": {
+ "name": "knowledge_connector_sync_log_connector_id_knowledge_connector_id_fk",
+ "tableFrom": "knowledge_connector_sync_log",
+ "tableTo": "knowledge_connector",
+ "columnsFrom": ["connector_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.knowledge_document_observation": {
+ "name": "knowledge_document_observation",
+ "schema": "",
+ "columns": {
+ "document_id": {
+ "name": "document_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "member_id": {
+ "name": "member_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "last_seen_at": {
+ "name": "last_seen_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "run_id": {
+ "name": "run_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "kdo_member_idx": {
+ "name": "kdo_member_idx",
+ "columns": [
+ {
+ "expression": "member_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "knowledge_document_observation_document_id_document_id_fk": {
+ "name": "knowledge_document_observation_document_id_document_id_fk",
+ "tableFrom": "knowledge_document_observation",
+ "tableTo": "document",
+ "columnsFrom": ["document_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "knowledge_document_observation_member_id_knowledge_connector_member_id_fk": {
+ "name": "knowledge_document_observation_member_id_knowledge_connector_member_id_fk",
+ "tableFrom": "knowledge_document_observation",
+ "tableTo": "knowledge_connector_member",
+ "columnsFrom": ["member_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "knowledge_document_observation_document_id_member_id_pk": {
+ "name": "knowledge_document_observation_document_id_member_id_pk",
+ "columns": ["document_id", "member_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.knowledge_external_directory": {
+ "name": "knowledge_external_directory",
+ "schema": "",
+ "columns": {
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "provider_id": {
+ "name": "provider_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "tenant_id": {
+ "name": "tenant_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "sync_lock_token": {
+ "name": "sync_lock_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sync_lock_lease_at": {
+ "name": "sync_lock_lease_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_started_at": {
+ "name": "last_started_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_complete_sync_at": {
+ "name": "last_complete_sync_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "ked_organization_id_idx": {
+ "name": "ked_organization_id_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "ked_workspace_identity_unique": {
+ "name": "ked_workspace_identity_unique",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "provider_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "tenant_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "ked_organization_identity_unique": {
+ "name": "ked_organization_identity_unique",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "provider_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "tenant_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "knowledge_external_directory_workspace_id_workspace_id_fk": {
+ "name": "knowledge_external_directory_workspace_id_workspace_id_fk",
+ "tableFrom": "knowledge_external_directory",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "knowledge_external_directory_organization_id_organization_id_fk": {
+ "name": "knowledge_external_directory_organization_id_organization_id_fk",
+ "tableFrom": "knowledge_external_directory",
+ "tableTo": "organization",
+ "columnsFrom": ["organization_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "ked_owner_check": {
+ "name": "ked_owner_check",
+ "value": "num_nonnulls(\"knowledge_external_directory\".\"workspace_id\", \"knowledge_external_directory\".\"organization_id\") = 1"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.knowledge_external_group": {
+ "name": "knowledge_external_group",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "provider_id": {
+ "name": "provider_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "tenant_id": {
+ "name": "tenant_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "external_group_id": {
+ "name": "external_group_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "last_synced_at": {
+ "name": "last_synced_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": {
+ "keg_organization_id_idx": {
+ "name": "keg_organization_id_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "keg_organization_identity_unique": {
+ "name": "keg_organization_identity_unique",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "provider_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "tenant_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "external_group_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "keg_organization_synced_idx": {
+ "name": "keg_organization_synced_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "last_synced_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "first"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "keg_identity_unique": {
+ "name": "keg_identity_unique",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "provider_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "tenant_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "external_group_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "keg_workspace_synced_idx": {
+ "name": "keg_workspace_synced_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "last_synced_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "first"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "knowledge_external_group_organization_id_organization_id_fk": {
+ "name": "knowledge_external_group_organization_id_organization_id_fk",
+ "tableFrom": "knowledge_external_group",
+ "tableTo": "organization",
+ "columnsFrom": ["organization_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "keg_workspace_fk": {
+ "name": "keg_workspace_fk",
+ "tableFrom": "knowledge_external_group",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "keg_owner_check": {
+ "name": "keg_owner_check",
+ "value": "num_nonnulls(\"knowledge_external_group\".\"workspace_id\", \"knowledge_external_group\".\"organization_id\") = 1"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.knowledge_external_group_member": {
+ "name": "knowledge_external_group_member",
+ "schema": "",
+ "columns": {
+ "group_id": {
+ "name": "group_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "subject_token": {
+ "name": "subject_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "kegm_subject_token_idx": {
+ "name": "kegm_subject_token_idx",
+ "columns": [
+ {
+ "expression": "subject_token",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "kegm_group_fk": {
+ "name": "kegm_group_fk",
+ "tableFrom": "knowledge_external_group_member",
+ "tableTo": "knowledge_external_group",
+ "columnsFrom": ["group_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "knowledge_external_group_member_group_id_subject_token_pk": {
+ "name": "knowledge_external_group_member_group_id_subject_token_pk",
+ "columns": ["group_id", "subject_token"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mcp_server_oauth": {
+ "name": "mcp_server_oauth",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "mcp_server_id": {
+ "name": "mcp_server_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "client_information": {
+ "name": "client_information",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tokens": {
+ "name": "tokens",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "code_verifier": {
+ "name": "code_verifier",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "state": {
+ "name": "state",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "state_created_at": {
+ "name": "state_created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_refreshed_at": {
+ "name": "last_refreshed_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_server_oauth_server_unique": {
+ "name": "mcp_server_oauth_server_unique",
+ "columns": [
+ {
+ "expression": "mcp_server_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "mcp_server_oauth_state_idx": {
+ "name": "mcp_server_oauth_state_idx",
+ "columns": [
+ {
+ "expression": "state",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "mcp_server_oauth_mcp_server_id_mcp_servers_id_fk": {
+ "name": "mcp_server_oauth_mcp_server_id_mcp_servers_id_fk",
+ "tableFrom": "mcp_server_oauth",
+ "tableTo": "mcp_servers",
+ "columnsFrom": ["mcp_server_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "mcp_server_oauth_user_id_user_id_fk": {
+ "name": "mcp_server_oauth_user_id_user_id_fk",
+ "tableFrom": "mcp_server_oauth",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "mcp_server_oauth_workspace_id_workspace_id_fk": {
+ "name": "mcp_server_oauth_workspace_id_workspace_id_fk",
+ "tableFrom": "mcp_server_oauth",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "mcp_server_oauth_organization_id_organization_id_fk": {
+ "name": "mcp_server_oauth_organization_id_organization_id_fk",
+ "tableFrom": "mcp_server_oauth",
+ "tableTo": "organization",
+ "columnsFrom": ["organization_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "mcp_server_oauth_owner_check": {
+ "name": "mcp_server_oauth_owner_check",
+ "value": "num_nonnulls(\"mcp_server_oauth\".\"workspace_id\", \"mcp_server_oauth\".\"organization_id\") = 1"
+ }
+ },
+ "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": false
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "credential_group_id": {
+ "name": "credential_group_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "managed_connector_id": {
+ "name": "managed_connector_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "oauth_config_version": {
+ "name": "oauth_config_version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 1
+ },
+ "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
+ },
+ "auth_type": {
+ "name": "auth_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'headers'"
+ },
+ "oauth_client_id": {
+ "name": "oauth_client_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "oauth_client_secret": {
+ "name": "oauth_client_secret",
+ "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_organization_id_idx": {
+ "name": "mcp_servers_organization_id_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "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_credential_group_idx": {
+ "name": "mcp_servers_credential_group_idx",
+ "columns": [
+ {
+ "expression": "credential_group_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "mcp_servers_credential_group_managed_connector_unique": {
+ "name": "mcp_servers_credential_group_managed_connector_unique",
+ "columns": [
+ {
+ "expression": "credential_group_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "managed_connector_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"mcp_servers\".\"credential_group_id\" IS NOT NULL AND \"mcp_servers\".\"managed_connector_id\" IS NOT NULL AND \"mcp_servers\".\"deleted_at\" IS NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "mcp_servers_workspace_deleted_partial_idx": {
+ "name": "mcp_servers_workspace_deleted_partial_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "deleted_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"mcp_servers\".\"deleted_at\" IS NOT NULL",
+ "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_organization_id_organization_id_fk": {
+ "name": "mcp_servers_organization_id_organization_id_fk",
+ "tableFrom": "mcp_servers",
+ "tableTo": "organization",
+ "columnsFrom": ["organization_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "mcp_servers_credential_group_id_credential_group_id_fk": {
+ "name": "mcp_servers_credential_group_id_credential_group_id_fk",
+ "tableFrom": "mcp_servers",
+ "tableTo": "credential_group",
+ "columnsFrom": ["credential_group_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "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": {
+ "mcp_servers_owner_check": {
+ "name": "mcp_servers_owner_check",
+ "value": "num_nonnulls(\"mcp_servers\".\"workspace_id\", \"mcp_servers\".\"organization_id\") = 1"
+ },
+ "mcp_servers_organization_managed_check": {
+ "name": "mcp_servers_organization_managed_check",
+ "value": "\"mcp_servers\".\"organization_id\" IS NULL OR \"mcp_servers\".\"credential_group_id\" IS NOT NULL"
+ },
+ "mcp_servers_credential_group_managed_connector_check": {
+ "name": "mcp_servers_credential_group_managed_connector_check",
+ "value": "\"mcp_servers\".\"credential_group_id\" IS NULL OR \"mcp_servers\".\"managed_connector_id\" IS NOT NULL"
+ },
+ "mcp_servers_managed_connector_oauth_check": {
+ "name": "mcp_servers_managed_connector_oauth_check",
+ "value": "\"mcp_servers\".\"managed_connector_id\" IS NULL OR \"mcp_servers\".\"auth_type\" = 'oauth'"
+ }
+ },
+ "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
+ },
+ "secret_provenance_version": {
+ "name": "secret_provenance_version",
+ "type": "integer",
+ "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()"
+ },
+ "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": {}
+ },
+ "memory_workspace_deleted_partial_idx": {
+ "name": "memory_workspace_deleted_partial_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "deleted_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"memory\".\"deleted_at\" IS NOT NULL",
+ "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.memory_secret_provenance": {
+ "name": "memory_secret_provenance",
+ "schema": "",
+ "columns": {
+ "memory_id": {
+ "name": "memory_id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "content_hash": {
+ "name": "content_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "entries": {
+ "name": "entries",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "memory_secret_provenance_memory_id_memory_id_fk": {
+ "name": "memory_secret_provenance_memory_id_memory_id_fk",
+ "tableFrom": "memory_secret_provenance",
+ "tableTo": "memory",
+ "columnsFrom": ["memory_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "memory_secret_provenance_status_check": {
+ "name": "memory_secret_provenance_status_check",
+ "value": "\"memory_secret_provenance\".\"status\" IN ('exact', 'unknown')"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.mothership_inbox_allowed_sender": {
+ "name": "mothership_inbox_allowed_sender",
+ "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
+ },
+ "label": {
+ "name": "label",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "added_by": {
+ "name": "added_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "inbox_sender_ws_email_idx": {
+ "name": "inbox_sender_ws_email_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "email",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "mothership_inbox_allowed_sender_workspace_id_workspace_id_fk": {
+ "name": "mothership_inbox_allowed_sender_workspace_id_workspace_id_fk",
+ "tableFrom": "mothership_inbox_allowed_sender",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "mothership_inbox_allowed_sender_added_by_user_id_fk": {
+ "name": "mothership_inbox_allowed_sender_added_by_user_id_fk",
+ "tableFrom": "mothership_inbox_allowed_sender",
+ "tableTo": "user",
+ "columnsFrom": ["added_by"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mothership_inbox_task": {
+ "name": "mothership_inbox_task",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "from_email": {
+ "name": "from_email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "from_name": {
+ "name": "from_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "subject": {
+ "name": "subject",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "body_preview": {
+ "name": "body_preview",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "body_text": {
+ "name": "body_text",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "body_html": {
+ "name": "body_html",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "email_message_id": {
+ "name": "email_message_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "in_reply_to": {
+ "name": "in_reply_to",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "response_message_id": {
+ "name": "response_message_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "agentmail_message_id": {
+ "name": "agentmail_message_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'received'"
+ },
+ "chat_id": {
+ "name": "chat_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "trigger_job_id": {
+ "name": "trigger_job_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "result_summary": {
+ "name": "result_summary",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "error_message": {
+ "name": "error_message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "rejection_reason": {
+ "name": "rejection_reason",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "has_attachments": {
+ "name": "has_attachments",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "cc_recipients": {
+ "name": "cc_recipients",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "processing_started_at": {
+ "name": "processing_started_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "completed_at": {
+ "name": "completed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "inbox_task_ws_created_at_idx": {
+ "name": "inbox_task_ws_created_at_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "inbox_task_ws_status_idx": {
+ "name": "inbox_task_ws_status_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "inbox_task_response_msg_id_idx": {
+ "name": "inbox_task_response_msg_id_idx",
+ "columns": [
+ {
+ "expression": "response_message_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "inbox_task_email_msg_id_idx": {
+ "name": "inbox_task_email_msg_id_idx",
+ "columns": [
+ {
+ "expression": "email_message_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "mothership_inbox_task_workspace_id_workspace_id_fk": {
+ "name": "mothership_inbox_task_workspace_id_workspace_id_fk",
+ "tableFrom": "mothership_inbox_task",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "mothership_inbox_task_chat_id_copilot_chats_id_fk": {
+ "name": "mothership_inbox_task_chat_id_copilot_chats_id_fk",
+ "tableFrom": "mothership_inbox_task",
+ "tableTo": "copilot_chats",
+ "columnsFrom": ["chat_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mothership_inbox_webhook": {
+ "name": "mothership_inbox_webhook",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "webhook_id": {
+ "name": "webhook_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "secret": {
+ "name": "secret",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "mothership_inbox_webhook_workspace_id_workspace_id_fk": {
+ "name": "mothership_inbox_webhook_workspace_id_workspace_id_fk",
+ "tableFrom": "mothership_inbox_webhook",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "mothership_inbox_webhook_workspace_id_unique": {
+ "name": "mothership_inbox_webhook_workspace_id_unique",
+ "nullsNotDistinct": false,
+ "columns": ["workspace_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mothership_settings": {
+ "name": "mothership_settings",
+ "schema": "",
+ "columns": {
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "mcp_tool_refs": {
+ "name": "mcp_tool_refs",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "custom_tool_refs": {
+ "name": "custom_tool_refs",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "skill_refs": {
+ "name": "skill_refs",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "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": {
+ "mothership_settings_workspace_id_workspace_id_fk": {
+ "name": "mothership_settings_workspace_id_workspace_id_fk",
+ "tableFrom": "mothership_settings",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.oauth_access_token": {
+ "name": "oauth_access_token",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "token": {
+ "name": "token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "client_id": {
+ "name": "client_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "session_id": {
+ "name": "session_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "reference_id": {
+ "name": "reference_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "refresh_id": {
+ "name": "refresh_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
+ },
+ "scopes": {
+ "name": "scopes",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "oauth_access_token_client_id_idx": {
+ "name": "oauth_access_token_client_id_idx",
+ "columns": [
+ {
+ "expression": "client_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "oauth_access_token_session_id_idx": {
+ "name": "oauth_access_token_session_id_idx",
+ "columns": [
+ {
+ "expression": "session_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "oauth_access_token_refresh_id_idx": {
+ "name": "oauth_access_token_refresh_id_idx",
+ "columns": [
+ {
+ "expression": "refresh_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "oauth_access_token_user_client_idx": {
+ "name": "oauth_access_token_user_client_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "client_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "oauth_access_token_expires_at_idx": {
+ "name": "oauth_access_token_expires_at_idx",
+ "columns": [
+ {
+ "expression": "expires_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "oauth_access_token_client_id_oauth_client_client_id_fk": {
+ "name": "oauth_access_token_client_id_oauth_client_client_id_fk",
+ "tableFrom": "oauth_access_token",
+ "tableTo": "oauth_client",
+ "columnsFrom": ["client_id"],
+ "columnsTo": ["client_id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "oauth_access_token_session_id_session_id_fk": {
+ "name": "oauth_access_token_session_id_session_id_fk",
+ "tableFrom": "oauth_access_token",
+ "tableTo": "session",
+ "columnsFrom": ["session_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "oauth_access_token_user_id_user_id_fk": {
+ "name": "oauth_access_token_user_id_user_id_fk",
+ "tableFrom": "oauth_access_token",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "oauth_access_token_refresh_id_oauth_refresh_token_id_fk": {
+ "name": "oauth_access_token_refresh_id_oauth_refresh_token_id_fk",
+ "tableFrom": "oauth_access_token",
+ "tableTo": "oauth_refresh_token",
+ "columnsFrom": ["refresh_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "oauth_access_token_token_unique": {
+ "name": "oauth_access_token_token_unique",
+ "nullsNotDistinct": false,
+ "columns": ["token"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.oauth_client": {
+ "name": "oauth_client",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "client_id": {
+ "name": "client_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "client_secret": {
+ "name": "client_secret",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "disabled": {
+ "name": "disabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "skip_consent": {
+ "name": "skip_consent",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "enable_end_session": {
+ "name": "enable_end_session",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "subject_type": {
+ "name": "subject_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "scopes": {
+ "name": "scopes",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "uri": {
+ "name": "uri",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "icon": {
+ "name": "icon",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "contacts": {
+ "name": "contacts",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tos": {
+ "name": "tos",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "policy": {
+ "name": "policy",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "software_id": {
+ "name": "software_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "software_version": {
+ "name": "software_version",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "software_statement": {
+ "name": "software_statement",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "redirect_uris": {
+ "name": "redirect_uris",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "post_logout_redirect_uris": {
+ "name": "post_logout_redirect_uris",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "token_endpoint_auth_method": {
+ "name": "token_endpoint_auth_method",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "grant_types": {
+ "name": "grant_types",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "response_types": {
+ "name": "response_types",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "public": {
+ "name": "public",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "require_pkce": {
+ "name": "require_pkce",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "reference_id": {
+ "name": "reference_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "oauth_client_user_id_idx": {
+ "name": "oauth_client_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "oauth_client_user_id_user_id_fk": {
+ "name": "oauth_client_user_id_user_id_fk",
+ "tableFrom": "oauth_client",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "oauth_client_client_id_unique": {
+ "name": "oauth_client_client_id_unique",
+ "nullsNotDistinct": false,
+ "columns": ["client_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.oauth_consent": {
+ "name": "oauth_consent",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "client_id": {
+ "name": "client_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "reference_id": {
+ "name": "reference_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "scopes": {
+ "name": "scopes",
+ "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
+ }
+ },
+ "indexes": {
+ "oauth_consent_client_id_idx": {
+ "name": "oauth_consent_client_id_idx",
+ "columns": [
+ {
+ "expression": "client_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "oauth_consent_client_id_oauth_client_client_id_fk": {
+ "name": "oauth_consent_client_id_oauth_client_client_id_fk",
+ "tableFrom": "oauth_consent",
+ "tableTo": "oauth_client",
+ "columnsFrom": ["client_id"],
+ "columnsTo": ["client_id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "oauth_consent_user_id_user_id_fk": {
+ "name": "oauth_consent_user_id_user_id_fk",
+ "tableFrom": "oauth_consent",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "oauth_consent_user_client_reference_unique": {
+ "name": "oauth_consent_user_client_reference_unique",
+ "nullsNotDistinct": true,
+ "columns": ["user_id", "client_id", "reference_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.oauth_refresh_token": {
+ "name": "oauth_refresh_token",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "token": {
+ "name": "token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "client_id": {
+ "name": "client_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "session_id": {
+ "name": "session_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "reference_id": {
+ "name": "reference_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
+ },
+ "revoked": {
+ "name": "revoked",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "auth_time": {
+ "name": "auth_time",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "scopes": {
+ "name": "scopes",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "family_id": {
+ "name": "family_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "generation": {
+ "name": "generation",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "oauth_refresh_token_client_id_idx": {
+ "name": "oauth_refresh_token_client_id_idx",
+ "columns": [
+ {
+ "expression": "client_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "oauth_refresh_token_session_id_idx": {
+ "name": "oauth_refresh_token_session_id_idx",
+ "columns": [
+ {
+ "expression": "session_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "oauth_refresh_token_user_client_idx": {
+ "name": "oauth_refresh_token_user_client_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "client_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "oauth_refresh_token_expires_at_idx": {
+ "name": "oauth_refresh_token_expires_at_idx",
+ "columns": [
+ {
+ "expression": "expires_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "oauth_refresh_token_client_id_oauth_client_client_id_fk": {
+ "name": "oauth_refresh_token_client_id_oauth_client_client_id_fk",
+ "tableFrom": "oauth_refresh_token",
+ "tableTo": "oauth_client",
+ "columnsFrom": ["client_id"],
+ "columnsTo": ["client_id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "oauth_refresh_token_session_id_session_id_fk": {
+ "name": "oauth_refresh_token_session_id_session_id_fk",
+ "tableFrom": "oauth_refresh_token",
+ "tableTo": "session",
+ "columnsFrom": ["session_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "oauth_refresh_token_user_id_user_id_fk": {
+ "name": "oauth_refresh_token_user_id_user_id_fk",
+ "tableFrom": "oauth_refresh_token",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "oauth_refresh_token_family_id_oauth_token_family_id_fk": {
+ "name": "oauth_refresh_token_family_id_oauth_token_family_id_fk",
+ "tableFrom": "oauth_refresh_token",
+ "tableTo": "oauth_token_family",
+ "columnsFrom": ["family_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "oauth_refresh_token_token_unique": {
+ "name": "oauth_refresh_token_token_unique",
+ "nullsNotDistinct": false,
+ "columns": ["token"]
+ },
+ "oauth_refresh_token_family_generation_unique": {
+ "name": "oauth_refresh_token_family_generation_unique",
+ "nullsNotDistinct": false,
+ "columns": ["family_id", "generation"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {
+ "oauth_refresh_token_generation_check": {
+ "name": "oauth_refresh_token_generation_check",
+ "value": "\"oauth_refresh_token\".\"generation\" BETWEEN 0 AND 1000"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.oauth_token_family": {
+ "name": "oauth_token_family",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "client_id": {
+ "name": "client_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "session_id": {
+ "name": "session_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "reference_id": {
+ "name": "reference_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "consent_id": {
+ "name": "consent_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "current_generation": {
+ "name": "current_generation",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "oauth_token_family_client_id_idx": {
+ "name": "oauth_token_family_client_id_idx",
+ "columns": [
+ {
+ "expression": "client_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "oauth_token_family_session_id_idx": {
+ "name": "oauth_token_family_session_id_idx",
+ "columns": [
+ {
+ "expression": "session_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "oauth_token_family_user_client_idx": {
+ "name": "oauth_token_family_user_client_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "client_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "oauth_token_family_consent_id_idx": {
+ "name": "oauth_token_family_consent_id_idx",
+ "columns": [
+ {
+ "expression": "consent_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "oauth_token_family_expires_at_idx": {
+ "name": "oauth_token_family_expires_at_idx",
+ "columns": [
+ {
+ "expression": "expires_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "oauth_token_family_client_id_oauth_client_client_id_fk": {
+ "name": "oauth_token_family_client_id_oauth_client_client_id_fk",
+ "tableFrom": "oauth_token_family",
+ "tableTo": "oauth_client",
+ "columnsFrom": ["client_id"],
+ "columnsTo": ["client_id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "oauth_token_family_session_id_session_id_fk": {
+ "name": "oauth_token_family_session_id_session_id_fk",
+ "tableFrom": "oauth_token_family",
+ "tableTo": "session",
+ "columnsFrom": ["session_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "oauth_token_family_user_id_user_id_fk": {
+ "name": "oauth_token_family_user_id_user_id_fk",
+ "tableFrom": "oauth_token_family",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "oauth_token_family_consent_id_oauth_consent_id_fk": {
+ "name": "oauth_token_family_consent_id_oauth_consent_id_fk",
+ "tableFrom": "oauth_token_family",
+ "tableTo": "oauth_consent",
+ "columnsFrom": ["consent_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "oauth_token_family_generation_check": {
+ "name": "oauth_token_family_generation_check",
+ "value": "\"oauth_token_family\".\"current_generation\" BETWEEN 0 AND 1000"
+ }
+ },
+ "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
+ },
+ "session_policy_settings": {
+ "name": "session_policy_settings",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "security_policy_version": {
+ "name": "security_policy_version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 1
+ },
+ "whitelabel_settings": {
+ "name": "whitelabel_settings",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "data_retention_settings": {
+ "name": "data_retention_settings",
+ "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
+ },
+ "limit_notifications": {
+ "name": "limit_notifications",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::jsonb"
+ },
+ "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.organization_byok_keys": {
+ "name": "organization_byok_keys",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_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
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": 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": {
+ "organization_byok_organization_provider_idx": {
+ "name": "organization_byok_organization_provider_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "provider_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "organization_byok_keys_organization_id_organization_id_fk": {
+ "name": "organization_byok_keys_organization_id_organization_id_fk",
+ "tableFrom": "organization_byok_keys",
+ "tableTo": "organization",
+ "columnsFrom": ["organization_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "organization_byok_keys_created_by_user_id_fk": {
+ "name": "organization_byok_keys_created_by_user_id_fk",
+ "tableFrom": "organization_byok_keys",
+ "tableTo": "user",
+ "columnsFrom": ["created_by"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.organization_member_usage_limit": {
+ "name": "organization_member_usage_limit",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "usage_limit": {
+ "name": "usage_limit",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "set_by": {
+ "name": "set_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": {
+ "org_member_usage_limit_org_user_unique": {
+ "name": "org_member_usage_limit_org_user_unique",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "org_member_usage_limit_organization_id_idx": {
+ "name": "org_member_usage_limit_organization_id_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "organization_member_usage_limit_organization_id_organization_id_fk": {
+ "name": "organization_member_usage_limit_organization_id_organization_id_fk",
+ "tableFrom": "organization_member_usage_limit",
+ "tableTo": "organization",
+ "columnsFrom": ["organization_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "organization_member_usage_limit_user_id_user_id_fk": {
+ "name": "organization_member_usage_limit_user_id_user_id_fk",
+ "tableFrom": "organization_member_usage_limit",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "organization_member_usage_limit_set_by_user_id_fk": {
+ "name": "organization_member_usage_limit_set_by_user_id_fk",
+ "tableFrom": "organization_member_usage_limit",
+ "tableTo": "user",
+ "columnsFrom": ["set_by"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.organization_search_integration": {
+ "name": "organization_search_integration",
+ "schema": "",
+ "columns": {
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "connector_type": {
+ "name": "connector_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "approved": {
+ "name": "approved",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "organization_search_integration_organization_id_organization_id_fk": {
+ "name": "organization_search_integration_organization_id_organization_id_fk",
+ "tableFrom": "organization_search_integration",
+ "tableTo": "organization",
+ "columnsFrom": ["organization_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "organization_search_integration_organization_id_connector_type_pk": {
+ "name": "organization_search_integration_organization_id_connector_type_pk",
+ "columns": ["organization_id", "connector_type"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.outbox_event": {
+ "name": "outbox_event",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "event_type": {
+ "name": "event_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "payload": {
+ "name": "payload",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "attempts": {
+ "name": "attempts",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "max_attempts": {
+ "name": "max_attempts",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 10
+ },
+ "available_at": {
+ "name": "available_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "locked_at": {
+ "name": "locked_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_error": {
+ "name": "last_error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "processed_at": {
+ "name": "processed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "outbox_event_status_available_idx": {
+ "name": "outbox_event_status_available_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "available_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "outbox_event_locked_at_idx": {
+ "name": "outbox_event_locked_at_idx",
+ "columns": [
+ {
+ "expression": "locked_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "outbox_event_type_created_idx": {
+ "name": "outbox_event_type_created_idx",
+ "columns": [
+ {
+ "expression": "event_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "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
+ },
+ "automatic_resume_retry_count": {
+ "name": "automatic_resume_retry_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
+ },
+ "next_resume_at": {
+ "name": "next_resume_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": {}
+ },
+ "paused_executions_next_resume_at_idx": {
+ "name": "paused_executions_next_resume_at_idx",
+ "columns": [
+ {
+ "expression": "next_resume_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "status = 'paused' AND next_resume_at IS NOT NULL",
+ "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.pending_credential_draft": {
+ "name": "pending_credential_draft",
+ "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
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "provider_id": {
+ "name": "provider_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "display_name": {
+ "name": "display_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "credential_id": {
+ "name": "credential_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "oauth_config": {
+ "name": "oauth_config",
+ "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()"
+ }
+ },
+ "indexes": {
+ "pending_draft_organization_id_idx": {
+ "name": "pending_draft_organization_id_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "pending_draft_user_provider_org": {
+ "name": "pending_draft_user_provider_org",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "provider_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "pending_draft_user_provider_ws": {
+ "name": "pending_draft_user_provider_ws",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "provider_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "pending_credential_draft_user_id_user_id_fk": {
+ "name": "pending_credential_draft_user_id_user_id_fk",
+ "tableFrom": "pending_credential_draft",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "pending_credential_draft_workspace_id_workspace_id_fk": {
+ "name": "pending_credential_draft_workspace_id_workspace_id_fk",
+ "tableFrom": "pending_credential_draft",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "pending_credential_draft_organization_id_organization_id_fk": {
+ "name": "pending_credential_draft_organization_id_organization_id_fk",
+ "tableFrom": "pending_credential_draft",
+ "tableTo": "organization",
+ "columnsFrom": ["organization_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "pending_credential_draft_credential_id_credential_id_fk": {
+ "name": "pending_credential_draft_credential_id_credential_id_fk",
+ "tableFrom": "pending_credential_draft",
+ "tableTo": "credential",
+ "columnsFrom": ["credential_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "pending_draft_owner_check": {
+ "name": "pending_draft_owner_check",
+ "value": "num_nonnulls(\"pending_credential_draft\".\"workspace_id\", \"pending_credential_draft\".\"organization_id\") = 1"
+ }
+ },
+ "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()"
+ },
+ "is_default": {
+ "name": "is_default",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "membership_mode": {
+ "name": "membership_mode",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'inherit'"
+ }
+ },
+ "indexes": {
+ "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_organization_name_unique": {
+ "name": "permission_group_organization_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_organization_default_unique": {
+ "name": "permission_group_organization_default_unique",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "is_default = 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
+ },
+ "organization_id": {
+ "name": "organization_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_group_user_unique": {
+ "name": "permission_group_member_group_user_unique",
+ "columns": [
+ {
+ "expression": "permission_group_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "permission_group_member_organization_user_idx": {
+ "name": "permission_group_member_organization_user_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "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_organization_id_organization_id_fk": {
+ "name": "permission_group_member_organization_id_organization_id_fk",
+ "tableFrom": "permission_group_member",
+ "tableTo": "organization",
+ "columnsFrom": ["organization_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.permission_group_workspace": {
+ "name": "permission_group_workspace",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "permission_group_id": {
+ "name": "permission_group_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "permission_group_workspace_workspace_id_idx": {
+ "name": "permission_group_workspace_workspace_id_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "permission_group_workspace_group_workspace_unique": {
+ "name": "permission_group_workspace_group_workspace_unique",
+ "columns": [
+ {
+ "expression": "permission_group_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "permission_group_workspace_permission_group_id_permission_group_id_fk": {
+ "name": "permission_group_workspace_permission_group_id_permission_group_id_fk",
+ "tableFrom": "permission_group_workspace",
+ "tableTo": "permission_group",
+ "columnsFrom": ["permission_group_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "permission_group_workspace_workspace_id_workspace_id_fk": {
+ "name": "permission_group_workspace_workspace_id_workspace_id_fk",
+ "tableFrom": "permission_group_workspace",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "permission_group_workspace_organization_id_organization_id_fk": {
+ "name": "permission_group_workspace_organization_id_organization_id_fk",
+ "tableFrom": "permission_group_workspace",
+ "tableTo": "organization",
+ "columnsFrom": ["organization_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "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_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.pinned_item": {
+ "name": "pinned_item",
+ "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": true
+ },
+ "resource_type": {
+ "name": "resource_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "resource_id": {
+ "name": "resource_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "pinned_at": {
+ "name": "pinned_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "pinned_item_user_workspace_idx": {
+ "name": "pinned_item_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": {}
+ },
+ "pinned_item_resource_idx": {
+ "name": "pinned_item_resource_idx",
+ "columns": [
+ {
+ "expression": "resource_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "resource_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "pinned_item_user_resource_unique": {
+ "name": "pinned_item_user_resource_unique",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "resource_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "resource_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "pinned_item_user_id_user_id_fk": {
+ "name": "pinned_item_user_id_user_id_fk",
+ "tableFrom": "pinned_item",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "pinned_item_workspace_id_workspace_id_fk": {
+ "name": "pinned_item_workspace_id_workspace_id_fk",
+ "tableFrom": "pinned_item",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.public_share": {
+ "name": "public_share",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "resource_type": {
+ "name": "resource_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "resource_id": {
+ "name": "resource_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "token": {
+ "name": "token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "is_active": {
+ "name": "is_active",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "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": "'[]'"
+ },
+ "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": {
+ "public_share_token_unique": {
+ "name": "public_share_token_unique",
+ "columns": [
+ {
+ "expression": "token",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "public_share_resource_unique": {
+ "name": "public_share_resource_unique",
+ "columns": [
+ {
+ "expression": "resource_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "resource_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "public_share_resource_id_idx": {
+ "name": "public_share_resource_id_idx",
+ "columns": [
+ {
+ "expression": "resource_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "public_share_workspace_id_idx": {
+ "name": "public_share_workspace_id_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "public_share_workspace_id_workspace_id_fk": {
+ "name": "public_share_workspace_id_workspace_id_fk",
+ "tableFrom": "public_share",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "public_share_created_by_user_id_fk": {
+ "name": "public_share_created_by_user_id_fk",
+ "tableFrom": "public_share",
+ "tableTo": "user",
+ "columnsFrom": ["created_by"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "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
+ },
+ "blocked_until": {
+ "name": "blocked_until",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.resource_policy": {
+ "name": "resource_policy",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "resource_type": {
+ "name": "resource_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "resource_id": {
+ "name": "resource_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "revision": {
+ "name": "revision",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 1
+ },
+ "document": {
+ "name": "document",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_by": {
+ "name": "updated_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": {
+ "resource_policy_organization_id_idx": {
+ "name": "resource_policy_organization_id_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "resource_policy_resource_unique": {
+ "name": "resource_policy_resource_unique",
+ "columns": [
+ {
+ "expression": "resource_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "resource_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "resource_policy_workspace_id_idx": {
+ "name": "resource_policy_workspace_id_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "resource_policy_workspace_id_workspace_id_fk": {
+ "name": "resource_policy_workspace_id_workspace_id_fk",
+ "tableFrom": "resource_policy",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "resource_policy_organization_id_organization_id_fk": {
+ "name": "resource_policy_organization_id_organization_id_fk",
+ "tableFrom": "resource_policy",
+ "tableTo": "organization",
+ "columnsFrom": ["organization_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "resource_policy_created_by_user_id_fk": {
+ "name": "resource_policy_created_by_user_id_fk",
+ "tableFrom": "resource_policy",
+ "tableTo": "user",
+ "columnsFrom": ["created_by"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "resource_policy_updated_by_user_id_fk": {
+ "name": "resource_policy_updated_by_user_id_fk",
+ "tableFrom": "resource_policy",
+ "tableTo": "user",
+ "columnsFrom": ["updated_by"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "resource_policy_owner_check": {
+ "name": "resource_policy_owner_check",
+ "value": "num_nonnulls(\"resource_policy\".\"workspace_id\", \"resource_policy\".\"organization_id\") = 1"
+ }
+ },
+ "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.sandbox_image": {
+ "name": "sandbox_image",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "provider": {
+ "name": "provider",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "spec_hash": {
+ "name": "spec_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "spec": {
+ "name": "spec",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "sandbox_image_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "image_ref": {
+ "name": "image_ref",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "provider_image_id": {
+ "name": "provider_image_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "build_id": {
+ "name": "build_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "materialization_generation": {
+ "name": "materialization_generation",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "error_code": {
+ "name": "error_code",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "error_message": {
+ "name": "error_message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "error_detail": {
+ "name": "error_detail",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_used_at": {
+ "name": "last_used_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": {
+ "sandbox_image_provider_spec_unique": {
+ "name": "sandbox_image_provider_spec_unique",
+ "columns": [
+ {
+ "expression": "provider",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "spec_hash",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "sandbox_image_status_idx": {
+ "name": "sandbox_image_status_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "sandbox_image_last_used_idx": {
+ "name": "sandbox_image_last_used_idx",
+ "columns": [
+ {
+ "expression": "last_used_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.scim_connection": {
+ "name": "scim_connection",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'active'"
+ },
+ "settings": {
+ "name": "settings",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::jsonb"
+ },
+ "last_request_at": {
+ "name": "last_request_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "reconcile_lock_token": {
+ "name": "reconcile_lock_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "reconcile_lease_at": {
+ "name": "reconcile_lease_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "reconciled_at": {
+ "name": "reconciled_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": 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": {
+ "scim_connection_organization_unique": {
+ "name": "scim_connection_organization_unique",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "scim_connection_reconcile_due_idx": {
+ "name": "scim_connection_reconcile_due_idx",
+ "columns": [
+ {
+ "expression": "reconciled_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "scim_connection_organization_id_organization_id_fk": {
+ "name": "scim_connection_organization_id_organization_id_fk",
+ "tableFrom": "scim_connection",
+ "tableTo": "organization",
+ "columnsFrom": ["organization_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "scim_connection_created_by_user_id_fk": {
+ "name": "scim_connection_created_by_user_id_fk",
+ "tableFrom": "scim_connection",
+ "tableTo": "user",
+ "columnsFrom": ["created_by"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.scim_credential": {
+ "name": "scim_credential",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "connection_id": {
+ "name": "connection_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "token_hash": {
+ "name": "token_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "token_prefix": {
+ "name": "token_prefix",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "scopes": {
+ "name": "scopes",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "revoked_at": {
+ "name": "revoked_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "revoked_by": {
+ "name": "revoked_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_used_at": {
+ "name": "last_used_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": 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()"
+ }
+ },
+ "indexes": {
+ "scim_credential_token_hash_unique": {
+ "name": "scim_credential_token_hash_unique",
+ "columns": [
+ {
+ "expression": "token_hash",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "scim_credential_connection_idx": {
+ "name": "scim_credential_connection_idx",
+ "columns": [
+ {
+ "expression": "connection_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "scim_credential_connection_id_scim_connection_id_fk": {
+ "name": "scim_credential_connection_id_scim_connection_id_fk",
+ "tableFrom": "scim_credential",
+ "tableTo": "scim_connection",
+ "columnsFrom": ["connection_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "scim_credential_revoked_by_user_id_fk": {
+ "name": "scim_credential_revoked_by_user_id_fk",
+ "tableFrom": "scim_credential",
+ "tableTo": "user",
+ "columnsFrom": ["revoked_by"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "scim_credential_created_by_user_id_fk": {
+ "name": "scim_credential_created_by_user_id_fk",
+ "tableFrom": "scim_credential",
+ "tableTo": "user",
+ "columnsFrom": ["created_by"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.scim_group": {
+ "name": "scim_group",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "connection_id": {
+ "name": "connection_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "external_id": {
+ "name": "external_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "display_name": {
+ "name": "display_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "display_name_key": {
+ "name": "display_name_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "order_key": {
+ "name": "order_key",
+ "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": {
+ "scim_group_connection_display_name_unique": {
+ "name": "scim_group_connection_display_name_unique",
+ "columns": [
+ {
+ "expression": "connection_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "display_name_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "scim_group_connection_external_id_unique": {
+ "name": "scim_group_connection_external_id_unique",
+ "columns": [
+ {
+ "expression": "connection_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "external_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "external_id is not null",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "scim_group_connection_order_idx": {
+ "name": "scim_group_connection_order_idx",
+ "columns": [
+ {
+ "expression": "connection_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "order_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "scim_group_connection_id_scim_connection_id_fk": {
+ "name": "scim_group_connection_id_scim_connection_id_fk",
+ "tableFrom": "scim_group",
+ "tableTo": "scim_connection",
+ "columnsFrom": ["connection_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.scim_group_mapping": {
+ "name": "scim_group_mapping",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "group_id": {
+ "name": "group_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "target_kind": {
+ "name": "target_kind",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "permission_group_id": {
+ "name": "permission_group_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "permission_type": {
+ "name": "permission_type",
+ "type": "permission_type",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "role": {
+ "name": "role",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "source": {
+ "name": "source",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'manual'"
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "scim_group_mapping_group_idx": {
+ "name": "scim_group_mapping_group_idx",
+ "columns": [
+ {
+ "expression": "group_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "scim_group_mapping_permission_group_idx": {
+ "name": "scim_group_mapping_permission_group_idx",
+ "columns": [
+ {
+ "expression": "permission_group_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "scim_group_mapping_workspace_idx": {
+ "name": "scim_group_mapping_workspace_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "scim_group_mapping_group_target_unique": {
+ "name": "scim_group_mapping_group_target_unique",
+ "columns": [
+ {
+ "expression": "group_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "target_kind",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "coalesce(\"permission_group_id\", \"workspace_id\", \"role\")",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "scim_group_mapping_group_id_scim_group_id_fk": {
+ "name": "scim_group_mapping_group_id_scim_group_id_fk",
+ "tableFrom": "scim_group_mapping",
+ "tableTo": "scim_group",
+ "columnsFrom": ["group_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "scim_group_mapping_permission_group_id_permission_group_id_fk": {
+ "name": "scim_group_mapping_permission_group_id_permission_group_id_fk",
+ "tableFrom": "scim_group_mapping",
+ "tableTo": "permission_group",
+ "columnsFrom": ["permission_group_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "scim_group_mapping_workspace_id_workspace_id_fk": {
+ "name": "scim_group_mapping_workspace_id_workspace_id_fk",
+ "tableFrom": "scim_group_mapping",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "scim_group_mapping_created_by_user_id_fk": {
+ "name": "scim_group_mapping_created_by_user_id_fk",
+ "tableFrom": "scim_group_mapping",
+ "tableTo": "user",
+ "columnsFrom": ["created_by"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "scim_group_mapping_target_shape": {
+ "name": "scim_group_mapping_target_shape",
+ "value": "(\n (\"scim_group_mapping\".\"target_kind\" = 'permission_group' AND \"scim_group_mapping\".\"permission_group_id\" IS NOT NULL AND \"scim_group_mapping\".\"workspace_id\" IS NULL AND \"scim_group_mapping\".\"permission_type\" IS NULL AND \"scim_group_mapping\".\"role\" IS NULL)\n OR (\"scim_group_mapping\".\"target_kind\" = 'workspace' AND \"scim_group_mapping\".\"workspace_id\" IS NOT NULL AND \"scim_group_mapping\".\"permission_type\" IS NOT NULL AND \"scim_group_mapping\".\"permission_group_id\" IS NULL AND \"scim_group_mapping\".\"role\" IS NULL)\n OR (\"scim_group_mapping\".\"target_kind\" = 'org_role' AND \"scim_group_mapping\".\"role\" IS NOT NULL AND \"scim_group_mapping\".\"permission_group_id\" IS NULL AND \"scim_group_mapping\".\"workspace_id\" IS NULL AND \"scim_group_mapping\".\"permission_type\" IS NULL)\n )"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.scim_group_member": {
+ "name": "scim_group_member",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "group_id": {
+ "name": "group_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "scim_user_id": {
+ "name": "scim_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "scim_group_member_group_user_unique": {
+ "name": "scim_group_member_group_user_unique",
+ "columns": [
+ {
+ "expression": "group_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "scim_user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "scim_group_member_scim_user_idx": {
+ "name": "scim_group_member_scim_user_idx",
+ "columns": [
+ {
+ "expression": "scim_user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "scim_group_member_group_id_scim_group_id_fk": {
+ "name": "scim_group_member_group_id_scim_group_id_fk",
+ "tableFrom": "scim_group_member",
+ "tableTo": "scim_group",
+ "columnsFrom": ["group_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "scim_group_member_scim_user_id_scim_user_id_fk": {
+ "name": "scim_group_member_scim_user_id_scim_user_id_fk",
+ "tableFrom": "scim_group_member",
+ "tableTo": "scim_user",
+ "columnsFrom": ["scim_user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.scim_projection_grant": {
+ "name": "scim_projection_grant",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "connection_id": {
+ "name": "connection_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "scim_user_id": {
+ "name": "scim_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "target_kind": {
+ "name": "target_kind",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "target_id": {
+ "name": "target_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "permission_type": {
+ "name": "permission_type",
+ "type": "permission_type",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "origin": {
+ "name": "origin",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'directory'"
+ },
+ "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": {
+ "scim_projection_grant_user_target_unique": {
+ "name": "scim_projection_grant_user_target_unique",
+ "columns": [
+ {
+ "expression": "scim_user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "target_kind",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "target_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "scim_projection_grant_connection_idx": {
+ "name": "scim_projection_grant_connection_idx",
+ "columns": [
+ {
+ "expression": "connection_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "scim_projection_grant_connection_id_scim_connection_id_fk": {
+ "name": "scim_projection_grant_connection_id_scim_connection_id_fk",
+ "tableFrom": "scim_projection_grant",
+ "tableTo": "scim_connection",
+ "columnsFrom": ["connection_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "scim_projection_grant_scim_user_id_scim_user_id_fk": {
+ "name": "scim_projection_grant_scim_user_id_scim_user_id_fk",
+ "tableFrom": "scim_projection_grant",
+ "tableTo": "scim_user",
+ "columnsFrom": ["scim_user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.scim_request_log": {
+ "name": "scim_request_log",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "connection_id": {
+ "name": "connection_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "credential_id": {
+ "name": "credential_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "method": {
+ "name": "method",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "path": {
+ "name": "path",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "scim_type": {
+ "name": "scim_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "detail": {
+ "name": "detail",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_agent": {
+ "name": "user_agent",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "duration_ms": {
+ "name": "duration_ms",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "scim_request_log_connection_created_idx": {
+ "name": "scim_request_log_connection_created_idx",
+ "columns": [
+ {
+ "expression": "connection_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "scim_request_log_connection_id_scim_connection_id_fk": {
+ "name": "scim_request_log_connection_id_scim_connection_id_fk",
+ "tableFrom": "scim_request_log",
+ "tableTo": "scim_connection",
+ "columnsFrom": ["connection_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.scim_user": {
+ "name": "scim_user",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "connection_id": {
+ "name": "connection_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "external_id": {
+ "name": "external_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_name": {
+ "name": "user_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "active": {
+ "name": "active",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "attributes": {
+ "name": "attributes",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "order_key": {
+ "name": "order_key",
+ "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": {
+ "scim_user_connection_user_unique": {
+ "name": "scim_user_connection_user_unique",
+ "columns": [
+ {
+ "expression": "connection_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "scim_user_connection_user_name_unique": {
+ "name": "scim_user_connection_user_name_unique",
+ "columns": [
+ {
+ "expression": "connection_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "user_name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "scim_user_connection_external_id_unique": {
+ "name": "scim_user_connection_external_id_unique",
+ "columns": [
+ {
+ "expression": "connection_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "external_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "external_id is not null",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "scim_user_connection_order_idx": {
+ "name": "scim_user_connection_order_idx",
+ "columns": [
+ {
+ "expression": "connection_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "order_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "scim_user_user_idx": {
+ "name": "scim_user_user_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "scim_user_connection_id_scim_connection_id_fk": {
+ "name": "scim_user_connection_id_scim_connection_id_fk",
+ "tableFrom": "scim_user",
+ "tableTo": "scim_connection",
+ "columnsFrom": ["connection_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "scim_user_user_id_user_id_fk": {
+ "name": "scim_user_user_id_user_id_fk",
+ "tableFrom": "scim_user",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.scim_user_tombstone": {
+ "name": "scim_user_tombstone",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "connection_id": {
+ "name": "connection_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "external_id": {
+ "name": "external_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "scim_user_tombstone_connection_external_id_unique": {
+ "name": "scim_user_tombstone_connection_external_id_unique",
+ "columns": [
+ {
+ "expression": "connection_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "external_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "scim_user_tombstone_user_idx": {
+ "name": "scim_user_tombstone_user_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "scim_user_tombstone_connection_id_scim_connection_id_fk": {
+ "name": "scim_user_tombstone_connection_id_scim_connection_id_fk",
+ "tableFrom": "scim_user_tombstone",
+ "tableTo": "scim_connection",
+ "columnsFrom": ["connection_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "scim_user_tombstone_user_id_user_id_fk": {
+ "name": "scim_user_tombstone_user_id_user_id_fk",
+ "tableFrom": "scim_user_tombstone",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.secret_usage": {
+ "name": "secret_usage",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "secret_name": {
+ "name": "secret_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "secret_scope": {
+ "name": "secret_scope",
+ "type": "secret_usage_scope",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "secret_owner_user_id": {
+ "name": "secret_owner_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "''"
+ },
+ "source": {
+ "name": "source",
+ "type": "secret_usage_source",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "''"
+ },
+ "actor_user_id": {
+ "name": "actor_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "''"
+ },
+ "usage_date": {
+ "name": "usage_date",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "use_count": {
+ "name": "use_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "last_used_at": {
+ "name": "last_used_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "last_execution_id": {
+ "name": "last_execution_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_trigger": {
+ "name": "last_trigger",
+ "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": {
+ "secret_usage_bucket_unique": {
+ "name": "secret_usage_bucket_unique",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "secret_name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "secret_scope",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "secret_owner_user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "source",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "actor_user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "usage_date",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "secret_usage_secret_recent_idx": {
+ "name": "secret_usage_secret_recent_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "secret_name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "secret_scope",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "secret_owner_user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "last_used_at",
+ "isExpression": false,
+ "asc": false,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "secret_usage_workspace_id_workspace_id_fk": {
+ "name": "secret_usage_workspace_id_workspace_id_fk",
+ "tableFrom": "secret_usage",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_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
+ },
+ "impersonated_by": {
+ "name": "impersonated_by",
+ "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": {}
+ }
+ },
+ "foreignKeys": {
+ "session_user_id_user_id_fk": {
+ "name": "session_user_id_user_id_fk",
+ "tableFrom": "session",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "session_active_organization_id_organization_id_fk": {
+ "name": "session_active_organization_id_organization_id_fk",
+ "tableFrom": "session",
+ "tableTo": "organization",
+ "columnsFrom": ["active_organization_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "session_token_unique": {
+ "name": "session_token_unique",
+ "nullsNotDistinct": false,
+ "columns": ["token"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.settings": {
+ "name": "settings",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "theme": {
+ "name": "theme",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'system'"
+ },
+ "auto_connect": {
+ "name": "auto_connect",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "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
+ },
+ "mothership_environment": {
+ "name": "mothership_environment",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'default'"
+ },
+ "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
+ },
+ "auto_focus_on_click": {
+ "name": "auto_focus_on_click",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "timezone": {
+ "name": "timezone",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "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": "'[]'"
+ },
+ "last_active_workspace_id": {
+ "name": "last_active_workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "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.sim_trigger_state": {
+ "name": "sim_trigger_state",
+ "schema": "",
+ "columns": {
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "block_id": {
+ "name": "block_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "scope_key": {
+ "name": "scope_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "''"
+ },
+ "last_fired_at": {
+ "name": "last_fired_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "sim_trigger_state_workflow_id_workflow_id_fk": {
+ "name": "sim_trigger_state_workflow_id_workflow_id_fk",
+ "tableFrom": "sim_trigger_state",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "sim_trigger_state_workflow_id_block_id_scope_key_pk": {
+ "name": "sim_trigger_state_workflow_id_block_id_scope_key_pk",
+ "columns": ["workflow_id", "block_id", "scope_key"]
+ }
+ },
+ "uniqueConstraints": {},
+ "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_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.skill_member": {
+ "name": "skill_member",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "skill_id": {
+ "name": "skill_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "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": {
+ "skill_member_user_id_idx": {
+ "name": "skill_member_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "skill_member_unique": {
+ "name": "skill_member_unique",
+ "columns": [
+ {
+ "expression": "skill_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "skill_member_skill_id_skill_id_fk": {
+ "name": "skill_member_skill_id_skill_id_fk",
+ "tableFrom": "skill_member",
+ "tableTo": "skill",
+ "columnsFrom": ["skill_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "skill_member_user_id_user_id_fk": {
+ "name": "skill_member_user_id_user_id_fk",
+ "tableFrom": "skill_member",
+ "tableTo": "user",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "skill_member_invited_by_user_id_fk": {
+ "name": "skill_member_invited_by_user_id_fk",
+ "tableFrom": "skill_member",
+ "tableTo": "user",
+ "columnsFrom": ["invited_by"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.sso_domain": {
+ "name": "sso_domain",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "domain": {
+ "name": "domain",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "verification_token": {
+ "name": "verification_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "verified_at": {
+ "name": "verified_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": 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": {
+ "sso_domain_organization_id_idx": {
+ "name": "sso_domain_organization_id_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "sso_domain_domain_idx": {
+ "name": "sso_domain_domain_idx",
+ "columns": [
+ {
+ "expression": "domain",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "sso_domain_org_domain_unique": {
+ "name": "sso_domain_org_domain_unique",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "domain",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "sso_domain_verified_unique": {
+ "name": "sso_domain_verified_unique",
+ "columns": [
+ {
+ "expression": "domain",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "status = 'verified'",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "sso_domain_organization_id_organization_id_fk": {
+ "name": "sso_domain_organization_id_organization_id_fk",
+ "tableFrom": "sso_domain",
+ "tableTo": "organization",
+ "columnsFrom": ["organization_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "sso_domain_created_by_user_id_fk": {
+ "name": "sso_domain_created_by_user_id_fk",
+ "tableFrom": "sso_domain",
+ "tableTo": "user",
+ "columnsFrom": ["created_by"],
+ "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
+ },
+ "domain_verified": {
+ "name": "domain_verified",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "jit_provisioning_enabled": {
+ "name": "jit_provisioning_enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ }
+ },
+ "indexes": {
+ "sso_provider_provider_id_unique": {
+ "name": "sso_provider_provider_id_unique",
+ "columns": [
+ {
+ "expression": "provider_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "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
+ },
+ "cancel_at": {
+ "name": "cancel_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "canceled_at": {
+ "name": "canceled_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "ended_at": {
+ "name": "ended_at",
+ "type": "timestamp",
+ "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
+ },
+ "billing_interval": {
+ "name": "billing_interval",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "stripe_schedule_id": {
+ "name": "stripe_schedule_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_closed_period_start": {
+ "name": "last_closed_period_start",
+ "type": "timestamp",
+ "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": {}
+ },
+ "subscription_cycle_close_lagging_idx": {
+ "name": "subscription_cycle_close_lagging_idx",
+ "columns": [
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"subscription\".\"status\" in ('active', 'past_due') and \"subscription\".\"period_start\" is not null and (\"subscription\".\"last_closed_period_start\" is null or \"subscription\".\"last_closed_period_start\" < \"subscription\".\"period_start\")",
+ "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.table_jobs": {
+ "name": "table_jobs",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "table_id": {
+ "name": "table_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'running'"
+ },
+ "payload": {
+ "name": "payload",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "rows_processed": {
+ "name": "rows_processed",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "error": {
+ "name": "error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "started_at": {
+ "name": "started_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": {
+ "table_jobs_one_active_per_table": {
+ "name": "table_jobs_one_active_per_table",
+ "columns": [
+ {
+ "expression": "table_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"table_jobs\".\"status\" = 'running' AND \"table_jobs\".\"type\" <> 'export'",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "table_jobs_watchdog_idx": {
+ "name": "table_jobs_watchdog_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "updated_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "table_jobs_table_started_idx": {
+ "name": "table_jobs_table_started_idx",
+ "columns": [
+ {
+ "expression": "table_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "started_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "table_jobs_table_id_user_table_definitions_id_fk": {
+ "name": "table_jobs_table_id_user_table_definitions_id_fk",
+ "tableFrom": "table_jobs",
+ "tableTo": "user_table_definitions",
+ "columnsFrom": ["table_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "table_jobs_workspace_id_workspace_id_fk": {
+ "name": "table_jobs_workspace_id_workspace_id_fk",
+ "tableFrom": "table_jobs",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.table_row_executions": {
+ "name": "table_row_executions",
+ "schema": "",
+ "columns": {
+ "table_id": {
+ "name": "table_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "row_id": {
+ "name": "row_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "group_id": {
+ "name": "group_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "execution_id": {
+ "name": "execution_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "job_id": {
+ "name": "job_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "error": {
+ "name": "error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "running_block_ids": {
+ "name": "running_block_ids",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::text[]"
+ },
+ "block_errors": {
+ "name": "block_errors",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::jsonb"
+ },
+ "cancelled_at": {
+ "name": "cancelled_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "capability_governed_user_id": {
+ "name": "capability_governed_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "enrichment_details": {
+ "name": "enrichment_details",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "table_row_executions_table_status_idx": {
+ "name": "table_row_executions_table_status_idx",
+ "columns": [
+ {
+ "expression": "table_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"table_row_executions\".\"status\" IN ('queued', 'running', 'pending')",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "table_row_executions_execution_id_idx": {
+ "name": "table_row_executions_execution_id_idx",
+ "columns": [
+ {
+ "expression": "execution_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"table_row_executions\".\"execution_id\" IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "table_row_executions_table_group_idx": {
+ "name": "table_row_executions_table_group_idx",
+ "columns": [
+ {
+ "expression": "table_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "group_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "table_row_executions_table_id_user_table_definitions_id_fk": {
+ "name": "table_row_executions_table_id_user_table_definitions_id_fk",
+ "tableFrom": "table_row_executions",
+ "tableTo": "user_table_definitions",
+ "columnsFrom": ["table_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "table_row_executions_row_id_user_table_rows_id_fk": {
+ "name": "table_row_executions_row_id_user_table_rows_id_fk",
+ "tableFrom": "table_row_executions",
+ "tableTo": "user_table_rows",
+ "columnsFrom": ["row_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "table_row_executions_capability_governed_user_id_user_id_fk": {
+ "name": "table_row_executions_capability_governed_user_id_user_id_fk",
+ "tableFrom": "table_row_executions",
+ "tableTo": "user",
+ "columnsFrom": ["capability_governed_user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "table_row_executions_row_id_group_id_pk": {
+ "name": "table_row_executions_row_id_group_id_pk",
+ "columns": ["row_id", "group_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.table_run_dispatches": {
+ "name": "table_run_dispatches",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "table_id": {
+ "name": "table_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "request_id": {
+ "name": "request_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "mode": {
+ "name": "mode",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "scope": {
+ "name": "scope",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "cursor": {
+ "name": "cursor",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "limit": {
+ "name": "limit",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "processed_count": {
+ "name": "processed_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "is_manual_run": {
+ "name": "is_manual_run",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "triggered_by_user_id": {
+ "name": "triggered_by_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "capability_governed_user_id": {
+ "name": "capability_governed_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "requested_at": {
+ "name": "requested_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "heartbeat_at": {
+ "name": "heartbeat_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "completed_at": {
+ "name": "completed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cancelled_at": {
+ "name": "cancelled_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "table_run_dispatches_active_idx": {
+ "name": "table_run_dispatches_active_idx",
+ "columns": [
+ {
+ "expression": "table_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "table_run_dispatches_watchdog_idx": {
+ "name": "table_run_dispatches_watchdog_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "requested_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "table_run_dispatches_governed_active_idx": {
+ "name": "table_run_dispatches_governed_active_idx",
+ "columns": [
+ {
+ "expression": "capability_governed_user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"table_run_dispatches\".\"status\" IN ('pending', 'dispatching')",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "table_run_dispatches_table_id_user_table_definitions_id_fk": {
+ "name": "table_run_dispatches_table_id_user_table_definitions_id_fk",
+ "tableFrom": "table_run_dispatches",
+ "tableTo": "user_table_definitions",
+ "columnsFrom": ["table_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "table_run_dispatches_workspace_id_workspace_id_fk": {
+ "name": "table_run_dispatches_workspace_id_workspace_id_fk",
+ "tableFrom": "table_run_dispatches",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "table_run_dispatches_triggered_by_user_id_user_id_fk": {
+ "name": "table_run_dispatches_triggered_by_user_id_user_id_fk",
+ "tableFrom": "table_run_dispatches",
+ "tableTo": "user",
+ "columnsFrom": ["triggered_by_user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "table_run_dispatches_capability_governed_user_id_user_id_fk": {
+ "name": "table_run_dispatches_capability_governed_user_id_user_id_fk",
+ "tableFrom": "table_run_dispatches",
+ "tableTo": "user",
+ "columnsFrom": ["capability_governed_user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.table_views": {
+ "name": "table_views",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "table_id": {
+ "name": "table_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "config": {
+ "name": "config",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'"
+ },
+ "is_default": {
+ "name": "is_default",
+ "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": {
+ "table_views_table_created_idx": {
+ "name": "table_views_table_created_idx",
+ "columns": [
+ {
+ "expression": "table_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "table_views_workspace_created_idx": {
+ "name": "table_views_workspace_created_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "table_views_table_default_unique": {
+ "name": "table_views_table_default_unique",
+ "columns": [
+ {
+ "expression": "table_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "is_default = true",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "table_views_table_id_user_table_definitions_id_fk": {
+ "name": "table_views_table_id_user_table_definitions_id_fk",
+ "tableFrom": "table_views",
+ "tableTo": "user_table_definitions",
+ "columnsFrom": ["table_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "table_views_workspace_id_workspace_id_fk": {
+ "name": "table_views_workspace_id_workspace_id_fk",
+ "tableFrom": "table_views",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "table_views_created_by_user_id_fk": {
+ "name": "table_views_created_by_user_id_fk",
+ "tableFrom": "table_views",
+ "tableTo": "user",
+ "columnsFrom": ["created_by"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.upload_session": {
+ "name": "upload_session",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "token_hash": {
+ "name": "token_hash",
+ "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
+ },
+ "knowledge_base_id": {
+ "name": "knowledge_base_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
+ },
+ "purpose": {
+ "name": "purpose",
+ "type": "upload_session_purpose",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "method": {
+ "name": "method",
+ "type": "upload_session_method",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "storage_context": {
+ "name": "storage_context",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "final_key": {
+ "name": "final_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "storage_provider": {
+ "name": "storage_provider",
+ "type": "upload_session_provider",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider_upload_id": {
+ "name": "provider_upload_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "provider_object_version": {
+ "name": "provider_object_version",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "file_name": {
+ "name": "file_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "content_type": {
+ "name": "content_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "file_size": {
+ "name": "file_size",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "part_size": {
+ "name": "part_size",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "part_count": {
+ "name": "part_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "upload_session_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'uploading'"
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::jsonb"
+ },
+ "processing_lease_id": {
+ "name": "processing_lease_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "processing_lease_expires_at": {
+ "name": "processing_lease_expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "completed_file_id": {
+ "name": "completed_file_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "error": {
+ "name": "error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "completed_at": {
+ "name": "completed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "upload_session_token_hash_unique": {
+ "name": "upload_session_token_hash_unique",
+ "columns": [
+ {
+ "expression": "token_hash",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "upload_session_final_key_unique": {
+ "name": "upload_session_final_key_unique",
+ "columns": [
+ {
+ "expression": "final_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "upload_session_status_expires_at_idx": {
+ "name": "upload_session_status_expires_at_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "expires_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "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
+ },
+ "event_key": {
+ "name": "event_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "billing_entity_type": {
+ "name": "billing_entity_type",
+ "type": "billing_entity_type",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "billing_entity_id": {
+ "name": "billing_entity_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "billing_period_start": {
+ "name": "billing_period_start",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "billing_period_end": {
+ "name": "billing_period_end",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "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": {}
+ },
+ "usage_log_event_key_unique": {
+ "name": "usage_log_event_key_unique",
+ "columns": [
+ {
+ "expression": "event_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"usage_log\".\"event_key\" IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "usage_log_billing_entity_period_idx": {
+ "name": "usage_log_billing_entity_period_idx",
+ "columns": [
+ {
+ "expression": "billing_entity_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "billing_entity_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "billing_period_start",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "billing_period_end",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"usage_log\".\"billing_entity_type\" IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "usage_log_billing_period_cost_idx": {
+ "name": "usage_log_billing_period_cost_idx",
+ "columns": [
+ {
+ "expression": "billing_entity_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "billing_entity_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "billing_period_start",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "billing_period_end",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "source",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "cost",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"usage_log\".\"billing_entity_type\" IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "usage_log_billing_entity_created_at_cost_idx": {
+ "name": "usage_log_billing_entity_created_at_cost_idx",
+ "columns": [
+ {
+ "expression": "billing_entity_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "billing_entity_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "source",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "cost",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"usage_log\".\"billing_entity_type\" IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "usage_log_workspace_created_at_idx": {
+ "name": "usage_log_workspace_created_at_idx",
+ "columns": [
+ {
+ "expression": "workspace_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_execution_id_idx": {
+ "name": "usage_log_execution_id_idx",
+ "columns": [
+ {
+ "expression": "execution_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": {
+ "usage_log_billing_scope_all_or_none": {
+ "name": "usage_log_billing_scope_all_or_none",
+ "value": "(\n (\"usage_log\".\"billing_entity_type\" IS NULL AND \"usage_log\".\"billing_entity_id\" IS NULL AND \"usage_log\".\"billing_period_start\" IS NULL AND \"usage_log\".\"billing_period_end\" IS NULL)\n OR\n (\"usage_log\".\"billing_entity_type\" IS NOT NULL AND \"usage_log\".\"billing_entity_id\" IS NOT NULL AND \"usage_log\".\"billing_period_start\" IS NOT NULL AND \"usage_log\".\"billing_period_end\" IS NOT NULL AND \"usage_log\".\"billing_period_start\" < \"usage_log\".\"billing_period_end\")\n )"
+ }
+ },
+ "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
+ },
+ "normalized_email": {
+ "name": "normalized_email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "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
+ },
+ "role": {
+ "name": "role",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'user'"
+ },
+ "banned": {
+ "name": "banned",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false,
+ "default": false
+ },
+ "ban_reason": {
+ "name": "ban_reason",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "ban_expires": {
+ "name": "ban_expires",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "suspended_at": {
+ "name": "suspended_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "suspension_source": {
+ "name": "suspension_source",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "user_email_lower_idx": {
+ "name": "user_email_lower_idx",
+ "columns": [
+ {
+ "expression": "lower(btrim(\"email\"))",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "user_email_unique": {
+ "name": "user_email_unique",
+ "nullsNotDistinct": false,
+ "columns": ["email"]
+ },
+ "user_normalized_email_unique": {
+ "name": "user_normalized_email_unique",
+ "nullsNotDistinct": false,
+ "columns": ["normalized_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_tokens_used": {
+ "name": "total_tokens_used",
+ "type": "bigint",
+ "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": "'5'"
+ },
+ "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'"
+ },
+ "pro_period_cost_snapshot_at": {
+ "name": "pro_period_cost_snapshot_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "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": "bigint",
+ "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
+ },
+ "limit_notifications": {
+ "name": "limit_notifications",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::jsonb"
+ }
+ },
+ "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.user_table_definitions": {
+ "name": "user_table_definitions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "folder_id": {
+ "name": "folder_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "schema": {
+ "name": "schema",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "max_rows": {
+ "name": "max_rows",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 10000
+ },
+ "row_count": {
+ "name": "row_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "rows_version": {
+ "name": "rows_version",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "schema_locked": {
+ "name": "schema_locked",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "insert_locked": {
+ "name": "insert_locked",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "update_locked": {
+ "name": "update_locked",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "delete_locked": {
+ "name": "delete_locked",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "archived_at": {
+ "name": "archived_at",
+ "type": "timestamp",
+ "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": {
+ "user_table_def_workspace_id_idx": {
+ "name": "user_table_def_workspace_id_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "user_table_def_folder_id_idx": {
+ "name": "user_table_def_folder_id_idx",
+ "columns": [
+ {
+ "expression": "folder_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "user_table_def_workspace_name_unique": {
+ "name": "user_table_def_workspace_name_unique",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"user_table_definitions\".\"archived_at\" IS NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "user_table_def_archived_at_idx": {
+ "name": "user_table_def_archived_at_idx",
+ "columns": [
+ {
+ "expression": "archived_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "user_table_def_workspace_archived_partial_idx": {
+ "name": "user_table_def_workspace_archived_partial_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "archived_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"user_table_definitions\".\"archived_at\" IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "user_table_definitions_workspace_id_workspace_id_fk": {
+ "name": "user_table_definitions_workspace_id_workspace_id_fk",
+ "tableFrom": "user_table_definitions",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "user_table_definitions_folder_id_folder_id_fk": {
+ "name": "user_table_definitions_folder_id_folder_id_fk",
+ "tableFrom": "user_table_definitions",
+ "tableTo": "folder",
+ "columnsFrom": ["folder_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "user_table_definitions_created_by_user_id_fk": {
+ "name": "user_table_definitions_created_by_user_id_fk",
+ "tableFrom": "user_table_definitions",
+ "tableTo": "user",
+ "columnsFrom": ["created_by"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.user_table_row_secret_provenance": {
+ "name": "user_table_row_secret_provenance",
+ "schema": "",
+ "columns": {
+ "row_id": {
+ "name": "row_id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "content_updated_at": {
+ "name": "content_updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "entries": {
+ "name": "entries",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "user_table_row_secret_provenance_row_id_user_table_rows_id_fk": {
+ "name": "user_table_row_secret_provenance_row_id_user_table_rows_id_fk",
+ "tableFrom": "user_table_row_secret_provenance",
+ "tableTo": "user_table_rows",
+ "columnsFrom": ["row_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "user_table_row_secret_provenance_status_check": {
+ "name": "user_table_row_secret_provenance_status_check",
+ "value": "\"user_table_row_secret_provenance\".\"status\" IN ('exact', 'unknown')"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.user_table_rows": {
+ "name": "user_table_rows",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "table_id": {
+ "name": "table_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "data": {
+ "name": "data",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "position": {
+ "name": "position",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "order_key": {
+ "name": "order_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "secret_provenance_version": {
+ "name": "secret_provenance_version",
+ "type": "integer",
+ "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()"
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "user_table_rows_tenant_data_gin_idx": {
+ "name": "user_table_rows_tenant_data_gin_idx",
+ "columns": [
+ {
+ "expression": "table_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "\"data\" jsonb_path_ops",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "gin",
+ "with": {}
+ },
+ "user_table_rows_workspace_table_idx": {
+ "name": "user_table_rows_workspace_table_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "table_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "user_table_rows_table_position_idx": {
+ "name": "user_table_rows_table_position_idx",
+ "columns": [
+ {
+ "expression": "table_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "position",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "user_table_rows_table_order_key_idx": {
+ "name": "user_table_rows_table_order_key_idx",
+ "columns": [
+ {
+ "expression": "table_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "order_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "user_table_rows_table_created_id_idx": {
+ "name": "user_table_rows_table_created_id_idx",
+ "columns": [
+ {
+ "expression": "table_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "user_table_rows_table_id_id_idx": {
+ "name": "user_table_rows_table_id_id_idx",
+ "columns": [
+ {
+ "expression": "table_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "user_table_rows_table_id_user_table_definitions_id_fk": {
+ "name": "user_table_rows_table_id_user_table_definitions_id_fk",
+ "tableFrom": "user_table_rows",
+ "tableTo": "user_table_definitions",
+ "columnsFrom": ["table_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "user_table_rows_workspace_id_workspace_id_fk": {
+ "name": "user_table_rows_workspace_id_workspace_id_fk",
+ "tableFrom": "user_table_rows",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "user_table_rows_created_by_user_id_fk": {
+ "name": "user_table_rows_created_by_user_id_fk",
+ "tableFrom": "user_table_rows",
+ "tableTo": "user",
+ "columnsFrom": ["created_by"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "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
+ },
+ "registration_status": {
+ "name": "registration_status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "registration_generation": {
+ "name": "registration_generation",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "config_fingerprint": {
+ "name": "config_fingerprint",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "prepared_at": {
+ "name": "prepared_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "block_id": {
+ "name": "block_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "path": {
+ "name": "path",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "routing_key": {
+ "name": "routing_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "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
+ },
+ "archived_at": {
+ "name": "archived_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": {
+ "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,
+ "where": "\"webhook\".\"archived_at\" IS NULL",
+ "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_routing_key_active_idx": {
+ "name": "webhook_routing_key_active_idx",
+ "columns": [
+ {
+ "expression": "routing_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "provider",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"webhook\".\"archived_at\" IS NULL AND \"webhook\".\"routing_key\" IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "webhook_archived_at_partial_idx": {
+ "name": "webhook_archived_at_partial_idx",
+ "columns": [
+ {
+ "expression": "archived_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"webhook\".\"archived_at\" IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_webhook_on_provider_is_active_workflow_id_deploym_bdeed5468": {
+ "name": "idx_webhook_on_provider_is_active_workflow_id_deploym_bdeed5468",
+ "columns": [
+ {
+ "expression": "provider",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "is_active",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "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": {}
+ },
+ "idx_webhook_on_workflow_id_block_id_updated_at_desc": {
+ "name": "idx_webhook_on_workflow_id_block_id_updated_at_desc",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "block_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "updated_at",
+ "isExpression": false,
+ "asc": false,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "webhook_active_registration_unique": {
+ "name": "webhook_active_registration_unique",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "block_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"webhook\".\"registration_status\" = 'active' AND \"webhook\".\"block_id\" IS NOT NULL AND \"webhook\".\"archived_at\" IS NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "webhook_candidate_registration_unique": {
+ "name": "webhook_candidate_registration_unique",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "block_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"webhook\".\"registration_status\" = 'candidate' AND \"webhook\".\"block_id\" IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "webhook_registration_status_generation_idx": {
+ "name": "webhook_registration_status_generation_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "registration_status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "registration_generation",
+ "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"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "webhook_registration_status_check": {
+ "name": "webhook_registration_status_check",
+ "value": "\"webhook\".\"registration_status\" IS NULL OR \"webhook\".\"registration_status\" IN ('active', 'candidate', 'retired', 'orphaned')"
+ },
+ "webhook_registration_generation_check": {
+ "name": "webhook_registration_generation_check",
+ "value": "\"webhook\".\"registration_generation\" IS NULL OR \"webhook\".\"registration_generation\" >= 0"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.webhook_path_claim": {
+ "name": "webhook_path_claim",
+ "schema": "",
+ "columns": {
+ "path": {
+ "name": "path",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "workflow_id": {
+ "name": "workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "generation": {
+ "name": "generation",
+ "type": "integer",
+ "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": {
+ "webhook_path_claim_workflow_idx": {
+ "name": "webhook_path_claim_workflow_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "webhook_path_claim_workflow_id_workflow_id_fk": {
+ "name": "webhook_path_claim_workflow_id_workflow_id_fk",
+ "tableFrom": "webhook_path_claim",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "webhook_path_claim_generation_check": {
+ "name": "webhook_path_claim_generation_check",
+ "value": "\"webhook_path_claim\".\"generation\" >= 0"
+ }
+ },
+ "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
+ },
+ "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
+ },
+ "is_public_api": {
+ "name": "is_public_api",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "locked": {
+ "name": "locked",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "fork_sync_excluded": {
+ "name": "fork_sync_excluded",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 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": "'{}'"
+ },
+ "archived_at": {
+ "name": "archived_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "workflow_user_id_idx": {
+ "name": "workflow_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_workspace_id_idx": {
+ "name": "workflow_workspace_id_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_user_workspace_idx": {
+ "name": "workflow_user_workspace_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_workspace_folder_name_active_unique": {
+ "name": "workflow_workspace_folder_name_active_unique",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "coalesce(\"folder_id\", '')",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"workflow\".\"archived_at\" IS NULL",
+ "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": {}
+ },
+ "workflow_active_workspace_sort_idx": {
+ "name": "workflow_active_workspace_sort_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "sort_order",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"workflow\".\"archived_at\" IS NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_archived_at_idx": {
+ "name": "workflow_archived_at_idx",
+ "columns": [
+ {
+ "expression": "archived_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_workspace_archived_partial_idx": {
+ "name": "workflow_workspace_archived_partial_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "archived_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"workflow\".\"archived_at\" IS NOT NULL",
+ "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_folder_id_fk": {
+ "name": "workflow_folder_id_folder_id_fk",
+ "tableFrom": "workflow",
+ "tableTo": "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
+ },
+ "error_enabled": {
+ "name": "error_enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "retry": {
+ "name": "retry",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": 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_operation": {
+ "name": "workflow_deployment_operation",
+ "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": true
+ },
+ "version": {
+ "name": "version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "previous_active_version_id": {
+ "name": "previous_active_version_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "action": {
+ "name": "action",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "protocol_version": {
+ "name": "protocol_version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "generation": {
+ "name": "generation",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'preparing'"
+ },
+ "component_readiness": {
+ "name": "component_readiness",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::jsonb"
+ },
+ "error_code": {
+ "name": "error_code",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "error_message": {
+ "name": "error_message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "idempotency_key": {
+ "name": "idempotency_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "request_hash": {
+ "name": "request_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "actor_id": {
+ "name": "actor_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "completed_at": {
+ "name": "completed_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_deployment_operation_workflow_generation_unique": {
+ "name": "workflow_deployment_operation_workflow_generation_unique",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "generation",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_deployment_operation_workflow_idempotency_unique": {
+ "name": "workflow_deployment_operation_workflow_idempotency_unique",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "idempotency_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"workflow_deployment_operation\".\"idempotency_key\" IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_deployment_operation_workflow_in_flight_unique": {
+ "name": "workflow_deployment_operation_workflow_in_flight_unique",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"workflow_deployment_operation\".\"status\" IN ('preparing', 'activating')",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_deployment_operation_workflow_status_idx": {
+ "name": "workflow_deployment_operation_workflow_status_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_deployment_operation_deployment_version_idx": {
+ "name": "workflow_deployment_operation_deployment_version_idx",
+ "columns": [
+ {
+ "expression": "deployment_version_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_deployment_operation_workflow_version_generation_idx": {
+ "name": "workflow_deployment_operation_workflow_version_generation_idx",
+ "columns": [
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "deployment_version_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "generation",
+ "isExpression": false,
+ "asc": false,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workflow_deployment_operation_workflow_id_workflow_id_fk": {
+ "name": "workflow_deployment_operation_workflow_id_workflow_id_fk",
+ "tableFrom": "workflow_deployment_operation",
+ "tableTo": "workflow",
+ "columnsFrom": ["workflow_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workflow_deployment_operation_deployment_version_id_workflow_deployment_version_id_fk": {
+ "name": "workflow_deployment_operation_deployment_version_id_workflow_deployment_version_id_fk",
+ "tableFrom": "workflow_deployment_operation",
+ "tableTo": "workflow_deployment_version",
+ "columnsFrom": ["deployment_version_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workflow_deployment_operation_previous_active_version_id_workflow_deployment_version_id_fk": {
+ "name": "workflow_deployment_operation_previous_active_version_id_workflow_deployment_version_id_fk",
+ "tableFrom": "workflow_deployment_operation",
+ "tableTo": "workflow_deployment_version",
+ "columnsFrom": ["previous_active_version_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "workflow_deployment_operation_action_check": {
+ "name": "workflow_deployment_operation_action_check",
+ "value": "\"workflow_deployment_operation\".\"action\" IN ('deploy', 'activate')"
+ },
+ "workflow_deployment_operation_status_check": {
+ "name": "workflow_deployment_operation_status_check",
+ "value": "\"workflow_deployment_operation\".\"status\" IN ('preparing', 'activating', 'active', 'failed', 'superseded')"
+ },
+ "workflow_deployment_operation_generation_check": {
+ "name": "workflow_deployment_operation_generation_check",
+ "value": "\"workflow_deployment_operation\".\"generation\" > 0"
+ },
+ "workflow_deployment_operation_protocol_version_check": {
+ "name": "workflow_deployment_operation_protocol_version_check",
+ "value": "\"workflow_deployment_operation\".\"protocol_version\" > 0"
+ }
+ },
+ "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
+ },
+ "execution_deadline_at": {
+ "name": "execution_deadline_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "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
+ },
+ "cost_total": {
+ "name": "cost_total",
+ "type": "numeric",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "models_used": {
+ "name": "models_used",
+ "type": "text[]",
+ "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": {}
+ },
+ "workflow_execution_logs_workspace_started_at_id_desc_idx": {
+ "name": "workflow_execution_logs_workspace_started_at_id_desc_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "\"started_at\" DESC NULLS LAST",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "\"id\" DESC",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_execution_logs_workspace_cost_total_idx": {
+ "name": "workflow_execution_logs_workspace_cost_total_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "cost_total",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_execution_logs_models_used_idx": {
+ "name": "workflow_execution_logs_models_used_idx",
+ "columns": [
+ {
+ "expression": "models_used",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "gin",
+ "with": {}
+ },
+ "workflow_execution_logs_workspace_ended_at_id_idx": {
+ "name": "workflow_execution_logs_workspace_ended_at_id_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "date_trunc('milliseconds', \"ended_at\")",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_execution_logs_running_started_at_idx": {
+ "name": "workflow_execution_logs_running_started_at_idx",
+ "columns": [
+ {
+ "expression": "started_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "status = 'running'",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_execution_logs_running_deadline_idx": {
+ "name": "workflow_execution_logs_running_deadline_idx",
+ "columns": [
+ {
+ "expression": "execution_deadline_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"workflow_execution_logs\".\"status\" = 'running' AND \"workflow_execution_logs\".\"execution_deadline_at\" IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_execution_logs_redacting_started_at_idx": {
+ "name": "workflow_execution_logs_redacting_started_at_idx",
+ "columns": [
+ {
+ "expression": "started_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "status = 'redacting'",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_execution_logs_redacting_deadline_idx": {
+ "name": "workflow_execution_logs_redacting_deadline_idx",
+ "columns": [
+ {
+ "expression": "execution_deadline_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"workflow_execution_logs\".\"status\" = 'redacting' AND \"workflow_execution_logs\".\"execution_deadline_at\" IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_execution_logs_completed_ended_at_idx": {
+ "name": "workflow_execution_logs_completed_ended_at_idx",
+ "columns": [
+ {
+ "expression": "ended_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "execution_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"workflow_execution_logs\".\"status\" = 'completed' AND \"workflow_execution_logs\".\"level\" = 'info' AND \"workflow_execution_logs\".\"ended_at\" IS NOT NULL",
+ "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_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
+ },
+ "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": {
+ "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": {}
+ },
+ "workflow_mcp_server_deleted_at_idx": {
+ "name": "workflow_mcp_server_deleted_at_idx",
+ "columns": [
+ {
+ "expression": "deleted_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_mcp_server_workspace_deleted_partial_idx": {
+ "name": "workflow_mcp_server_workspace_deleted_partial_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "deleted_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"workflow_mcp_server\".\"deleted_at\" IS NOT NULL",
+ "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": "'{}'"
+ },
+ "parameter_description_overrides": {
+ "name": "parameter_description_overrides",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::json"
+ },
+ "archived_at": {
+ "name": "archived_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_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,
+ "where": "\"workflow_mcp_tool\".\"archived_at\" IS NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_mcp_tool_archived_at_partial_idx": {
+ "name": "workflow_mcp_tool_archived_at_partial_idx",
+ "columns": [
+ {
+ "expression": "archived_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"workflow_mcp_tool\".\"archived_at\" IS NOT NULL",
+ "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": false
+ },
+ "deployment_version_id": {
+ "name": "deployment_version_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "deployment_operation_id": {
+ "name": "deployment_operation_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
+ },
+ "infra_retry_count": {
+ "name": "infra_retry_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
+ },
+ "source_type": {
+ "name": "source_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'workflow'"
+ },
+ "job_title": {
+ "name": "job_title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "prompt": {
+ "name": "prompt",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "lifecycle": {
+ "name": "lifecycle",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'persistent'"
+ },
+ "success_condition": {
+ "name": "success_condition",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "max_runs": {
+ "name": "max_runs",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "run_count": {
+ "name": "run_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "source_chat_id": {
+ "name": "source_chat_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "source_task_name": {
+ "name": "source_task_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "source_user_id": {
+ "name": "source_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "source_workspace_id": {
+ "name": "source_workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "secret_scope": {
+ "name": "secret_scope",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'all'"
+ },
+ "mounted_secrets": {
+ "name": "mounted_secrets",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "job_history": {
+ "name": "job_history",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "contexts": {
+ "name": "contexts",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "excluded_dates": {
+ "name": "excluded_dates",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "ends_at": {
+ "name": "ends_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "archived_at": {
+ "name": "archived_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,
+ "where": "\"workflow_schedule\".\"archived_at\" IS NULL",
+ "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": {}
+ },
+ "workflow_schedule_archived_at_partial_idx": {
+ "name": "workflow_schedule_archived_at_partial_idx",
+ "columns": [
+ {
+ "expression": "archived_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"workflow_schedule\".\"archived_at\" IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_workflow_schedule_on_source_workspace_id_source_t_c07f3bba6": {
+ "name": "idx_workflow_schedule_on_source_workspace_id_source_t_c07f3bba6",
+ "columns": [
+ {
+ "expression": "source_workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "source_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "archived_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_schedule_due_workflow_idx": {
+ "name": "workflow_schedule_due_workflow_idx",
+ "columns": [
+ {
+ "expression": "next_run_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "last_queued_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "deployment_version_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"workflow_schedule\".\"archived_at\" IS NULL AND \"workflow_schedule\".\"status\" NOT IN ('disabled', 'completed') AND (\"workflow_schedule\".\"source_type\" = 'workflow' OR \"workflow_schedule\".\"source_type\" IS NULL)",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workflow_schedule_due_job_idx": {
+ "name": "workflow_schedule_due_job_idx",
+ "columns": [
+ {
+ "expression": "next_run_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "last_queued_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"workflow_schedule\".\"archived_at\" IS NULL AND \"workflow_schedule\".\"status\" NOT IN ('disabled', 'completed') AND \"workflow_schedule\".\"source_type\" = 'job'",
+ "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"
+ },
+ "workflow_schedule_deployment_operation_id_workflow_deployment_operation_id_fk": {
+ "name": "workflow_schedule_deployment_operation_id_workflow_deployment_operation_id_fk",
+ "tableFrom": "workflow_schedule",
+ "tableTo": "workflow_deployment_operation",
+ "columnsFrom": ["deployment_operation_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "workflow_schedule_source_user_id_user_id_fk": {
+ "name": "workflow_schedule_source_user_id_user_id_fk",
+ "tableFrom": "workflow_schedule",
+ "tableTo": "user",
+ "columnsFrom": ["source_user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workflow_schedule_source_workspace_id_workspace_id_fk": {
+ "name": "workflow_schedule_source_workspace_id_workspace_id_fk",
+ "tableFrom": "workflow_schedule",
+ "tableTo": "workspace",
+ "columnsFrom": ["source_workspace_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
+ },
+ "color": {
+ "name": "color",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'#33C482'"
+ },
+ "logo_url": {
+ "name": "logo_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "owner_id": {
+ "name": "owner_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "workspace_mode": {
+ "name": "workspace_mode",
+ "type": "workspace_mode",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'grandfathered_shared'"
+ },
+ "billed_account_user_id": {
+ "name": "billed_account_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "storage_used_bytes": {
+ "name": "storage_used_bytes",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "allow_personal_api_keys": {
+ "name": "allow_personal_api_keys",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "inbox_enabled": {
+ "name": "inbox_enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "inbox_address": {
+ "name": "inbox_address",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "inbox_provider_id": {
+ "name": "inbox_provider_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "inbox_secret_scope": {
+ "name": "inbox_secret_scope",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'all'"
+ },
+ "inbox_mounted_secrets": {
+ "name": "inbox_mounted_secrets",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "archived_at": {
+ "name": "archived_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "organization_assigned_at": {
+ "name": "organization_assigned_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "forked_from_workspace_id": {
+ "name": "forked_from_workspace_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": {
+ "workspace_owner_id_idx": {
+ "name": "workspace_owner_id_idx",
+ "columns": [
+ {
+ "expression": "owner_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workspace_organization_id_idx": {
+ "name": "workspace_organization_id_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workspace_mode_idx": {
+ "name": "workspace_mode_idx",
+ "columns": [
+ {
+ "expression": "workspace_mode",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workspace_forked_from_workspace_id_idx": {
+ "name": "workspace_forked_from_workspace_id_idx",
+ "columns": [
+ {
+ "expression": "forked_from_workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workspace_inbox_provider_id_idx": {
+ "name": "workspace_inbox_provider_id_idx",
+ "columns": [
+ {
+ "expression": "inbox_provider_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"workspace\".\"inbox_provider_id\" IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "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_organization_id_organization_id_fk": {
+ "name": "workspace_organization_id_organization_id_fk",
+ "tableFrom": "workspace",
+ "tableTo": "organization",
+ "columnsFrom": ["organization_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "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"
+ },
+ "workspace_forked_from_workspace_id_workspace_id_fk": {
+ "name": "workspace_forked_from_workspace_id_workspace_id_fk",
+ "tableFrom": "workspace",
+ "tableTo": "workspace",
+ "columnsFrom": ["forked_from_workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "workspace_storage_used_bytes_non_negative": {
+ "name": "workspace_storage_used_bytes_non_negative",
+ "value": "\"workspace\".\"storage_used_bytes\" >= 0"
+ }
+ },
+ "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
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": 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": {
+ "workspace_byok_workspace_provider_idx": {
+ "name": "workspace_byok_workspace_provider_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "provider_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
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "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_deleted_at_idx": {
+ "name": "workspace_file_deleted_at_idx",
+ "columns": [
+ {
+ "expression": "deleted_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workspace_file_workspace_deleted_partial_idx": {
+ "name": "workspace_file_workspace_deleted_partial_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "deleted_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"workspace_file\".\"deleted_at\" IS NOT NULL",
+ "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_file_collab_state": {
+ "name": "workspace_file_collab_state",
+ "schema": "",
+ "columns": {
+ "file_id": {
+ "name": "file_id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "doc_state": {
+ "name": "doc_state",
+ "type": "bytea",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source_hash": {
+ "name": "source_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "workspace_file_collab_state_file_id_workspace_files_id_fk": {
+ "name": "workspace_file_collab_state_file_id_workspace_files_id_fk",
+ "tableFrom": "workspace_file_collab_state",
+ "tableTo": "workspace_files",
+ "columnsFrom": ["file_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workspace_file_search_backfill": {
+ "name": "workspace_file_search_backfill",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "after_workspace_id": {
+ "name": "after_workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "after_file_id": {
+ "name": "after_file_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "completed_at": {
+ "name": "completed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workspace_file_search_dispatch_queue": {
+ "name": "workspace_file_search_dispatch_queue",
+ "schema": "",
+ "columns": {
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "enqueued_at": {
+ "name": "enqueued_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "last_dispatched_at": {
+ "name": "last_dispatched_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "workspace_file_search_dispatch_queue_schedule_idx": {
+ "name": "workspace_file_search_dispatch_queue_schedule_idx",
+ "columns": [
+ {
+ "expression": "last_dispatched_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "first"
+ },
+ {
+ "expression": "enqueued_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workspace_file_search_queue_workspace_fk": {
+ "name": "workspace_file_search_queue_workspace_fk",
+ "tableFrom": "workspace_file_search_dispatch_queue",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workspace_file_search_index": {
+ "name": "workspace_file_search_index",
+ "schema": "",
+ "columns": {
+ "file_id": {
+ "name": "file_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source_content_updated_at": {
+ "name": "source_content_updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "workspace_file_search_index_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "partial": {
+ "name": "partial",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "failure_reason": {
+ "name": "failure_reason",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "line_count": {
+ "name": "line_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "indexed_bytes": {
+ "name": "indexed_bytes",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "dispatched_at": {
+ "name": "dispatched_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": {
+ "workspace_file_search_index_workspace_status_idx": {
+ "name": "workspace_file_search_index_workspace_status_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "source_content_updated_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workspace_file_search_index_pending_dispatch_idx": {
+ "name": "workspace_file_search_index_pending_dispatch_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "updated_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "file_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "source_content_updated_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"workspace_file_search_index\".\"status\" = 'pending' AND \"workspace_file_search_index\".\"dispatched_at\" IS NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workspace_file_search_index_active_dispatch_idx": {
+ "name": "workspace_file_search_index_active_dispatch_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "dispatched_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"workspace_file_search_index\".\"status\" = 'pending' AND \"workspace_file_search_index\".\"dispatched_at\" IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workspace_file_search_index_file_fk": {
+ "name": "workspace_file_search_index_file_fk",
+ "tableFrom": "workspace_file_search_index",
+ "tableTo": "workspace_files",
+ "columnsFrom": ["file_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workspace_file_search_index_workspace_fk": {
+ "name": "workspace_file_search_index_workspace_fk",
+ "tableFrom": "workspace_file_search_index",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "workspace_file_search_index_pk": {
+ "name": "workspace_file_search_index_pk",
+ "columns": ["file_id", "source_content_updated_at"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workspace_file_search_segment": {
+ "name": "workspace_file_search_segment",
+ "schema": "",
+ "columns": {
+ "file_id": {
+ "name": "file_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source_content_updated_at": {
+ "name": "source_content_updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "line_number": {
+ "name": "line_number",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "segment_number": {
+ "name": "segment_number",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "segment_start": {
+ "name": "segment_start",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "line_length": {
+ "name": "line_length",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "content": {
+ "name": "content",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "workspace_file_search_segment_workspace_revision_idx": {
+ "name": "workspace_file_search_segment_workspace_revision_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "file_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "source_content_updated_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workspace_file_search_segment_workspace_content_trgm_idx": {
+ "name": "workspace_file_search_segment_workspace_content_trgm_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last",
+ "opclass": "text_ops"
+ },
+ {
+ "expression": "content",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last",
+ "opclass": "gin_trgm_ops"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "gin",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workspace_file_search_segment_file_fk": {
+ "name": "workspace_file_search_segment_file_fk",
+ "tableFrom": "workspace_file_search_segment",
+ "tableTo": "workspace_files",
+ "columnsFrom": ["file_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workspace_file_search_segment_workspace_fk": {
+ "name": "workspace_file_search_segment_workspace_fk",
+ "tableFrom": "workspace_file_search_segment",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "workspace_file_search_segment_pk": {
+ "name": "workspace_file_search_segment_pk",
+ "columns": ["file_id", "source_content_updated_at", "line_number", "segment_number"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workspace_file_secret_provenance": {
+ "name": "workspace_file_secret_provenance",
+ "schema": "",
+ "columns": {
+ "file_id": {
+ "name": "file_id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "content_updated_at": {
+ "name": "content_updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "entries": {
+ "name": "entries",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "workspace_file_secret_provenance_file_id_workspace_files_id_fk": {
+ "name": "workspace_file_secret_provenance_file_id_workspace_files_id_fk",
+ "tableFrom": "workspace_file_secret_provenance",
+ "tableTo": "workspace_files",
+ "columnsFrom": ["file_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "workspace_file_secret_provenance_status_check": {
+ "name": "workspace_file_secret_provenance_status_check",
+ "value": "\"workspace_file_secret_provenance\".\"status\" IN ('exact', 'unknown', 'unrecorded')"
+ }
+ },
+ "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
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "folder_id": {
+ "name": "folder_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "context": {
+ "name": "context",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "chat_id": {
+ "name": "chat_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "message_id": {
+ "name": "message_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "original_name": {
+ "name": "original_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "display_name": {
+ "name": "display_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "content_type": {
+ "name": "content_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "size": {
+ "name": "size",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "size_bytes": {
+ "name": "size_bytes",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "width": {
+ "name": "width",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "height": {
+ "name": "height",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "uploaded_at": {
+ "name": "uploaded_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "content_updated_at": {
+ "name": "content_updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "secret_provenance_version": {
+ "name": "secret_provenance_version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "workspace_files_key_active_unique": {
+ "name": "workspace_files_key_active_unique",
+ "columns": [
+ {
+ "expression": "key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"workspace_files\".\"deleted_at\" IS NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workspace_files_workspace_folder_name_active_unique": {
+ "name": "workspace_files_workspace_folder_name_active_unique",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "coalesce(\"folder_id\", '')",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "original_name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"workspace_files\".\"deleted_at\" IS NULL AND \"workspace_files\".\"context\" = 'workspace' AND \"workspace_files\".\"workspace_id\" IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workspace_files_chat_display_name_unique": {
+ "name": "workspace_files_chat_display_name_unique",
+ "columns": [
+ {
+ "expression": "chat_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "display_name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"workspace_files\".\"context\" = 'mothership' AND \"workspace_files\".\"chat_id\" IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workspace_files_organization_id_idx": {
+ "name": "workspace_files_organization_id_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "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_folder_id_idx": {
+ "name": "workspace_files_folder_id_idx",
+ "columns": [
+ {
+ "expression": "folder_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": {}
+ },
+ "workspace_files_chat_id_idx": {
+ "name": "workspace_files_chat_id_idx",
+ "columns": [
+ {
+ "expression": "chat_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workspace_files_deleted_at_idx": {
+ "name": "workspace_files_deleted_at_idx",
+ "columns": [
+ {
+ "expression": "deleted_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workspace_files_workspace_deleted_partial_idx": {
+ "name": "workspace_files_workspace_deleted_partial_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "deleted_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"workspace_files\".\"deleted_at\" IS NOT NULL",
+ "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"
+ },
+ "workspace_files_organization_id_organization_id_fk": {
+ "name": "workspace_files_organization_id_organization_id_fk",
+ "tableFrom": "workspace_files",
+ "tableTo": "organization",
+ "columnsFrom": ["organization_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workspace_files_folder_id_folder_id_fk": {
+ "name": "workspace_files_folder_id_folder_id_fk",
+ "tableFrom": "workspace_files",
+ "tableTo": "folder",
+ "columnsFrom": ["folder_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "workspace_files_chat_id_copilot_chats_id_fk": {
+ "name": "workspace_files_chat_id_copilot_chats_id_fk",
+ "tableFrom": "workspace_files",
+ "tableTo": "copilot_chats",
+ "columnsFrom": ["chat_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "workspace_files_organization_binding_check": {
+ "name": "workspace_files_organization_binding_check",
+ "value": "\"workspace_files\".\"organization_id\" IS NULL OR (\"workspace_files\".\"workspace_id\" IS NULL AND \"workspace_files\".\"context\" = 'knowledge-base' AND \"workspace_files\".\"folder_id\" IS NULL AND \"workspace_files\".\"chat_id\" IS NULL)"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.workspace_fork_block_map": {
+ "name": "workspace_fork_block_map",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "child_workspace_id": {
+ "name": "child_workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "parent_workflow_id": {
+ "name": "parent_workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "parent_block_id": {
+ "name": "parent_block_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "child_workflow_id": {
+ "name": "child_workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "child_block_id": {
+ "name": "child_block_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "workspace_fork_block_map_child_ws_parent_unique": {
+ "name": "workspace_fork_block_map_child_ws_parent_unique",
+ "columns": [
+ {
+ "expression": "child_workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "parent_block_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workspace_fork_block_map_child_ws_child_unique": {
+ "name": "workspace_fork_block_map_child_ws_child_unique",
+ "columns": [
+ {
+ "expression": "child_workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "child_block_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workspace_fork_block_map_child_ws_parent_wf_idx": {
+ "name": "workspace_fork_block_map_child_ws_parent_wf_idx",
+ "columns": [
+ {
+ "expression": "child_workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "parent_workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workspace_fork_block_map_child_ws_child_wf_idx": {
+ "name": "workspace_fork_block_map_child_ws_child_wf_idx",
+ "columns": [
+ {
+ "expression": "child_workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "child_workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workspace_fork_block_map_child_workspace_id_workspace_id_fk": {
+ "name": "workspace_fork_block_map_child_workspace_id_workspace_id_fk",
+ "tableFrom": "workspace_fork_block_map",
+ "tableTo": "workspace",
+ "columnsFrom": ["child_workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workspace_fork_dependent_value": {
+ "name": "workspace_fork_dependent_value",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "child_workspace_id": {
+ "name": "child_workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "target_workflow_id": {
+ "name": "target_workflow_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "target_block_id": {
+ "name": "target_block_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "sub_block_key": {
+ "name": "sub_block_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "value": {
+ "name": "value",
+ "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_fork_dependent_value_child_ws_wf_idx": {
+ "name": "workspace_fork_dependent_value_child_ws_wf_idx",
+ "columns": [
+ {
+ "expression": "child_workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "target_workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workspace_fork_dependent_value_field_unique": {
+ "name": "workspace_fork_dependent_value_field_unique",
+ "columns": [
+ {
+ "expression": "child_workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "target_workflow_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "target_block_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "sub_block_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workspace_fork_dependent_value_child_workspace_id_workspace_id_fk": {
+ "name": "workspace_fork_dependent_value_child_workspace_id_workspace_id_fk",
+ "tableFrom": "workspace_fork_dependent_value",
+ "tableTo": "workspace",
+ "columnsFrom": ["child_workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workspace_fork_promote_run": {
+ "name": "workspace_fork_promote_run",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "child_workspace_id": {
+ "name": "child_workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source_workspace_id": {
+ "name": "source_workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "target_workspace_id": {
+ "name": "target_workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "direction": {
+ "name": "direction",
+ "type": "workspace_fork_promote_direction",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "snapshot": {
+ "name": "snapshot",
+ "type": "jsonb",
+ "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()"
+ }
+ },
+ "indexes": {
+ "workspace_fork_promote_run_child_ws_target_unique": {
+ "name": "workspace_fork_promote_run_child_ws_target_unique",
+ "columns": [
+ {
+ "expression": "child_workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "target_workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workspace_fork_promote_run_target_ws_idx": {
+ "name": "workspace_fork_promote_run_target_ws_idx",
+ "columns": [
+ {
+ "expression": "target_workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workspace_fork_promote_run_child_workspace_id_workspace_id_fk": {
+ "name": "workspace_fork_promote_run_child_workspace_id_workspace_id_fk",
+ "tableFrom": "workspace_fork_promote_run",
+ "tableTo": "workspace",
+ "columnsFrom": ["child_workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workspace_fork_promote_run_created_by_user_id_fk": {
+ "name": "workspace_fork_promote_run_created_by_user_id_fk",
+ "tableFrom": "workspace_fork_promote_run",
+ "tableTo": "user",
+ "columnsFrom": ["created_by"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workspace_fork_resource_map": {
+ "name": "workspace_fork_resource_map",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "child_workspace_id": {
+ "name": "child_workspace_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "resource_type": {
+ "name": "resource_type",
+ "type": "workspace_fork_resource_type",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "parent_resource_id": {
+ "name": "parent_resource_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "child_resource_id": {
+ "name": "child_resource_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": 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": {
+ "workspace_fork_resource_map_child_ws_idx": {
+ "name": "workspace_fork_resource_map_child_ws_idx",
+ "columns": [
+ {
+ "expression": "child_workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workspace_fork_resource_map_child_ws_type_idx": {
+ "name": "workspace_fork_resource_map_child_ws_type_idx",
+ "columns": [
+ {
+ "expression": "child_workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "resource_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workspace_fork_resource_map_child_type_parent_unique": {
+ "name": "workspace_fork_resource_map_child_type_parent_unique",
+ "columns": [
+ {
+ "expression": "child_workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "resource_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "parent_resource_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workspace_fork_resource_map_child_workspace_id_workspace_id_fk": {
+ "name": "workspace_fork_resource_map_child_workspace_id_workspace_id_fk",
+ "tableFrom": "workspace_fork_resource_map",
+ "tableTo": "workspace",
+ "columnsFrom": ["child_workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workspace_fork_resource_map_created_by_user_id_fk": {
+ "name": "workspace_fork_resource_map_created_by_user_id_fk",
+ "tableFrom": "workspace_fork_resource_map",
+ "tableTo": "user",
+ "columnsFrom": ["created_by"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workspace_sandbox": {
+ "name": "workspace_sandbox",
+ "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
+ },
+ "language": {
+ "name": "language",
+ "type": "sandbox_language",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "dependencies": {
+ "name": "dependencies",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "cli_tools": {
+ "name": "cli_tools",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "system_packages": {
+ "name": "system_packages",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "spec_hash": {
+ "name": "spec_hash",
+ "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_sandbox_workspace_name_unique": {
+ "name": "workspace_sandbox_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": {}
+ },
+ "workspace_sandbox_workspace_idx": {
+ "name": "workspace_sandbox_workspace_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workspace_sandbox_spec_hash_idx": {
+ "name": "workspace_sandbox_spec_hash_idx",
+ "columns": [
+ {
+ "expression": "spec_hash",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workspace_sandbox_workspace_id_workspace_id_fk": {
+ "name": "workspace_sandbox_workspace_id_workspace_id_fk",
+ "tableFrom": "workspace_sandbox",
+ "tableTo": "workspace",
+ "columnsFrom": ["workspace_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workspace_sandbox_created_by_user_id_fk": {
+ "name": "workspace_sandbox_created_by_user_id_fk",
+ "tableFrom": "workspace_sandbox",
+ "tableTo": "user",
+ "columnsFrom": ["created_by"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ }
+ },
+ "enums": {
+ "public.academy_cert_status": {
+ "name": "academy_cert_status",
+ "schema": "public",
+ "values": ["active", "revoked", "expired"]
+ },
+ "public.background_work_kind": {
+ "name": "background_work_kind",
+ "schema": "public",
+ "values": ["deployment_side_effects", "fork_content_copy", "fork_sync", "fork_rollback"]
+ },
+ "public.background_work_status_value": {
+ "name": "background_work_status_value",
+ "schema": "public",
+ "values": ["pending", "processing", "completed", "completed_with_warnings", "failed"]
+ },
+ "public.billing_blocked_reason": {
+ "name": "billing_blocked_reason",
+ "schema": "public",
+ "values": ["payment_failed", "dispute"]
+ },
+ "public.billing_entity_type": {
+ "name": "billing_entity_type",
+ "schema": "public",
+ "values": ["user", "organization"]
+ },
+ "public.chat_type": {
+ "name": "chat_type",
+ "schema": "public",
+ "values": ["mothership", "copilot"]
+ },
+ "public.copilot_async_tool_status": {
+ "name": "copilot_async_tool_status",
+ "schema": "public",
+ "values": ["pending", "running", "completed", "failed", "cancelled", "delivered"]
+ },
+ "public.copilot_run_status": {
+ "name": "copilot_run_status",
+ "schema": "public",
+ "values": ["active", "paused_waiting_for_tool", "resuming", "complete", "error", "cancelled"]
+ },
+ "public.copilot_tool_permission_decision": {
+ "name": "copilot_tool_permission_decision",
+ "schema": "public",
+ "values": ["allow", "allow_chat", "always_allow", "skip"]
+ },
+ "public.credential_group_enrollment_status": {
+ "name": "credential_group_enrollment_status",
+ "schema": "public",
+ "values": ["invited", "delivery_failed", "in_progress", "completed", "revoked"]
+ },
+ "public.credential_group_status": {
+ "name": "credential_group_status",
+ "schema": "public",
+ "values": ["active", "disabled"]
+ },
+ "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_type": {
+ "name": "credential_type",
+ "schema": "public",
+ "values": [
+ "oauth",
+ "managed_oauth",
+ "managed_mcp",
+ "env_workspace",
+ "env_personal",
+ "service_account",
+ "personal_token"
+ ]
+ },
+ "public.data_drain_cadence": {
+ "name": "data_drain_cadence",
+ "schema": "public",
+ "values": ["hourly", "daily"]
+ },
+ "public.data_drain_destination": {
+ "name": "data_drain_destination",
+ "schema": "public",
+ "values": ["s3", "gcs", "azure_blob", "datadog", "bigquery", "snowflake", "webhook"]
+ },
+ "public.data_drain_run_status": {
+ "name": "data_drain_run_status",
+ "schema": "public",
+ "values": ["running", "success", "failed"]
+ },
+ "public.data_drain_run_trigger": {
+ "name": "data_drain_run_trigger",
+ "schema": "public",
+ "values": ["cron", "manual"]
+ },
+ "public.data_drain_source": {
+ "name": "data_drain_source",
+ "schema": "public",
+ "values": ["workflow_logs", "job_logs", "audit_logs", "copilot_chats", "copilot_runs"]
+ },
+ "public.execution_large_value_reference_source": {
+ "name": "execution_large_value_reference_source",
+ "schema": "public",
+ "values": ["execution_log", "paused_snapshot"]
+ },
+ "public.folder_resource_type": {
+ "name": "folder_resource_type",
+ "schema": "public",
+ "values": ["workflow", "file", "knowledge_base", "table"]
+ },
+ "public.invitation_kind": {
+ "name": "invitation_kind",
+ "schema": "public",
+ "values": ["organization", "workspace"]
+ },
+ "public.invitation_membership_intent": {
+ "name": "invitation_membership_intent",
+ "schema": "public",
+ "values": ["internal", "external"]
+ },
+ "public.invitation_status": {
+ "name": "invitation_status",
+ "schema": "public",
+ "values": ["pending", "accepted", "rejected", "cancelled", "expired"]
+ },
+ "public.managed_oauth_credential_status": {
+ "name": "managed_oauth_credential_status",
+ "schema": "public",
+ "values": ["active", "needs_reauth", "revoked"]
+ },
+ "public.permission_type": {
+ "name": "permission_type",
+ "schema": "public",
+ "values": ["admin", "write", "read"]
+ },
+ "public.sandbox_image_status": {
+ "name": "sandbox_image_status",
+ "schema": "public",
+ "values": ["pending", "building", "ready", "failed"]
+ },
+ "public.sandbox_language": {
+ "name": "sandbox_language",
+ "schema": "public",
+ "values": ["javascript", "python"]
+ },
+ "public.secret_usage_scope": {
+ "name": "secret_usage_scope",
+ "schema": "public",
+ "values": ["workspace", "personal"]
+ },
+ "public.secret_usage_source": {
+ "name": "secret_usage_source",
+ "schema": "public",
+ "values": ["workflow", "copilot", "mcp"]
+ },
+ "public.upload_session_method": {
+ "name": "upload_session_method",
+ "schema": "public",
+ "values": ["put", "multipart"]
+ },
+ "public.upload_session_provider": {
+ "name": "upload_session_provider",
+ "schema": "public",
+ "values": ["local", "s3", "blob", "gcs"]
+ },
+ "public.upload_session_purpose": {
+ "name": "upload_session_purpose",
+ "schema": "public",
+ "values": [
+ "workspace_file",
+ "table_import",
+ "knowledge_document",
+ "profile_picture",
+ "workspace_logo",
+ "mothership_attachment",
+ "execution_attachment"
+ ]
+ },
+ "public.upload_session_status": {
+ "name": "upload_session_status",
+ "schema": "public",
+ "values": [
+ "uploading",
+ "completing",
+ "finalizing",
+ "completed",
+ "aborting",
+ "aborted",
+ "failed",
+ "expired"
+ ]
+ },
+ "public.usage_log_category": {
+ "name": "usage_log_category",
+ "schema": "public",
+ "values": ["model", "fixed", "tool", "model_unbilled"]
+ },
+ "public.usage_log_source": {
+ "name": "usage_log_source",
+ "schema": "public",
+ "values": [
+ "workflow",
+ "wand",
+ "copilot",
+ "workspace-chat",
+ "mcp_copilot",
+ "mothership_block",
+ "knowledge-base",
+ "voice-input",
+ "enrichment",
+ "voice-output",
+ "api-tool"
+ ]
+ },
+ "public.workspace_file_search_index_status": {
+ "name": "workspace_file_search_index_status",
+ "schema": "public",
+ "values": ["pending", "ready", "skipped", "failed"]
+ },
+ "public.workspace_fork_promote_direction": {
+ "name": "workspace_fork_promote_direction",
+ "schema": "public",
+ "values": ["push", "pull"]
+ },
+ "public.workspace_fork_resource_type": {
+ "name": "workspace_fork_resource_type",
+ "schema": "public",
+ "values": [
+ "workflow",
+ "oauth_credential",
+ "service_account_credential",
+ "env_var",
+ "table",
+ "knowledge_base",
+ "knowledge_document",
+ "file",
+ "file_folder",
+ "mcp_server",
+ "workflow_mcp_server",
+ "custom_block",
+ "custom_tool",
+ "skill"
+ ]
+ },
+ "public.workspace_mode": {
+ "name": "workspace_mode",
+ "schema": "public",
+ "values": ["personal", "organization", "grandfathered_shared"]
+ }
+ },
+ "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 0696d87277e..bb21d021c69 100644
--- a/packages/db/migrations/meta/_journal.json
+++ b/packages/db/migrations/meta/_journal.json
@@ -2283,6 +2283,13 @@
"when": 1788844432798,
"tag": "0326_organization_search_approvals",
"breakpoints": true
+ },
+ {
+ "idx": 327,
+ "version": "7",
+ "when": 1788849461024,
+ "tag": "0327_organization_connected_accounts",
+ "breakpoints": true
}
]
}
diff --git a/packages/db/schema.ts b/packages/db/schema.ts
index 4dcee2ebfee..7c9a735a7f6 100644
--- a/packages/db/schema.ts
+++ b/packages/db/schema.ts
@@ -3861,14 +3861,16 @@ export const mcpServers = pgTable(
'mcp_servers',
{
id: text('id').primaryKey(),
- workspaceId: text('workspace_id')
- .notNull()
- .references(() => workspace.id, { onDelete: 'cascade' }),
+ workspaceId: text('workspace_id').references(() => workspace.id, { onDelete: 'cascade' }),
+ organizationId: text('organization_id').references(() => organization.id, {
+ onDelete: 'cascade',
+ }),
credentialGroupId: text('credential_group_id').references(
(): AnyPgColumn => credentialGroup.id,
{ onDelete: 'set null' }
),
managedConnectorId: text('managed_connector_id'),
+ oauthConfigVersion: integer('oauth_config_version').notNull().default(1),
// Track who created the server, but workspace owns it
createdBy: text('created_by').references(() => user.id, { onDelete: 'set null' }),
@@ -3909,6 +3911,15 @@ export const mcpServers = pgTable(
updatedAt: timestamp('updated_at').notNull().defaultNow(),
},
(table) => ({
+ ownerCheck: check(
+ 'mcp_servers_owner_check',
+ sql`num_nonnulls(${table.workspaceId}, ${table.organizationId}) = 1`
+ ),
+ organizationManagedCheck: check(
+ 'mcp_servers_organization_managed_check',
+ sql`${table.organizationId} IS NULL OR ${table.credentialGroupId} IS NOT NULL`
+ ),
+ organizationIdx: index('mcp_servers_organization_id_idx').on(table.organizationId),
// Primary access pattern - active servers by workspace
workspaceEnabledIdx: index('mcp_servers_workspace_enabled_idx').on(
table.workspaceId,
@@ -3955,9 +3966,10 @@ export const mcpServerOauth = pgTable(
.references(() => mcpServers.id, { onDelete: 'cascade' }),
/** Last workspace user who initiated/completed authorization. */
userId: text('user_id').references(() => user.id, { onDelete: 'set null' }),
- workspaceId: text('workspace_id')
- .notNull()
- .references(() => workspace.id, { onDelete: 'cascade' }),
+ workspaceId: text('workspace_id').references(() => workspace.id, { onDelete: 'cascade' }),
+ organizationId: text('organization_id').references(() => organization.id, {
+ onDelete: 'cascade',
+ }),
/**
* Encrypted JSON of the RFC 7591 dynamic client registration result.
@@ -3987,6 +3999,10 @@ export const mcpServerOauth = pgTable(
updatedAt: timestamp('updated_at').notNull().defaultNow(),
},
(table) => ({
+ ownerCheck: check(
+ 'mcp_server_oauth_owner_check',
+ sql`num_nonnulls(${table.workspaceId}, ${table.organizationId}) = 1`
+ ),
serverUnique: uniqueIndex('mcp_server_oauth_server_unique').on(table.mcpServerId),
stateIdx: index('mcp_server_oauth_state_idx').on(table.state),
})
@@ -4624,6 +4640,7 @@ export const credential = pgTable(
mcpServerId: text('mcp_server_id').references(() => mcpServers.id, {
onDelete: 'cascade',
}),
+ mcpOauthConfigVersion: integer('mcp_oauth_config_version'),
managedOauthScopeVersion: integer('managed_oauth_scope_version'),
providerSubjectId: text('provider_subject_id'),
providerTenantId: text('provider_tenant_id'),
@@ -4650,7 +4667,7 @@ export const credential = pgTable(
organizationIdIdx: index('credential_organization_id_idx').on(table.organizationId),
organizationTypeCheck: check(
'credential_organization_type_check',
- sql`${table.organizationId} IS NULL OR ${table.type} IN ('oauth', 'managed_oauth', 'service_account', 'personal_token')`
+ sql`${table.organizationId} IS NULL OR ${table.type} IN ('oauth', 'managed_oauth', 'managed_mcp', 'service_account', 'personal_token')`
),
organizationAccountUnique: uniqueIndex('credential_organization_account_unique')
.on(table.organizationId, table.accountId)
@@ -4800,11 +4817,12 @@ export interface CredentialGroupOptionConfig {
status: 'active' | 'disabled'
}
-/** Workspace-owned configuration for collecting several managed OAuth credentials. */
+/** Singleton configuration for collecting an organization's connected accounts. */
export const credentialGroup = pgTable(
'credential_group',
{
id: text('id').primaryKey(),
+ /** contract-pending(org connected accounts fully deployed and legacy Search migrated): remove workspace ownership. */
workspaceId: text('workspace_id').references(() => workspace.id, { onDelete: 'cascade' }),
organizationId: text('organization_id').references(() => organization.id, {
onDelete: 'cascade',
@@ -4850,6 +4868,8 @@ export const credentialGroupEnrollment = pgTable(
.notNull()
.references(() => credentialGroup.id, { onDelete: 'cascade' }),
email: text('email').notNull(),
+ /** Bound once after the invitee signs in with the verified invitation email. */
+ userId: text('user_id').references(() => user.id, { onDelete: 'cascade' }),
status: credentialGroupEnrollmentStatusEnum('status').notNull().default('invited'),
invitationTokenHash: text('invitation_token_hash').notNull(),
invitationExpiresAt: timestamp('invitation_expires_at').notNull(),
@@ -4863,6 +4883,10 @@ export const credentialGroupEnrollment = pgTable(
updatedAt: timestamp('updated_at').notNull().defaultNow(),
},
(table) => ({
+ groupUserUnique: uniqueIndex('credential_group_enrollment_group_user_unique')
+ .on(table.credentialGroupId, table.userId)
+ .where(sql`${table.userId} IS NOT NULL`),
+ userIdx: index('credential_group_enrollment_user_id_idx').on(table.userId),
groupEmailUnique: uniqueIndex('credential_group_enrollment_group_email_unique').on(
table.credentialGroupId,
table.email
@@ -5081,14 +5105,15 @@ export const permissionGroupMember = pgTable(
})
)
-/** Versioned statement policy attached to one canonical workspace resource. */
+/** Versioned statement policy attached to one canonical workspace or organization resource. */
export const resourcePolicy = pgTable(
'resource_policy',
{
id: text('id').primaryKey(),
- workspaceId: text('workspace_id')
- .notNull()
- .references(() => workspace.id, { onDelete: 'cascade' }),
+ workspaceId: text('workspace_id').references(() => workspace.id, { onDelete: 'cascade' }),
+ organizationId: text('organization_id').references(() => organization.id, {
+ onDelete: 'cascade',
+ }),
resourceType: text('resource_type').notNull(),
resourceId: text('resource_id').notNull(),
revision: integer('revision').notNull().default(1),
@@ -5099,6 +5124,11 @@ export const resourcePolicy = pgTable(
updatedAt: timestamp('updated_at').notNull().defaultNow(),
},
(table) => ({
+ ownerCheck: check(
+ 'resource_policy_owner_check',
+ sql`num_nonnulls(${table.workspaceId}, ${table.organizationId}) = 1`
+ ),
+ organizationIdx: index('resource_policy_organization_id_idx').on(table.organizationId),
resourceUnique: uniqueIndex('resource_policy_resource_unique').on(
table.resourceType,
table.resourceId
diff --git a/packages/db/script-migrations/0010_backfill_credential_group_resource_policies.test.ts b/packages/db/script-migrations/0010_backfill_credential_group_resource_policies.test.ts
index 9f844ea951c..f5644fd2b62 100644
--- a/packages/db/script-migrations/0010_backfill_credential_group_resource_policies.test.ts
+++ b/packages/db/script-migrations/0010_backfill_credential_group_resource_policies.test.ts
@@ -15,6 +15,7 @@ import {
parseCredentialGroupPolicyDocument,
reconcileCredentialGroupResourcePolicies,
type StoredCredentialGroupPolicyRow,
+ validateOrganizationAccountPolicyDocument,
} from '../credential-group-resource-policies'
const WORKFLOW_POLICY = (id: string, workflowIds: string[]) => ({
@@ -336,7 +337,8 @@ describe('Credential Group resource policy lifecycle', () => {
await store.listMissingPolicies('', 2)
await store.findRelationalInvariantViolation()
expect(queries[6]).toContain('AND cg.workspace_id IS NOT NULL')
- expect(queries[7]).toContain('WHERE cg.workspace_id IS NOT NULL AND (rp.resource_id IS NULL')
+ expect(queries[7]).toContain('WHERE rp.resource_id IS NULL')
+ expect(queries[7]).toContain('rp.organization_id IS DISTINCT FROM cg.organization_id')
})
it('keeps table creation in 0309 and lifecycle reconciliation in the db:push post-step', async () => {
@@ -363,3 +365,48 @@ describe('Credential Group resource policy lifecycle', () => {
expect(helperSource).not.toContain("document ? 'grants'")
})
})
+
+describe('organization account policy validation', () => {
+ const policy = (ids: string[]) => ({
+ version: 2,
+ resource: { type: 'credential_group', id: 'group-org' },
+ statements: ids.length
+ ? [
+ {
+ sid: 'WorkspaceCredentialAccess',
+ effect: 'allow',
+ actions: ['credential_groups.credentials.use'],
+ principals: ids.map((workspaceId) => ({ type: 'workspace', workspaceId })),
+ },
+ ]
+ : [],
+ })
+ it('accepts deny-by-default and the maximum workspace allowlist', () => {
+ expect(() => validateOrganizationAccountPolicyDocument(policy([]), 'group-org')).not.toThrow()
+ expect(() =>
+ validateOrganizationAccountPolicyDocument(
+ policy(Array.from({ length: 1000 }, (_, i) => `workspace-${String(i).padStart(4, '0')}`)),
+ 'group-org'
+ )
+ ).not.toThrow()
+ })
+ it.each([
+ ['b', 'a'],
+ ['a', 'a'],
+ ])('rejects unsorted or duplicate workspace principals', (...ids) => {
+ expect(() => validateOrganizationAccountPolicyDocument(policy(ids), 'group-org')).toThrow(
+ 'unique and sorted'
+ )
+ })
+ it('rejects workflow grants and a policy for another group', () => {
+ expect(() =>
+ validateOrganizationAccountPolicyDocument(
+ WORKFLOW_POLICY('group-org', ['workflow-1']),
+ 'group-org'
+ )
+ ).toThrow('version must be 2')
+ expect(() => validateOrganizationAccountPolicyDocument(policy([]), 'other-group')).toThrow(
+ 'canonical resource'
+ )
+ })
+})