From 929291635dc670b14b6b2dc435999d46a089b069 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Thu, 20 Aug 2026 08:15:05 -0700 Subject: [PATCH 1/6] feat(credential-groups): add resource access policies --- .../api/workflows/[id]/deployed/route.test.ts | 1 + .../app/api/workflows/[id]/deployed/route.ts | 28 +- .../[groupId]/access/route.test.ts | 125 ++++++++ .../[groupId]/access/route.ts | 47 +++ .../settings/[section]/search-params.ts | 2 +- .../components/credential-group-access.tsx | 271 ++++++++++++++++++ .../components/credential-group-detail.tsx | 28 +- .../workflow/workflow-handler.test.ts | 56 +++- .../handlers/workflow/workflow-handler.ts | 24 ++ apps/sim/executor/types.ts | 3 +- apps/sim/hooks/queries/credential-groups.ts | 43 +++ .../queries/utils/credential-group-queries.ts | 2 + .../api/contracts/credential-groups.test.ts | 35 +++ .../lib/api/contracts/credential-groups.ts | 52 ++++ apps/sim/lib/auth/internal-delegation.test.ts | 85 +++++- apps/sim/lib/auth/internal-delegation.ts | 35 +++ apps/sim/lib/auth/internal.test.ts | 36 +++ apps/sim/lib/auth/internal.ts | 40 +++ .../application/authorization.ts | 58 ++-- .../application/list-credentials.test.ts | 21 ++ .../application/manage-access.test.ts | 166 +++++++++++ .../application/manage-access.ts | 117 ++++++++ .../application/operations.ts | 12 + .../resolve-managed-oauth-token.test.ts | 19 +- .../resolve-managed-oauth-token.ts | 12 +- .../resource-policies/authorization.test.ts | 235 +++++++++++++++ .../lib/resource-policies/authorization.ts | 123 ++++++++ apps/sim/lib/resource-policies/management.ts | 106 +++++++ apps/sim/lib/resource-policies/repository.ts | 122 ++++++++ apps/sim/lib/resource-policies/types.test.ts | 75 +++++ apps/sim/lib/resource-policies/types.ts | 128 +++++++++ .../lib/workflows/executor/execution-core.ts | 3 + design/credential-group-authorization.md | 20 +- design/principal-passing.md | 8 +- design/resource-policies.md | 13 +- packages/auth/src/principal.ts | 5 + packages/db/schema.ts | 26 ++ scripts/check-api-validation-contracts.ts | 4 +- 38 files changed, 2106 insertions(+), 80 deletions(-) create mode 100644 apps/sim/app/api/workspaces/[id]/credential-groups/[groupId]/access/route.test.ts create mode 100644 apps/sim/app/api/workspaces/[id]/credential-groups/[groupId]/access/route.ts create mode 100644 apps/sim/ee/credential-groups/components/credential-group-access.tsx create mode 100644 apps/sim/lib/credential-groups/application/manage-access.test.ts create mode 100644 apps/sim/lib/credential-groups/application/manage-access.ts create mode 100644 apps/sim/lib/resource-policies/authorization.test.ts create mode 100644 apps/sim/lib/resource-policies/authorization.ts create mode 100644 apps/sim/lib/resource-policies/management.ts create mode 100644 apps/sim/lib/resource-policies/repository.ts create mode 100644 apps/sim/lib/resource-policies/types.test.ts create mode 100644 apps/sim/lib/resource-policies/types.ts diff --git a/apps/sim/app/api/workflows/[id]/deployed/route.test.ts b/apps/sim/app/api/workflows/[id]/deployed/route.test.ts index 374b99edaa5..fe903c3a19f 100644 --- a/apps/sim/app/api/workflows/[id]/deployed/route.test.ts +++ b/apps/sim/app/api/workflows/[id]/deployed/route.test.ts @@ -49,6 +49,7 @@ const DEPLOYED_STATE = { loops: {}, parallels: {}, variables: {}, + deploymentVersionId: 'deployment-version-1', } const SESSION = { diff --git a/apps/sim/app/api/workflows/[id]/deployed/route.ts b/apps/sim/app/api/workflows/[id]/deployed/route.ts index 1df1cefeb2c..6d806999422 100644 --- a/apps/sim/app/api/workflows/[id]/deployed/route.ts +++ b/apps/sim/app/api/workflows/[id]/deployed/route.ts @@ -26,17 +26,23 @@ export const GET = defineInternalJsonRoute({ errorPolicy: internalOrchestrationErrorPolicy, mapInput: ({ params }) => ({ workflowId: params.id, state: 'deployed' as const }), useCase: readWorkflowDefinition, - present: ({ state }) => ({ - deployedState: state - ? deployedWorkflowStateSchema.parse({ - blocks: state.blocks, - edges: state.edges, - loops: state.loops, - parallels: state.parallels, - variables: 'variables' in state ? (state.variables ?? {}) : {}, - }) - : null, - }), + present: ({ state }) => { + if (state && (!('deploymentVersionId' in state) || !state.deploymentVersionId)) { + throw new Error('Deployed workflow state is missing its deployment version') + } + return { + deployedState: state + ? deployedWorkflowStateSchema.parse({ + blocks: state.blocks, + edges: state.edges, + loops: state.loops, + parallels: state.parallels, + variables: 'variables' in state ? (state.variables ?? {}) : {}, + deploymentVersionId: state.deploymentVersionId, + }) + : null, + } + }, onSuccess: ({ input, result }) => { if (!result.state) logger.warn('Workflow has no active deployed state', input) }, diff --git a/apps/sim/app/api/workspaces/[id]/credential-groups/[groupId]/access/route.test.ts b/apps/sim/app/api/workspaces/[id]/credential-groups/[groupId]/access/route.test.ts new file mode 100644 index 00000000000..5dc109ea52a --- /dev/null +++ b/apps/sim/app/api/workspaces/[id]/credential-groups/[groupId]/access/route.test.ts @@ -0,0 +1,125 @@ +/** + * @vitest-environment node + */ + +import { NextRequest } from 'next/server' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const mocks = vi.hoisted(() => ({ + getSession: vi.fn(), + read: vi.fn(), + update: vi.fn(), +})) + +vi.mock('@/lib/auth', () => ({ getSession: mocks.getSession })) + +vi.mock('@/lib/credential-groups/application/manage-access', () => ({ + readCredentialGroupAccess: { + operation: { id: 'credential_groups.access.read' }, + execute: mocks.read, + }, + updateCredentialGroupAccess: { + operation: { id: 'credential_groups.access.update' }, + execute: mocks.update, + }, +})) + +import { GET, PUT } from '@/app/api/workspaces/[id]/credential-groups/[groupId]/access/route' + +const WORKSPACE_ID = '11111111-1111-4111-8111-111111111111' +const GROUP_ID = 'group-1' +const url = `http://localhost:3000/api/workspaces/${WORKSPACE_ID}/credential-groups/${GROUP_ID}/access` +const context = { params: Promise.resolve({ id: WORKSPACE_ID, groupId: GROUP_ID }) } + +describe('Credential Group access route', () => { + beforeEach(() => { + vi.clearAllMocks() + mocks.getSession.mockResolvedValue({ + user: { id: 'admin-1' }, + session: { id: 'session-1' }, + }) + mocks.read.mockResolvedValue({ revision: 0, grants: [] }) + mocks.update.mockResolvedValue({ + revision: 1, + grants: [ + { + id: 'grant-1', + subject: { type: 'workflow', workflowId: 'workflow-1' }, + }, + ], + }) + }) + + it('reads the managed policy without exposing the built-in actor rule', async () => { + const request = new NextRequest(url) + const response = await GET(request, context) + + expect(response.status).toBe(200) + expect(await response.json()).toEqual({ revision: 0, grants: [] }) + expect(mocks.read).toHaveBeenCalledWith({ + principal: { kind: 'session', userId: 'admin-1', sessionId: 'session-1' }, + input: { assertedWorkspaceId: WORKSPACE_ID, credentialGroupId: GROUP_ID }, + request, + }) + }) + + it('updates exact managed subjects with optimistic revision input', async () => { + const body = { + expectedRevision: 0, + grants: [{ subject: { type: 'workflow', workflowId: 'workflow-1' } }], + } + const request = new NextRequest(url, { + method: 'PUT', + body: JSON.stringify(body), + headers: { 'content-type': 'application/json' }, + }) + const response = await PUT(request, context) + + expect(response.status).toBe(200) + expect(mocks.update).toHaveBeenCalledWith({ + principal: { kind: 'session', userId: 'admin-1', sessionId: 'session-1' }, + input: { + assertedWorkspaceId: WORKSPACE_ID, + credentialGroupId: GROUP_ID, + ...body, + }, + request, + }) + }) + + it('authenticates before parsing a malformed policy body', async () => { + mocks.getSession.mockResolvedValue(null) + const request = new NextRequest(url, { + method: 'PUT', + body: '{', + headers: { 'content-type': 'application/json' }, + }) + + const response = await PUT(request, context) + + expect(response.status).toBe(401) + expect(mocks.update).not.toHaveBeenCalled() + }) + + it('rejects caller-supplied effects and actions at the HTTP boundary', async () => { + const request = new NextRequest(url, { + method: 'PUT', + body: JSON.stringify({ + expectedRevision: 0, + grants: [ + { + subject: { type: 'workflow', workflowId: 'workflow-1' }, + effect: 'allow', + actions: ['credential_groups.credentials.use'], + }, + ], + }), + headers: { 'content-type': 'application/json' }, + }) + + const response = await PUT(request, context) + + expect(response.status).toBe(400) + expect(mocks.update).not.toHaveBeenCalled() + }) +}) diff --git a/apps/sim/app/api/workspaces/[id]/credential-groups/[groupId]/access/route.ts b/apps/sim/app/api/workspaces/[id]/credential-groups/[groupId]/access/route.ts new file mode 100644 index 00000000000..735a2b2e608 --- /dev/null +++ b/apps/sim/app/api/workspaces/[id]/credential-groups/[groupId]/access/route.ts @@ -0,0 +1,47 @@ +import { + getCredentialGroupAccessContract, + updateCredentialGroupAccessContract, +} from '@/lib/api/contracts/credential-groups' +import { + defineInternalJsonRoute, + internalRateLimits, + internalSessionAuth, +} from '@/lib/api/server/routes' +import { + readCredentialGroupAccess, + updateCredentialGroupAccess, +} from '@/lib/credential-groups/application/manage-access' +import { credentialGroupOperations } from '@/lib/credential-groups/application/operations' +import { createCredentialGroupInternalErrorPolicy } from '@/app/api/workspaces/[id]/credential-groups/error-policy' + +const rateLimit = internalRateLimits.none({ + reason: 'Credential Group access changes are workspace-admin control-plane operations', +}) + +export const GET = defineInternalJsonRoute({ + contract: getCredentialGroupAccessContract, + auth: internalSessionAuth, + operation: credentialGroupOperations.readAccess, + rateLimit, + errorPolicy: createCredentialGroupInternalErrorPolicy('Failed to read Credential Group access'), + mapInput: ({ params }) => ({ + assertedWorkspaceId: params.id, + credentialGroupId: params.groupId, + }), + useCase: readCredentialGroupAccess, +}) + +export const PUT = defineInternalJsonRoute({ + contract: updateCredentialGroupAccessContract, + auth: internalSessionAuth, + operation: credentialGroupOperations.updateAccess, + rateLimit, + errorPolicy: createCredentialGroupInternalErrorPolicy('Failed to update Credential Group access'), + mapInput: ({ params, body }) => ({ + assertedWorkspaceId: params.id, + credentialGroupId: params.groupId, + expectedRevision: body.expectedRevision, + grants: body.grants, + }), + useCase: updateCredentialGroupAccess, +}) diff --git a/apps/sim/app/workspace/[workspaceId]/settings/[section]/search-params.ts b/apps/sim/app/workspace/[workspaceId]/settings/[section]/search-params.ts index aeb354f260d..c95046d925f 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/[section]/search-params.ts +++ b/apps/sim/app/workspace/[workspaceId]/settings/[section]/search-params.ts @@ -107,7 +107,7 @@ export const credentialGroupIdUrlKeys = { /** Active view inside a credential-group detail page. */ export const credentialGroupTabParam = { key: 'credential-group-tab', - parser: parseAsStringLiteral(['details', 'people'] as const).withDefault('details'), + parser: parseAsStringLiteral(['details', 'people', 'access'] as const).withDefault('details'), } as const /** Tab view-state: clean URLs, no back-stack churn. */ diff --git a/apps/sim/ee/credential-groups/components/credential-group-access.tsx b/apps/sim/ee/credential-groups/components/credential-group-access.tsx new file mode 100644 index 00000000000..b7ea50f931a --- /dev/null +++ b/apps/sim/ee/credential-groups/components/credential-group-access.tsx @@ -0,0 +1,271 @@ +'use client' + +import { useState } from 'react' +import { + Chip, + ChipDropdown, + ChipModal, + ChipModalBody, + ChipModalError, + ChipModalField, + ChipModalFooter, + ChipModalHeader, + toast, +} from '@sim/emcn' +import { Plus, ShieldCheck } from '@sim/emcn/icons' +import { getErrorMessage } from '@sim/utils/errors' +import type { CredentialGroupAccessGrant } from '@/lib/api/contracts/credential-groups' +import type { ResourcePolicySubject } from '@/lib/resource-policies/types' +import { RowActionsMenu } from '@/app/workspace/[workspaceId]/settings/components/row-actions-menu' +import { SettingsEmptyState } from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state' +import { + RESOURCE_LIST_STACK, + SettingsResourceRow, +} from '@/app/workspace/[workspaceId]/settings/components/settings-resource-row' +import { SettingsSection } from '@/app/workspace/[workspaceId]/settings/components/settings-section/settings-section' +import { + usePermissionGroups, + useUserPermissionConfig, +} from '@/ee/access-control/hooks/permission-groups' +import { + useCredentialGroupAccess, + useUpdateCredentialGroupAccess, +} from '@/hooks/queries/credential-groups' +import { useWorkflows } from '@/hooks/queries/workflows' + +interface CredentialGroupAccessProps { + workspaceId: string + groupId: string +} + +type ManagedSubjectType = 'workflow' | 'access_control_group' | 'workspace_role' + +const SUBJECT_TYPE_OPTIONS = [ + { value: 'workflow', label: 'Workflow' }, + { value: 'access_control_group', label: 'Access Control Group' }, + { value: 'workspace_role', label: 'Workspace role' }, +] as const + +const WORKSPACE_ROLE_OPTIONS = [ + { value: 'read', label: 'Readers and above' }, + { value: 'write', label: 'Writers and admins' }, + { value: 'admin', label: 'Admins only' }, +] as const + +function subjectKey(subject: ResourcePolicySubject): string { + return JSON.stringify(subject) +} + +export function CredentialGroupAccess({ workspaceId, groupId }: CredentialGroupAccessProps) { + const access = useCredentialGroupAccess(workspaceId, groupId) + const updateAccess = useUpdateCredentialGroupAccess() + const workflows = useWorkflows(workspaceId) + const permissionConfig = useUserPermissionConfig(workspaceId) + const permissionGroups = usePermissionGroups(permissionConfig.data?.organizationId ?? undefined) + const [showAdd, setShowAdd] = useState(false) + const [subjectType, setSubjectType] = useState('workflow') + const [targetId, setTargetId] = useState('') + const [modalError, setModalError] = useState(null) + + const policy = access.data + const applicablePermissionGroups = (permissionGroups.data ?? []).filter( + (group) => group.isDefault || group.workspaces.some((workspace) => workspace.id === workspaceId) + ) + const targetOptions = + subjectType === 'workflow' + ? (workflows.data ?? []).map((workflow) => ({ + value: workflow.id, + label: `${workflow.name}${workflow.isDeployed ? '' : ' (not deployed)'}`, + })) + : subjectType === 'access_control_group' + ? applicablePermissionGroups.map((group) => ({ value: group.id, label: group.name })) + : [...WORKSPACE_ROLE_OPTIONS] + + const labelForSubject = (subject: ResourcePolicySubject): string => { + switch (subject.type) { + case 'workflow': + return ( + workflows.data?.find((workflow) => workflow.id === subject.workflowId)?.name ?? + subject.workflowId + ) + case 'access_control_group': + return ( + permissionGroups.data?.find((group) => group.id === subject.accessControlGroupId)?.name ?? + subject.accessControlGroupId + ) + case 'workspace_role': + return ( + WORKSPACE_ROLE_OPTIONS.find((role) => role.value === subject.minimumRole)?.label ?? + subject.minimumRole + ) + case 'user': + return `User ${subject.userId}` + case 'external_identity': + return `${subject.provider} user ${subject.subjectId}` + } + } + + const descriptionForSubject = (subject: ResourcePolicySubject): string => { + if (subject.type === 'workflow') return 'Use all credentials in deployed runs' + return 'Use all credentials in this group' + } + + const saveGrants = async ( + grants: Array<{ id?: string; subject: ResourcePolicySubject }> + ): Promise => { + if (!policy) return false + try { + await updateAccess.mutateAsync({ + workspaceId, + groupId, + body: { expectedRevision: policy.revision, grants }, + }) + return true + } catch (error) { + toast.error(getErrorMessage(error, 'Could not update access')) + return false + } + } + + const handleAdd = async () => { + if (!targetId || !policy) return + let subject: ResourcePolicySubject + if (subjectType === 'workflow') { + subject = { type: 'workflow', workflowId: targetId } + } else if (subjectType === 'access_control_group') { + subject = { type: 'access_control_group', accessControlGroupId: targetId } + } else { + subject = { + type: 'workspace_role', + minimumRole: targetId as 'read' | 'write' | 'admin', + } + } + if (policy.grants.some((grant) => subjectKey(grant.subject) === subjectKey(subject))) { + setModalError('This subject already has access') + return + } + if ( + await saveGrants([ + ...policy.grants.map((grant) => ({ id: grant.id, subject: grant.subject })), + { subject }, + ]) + ) { + setShowAdd(false) + setTargetId('') + setModalError(null) + toast.success('Access added') + } + } + + const handleRemove = async (grantId: string) => { + if (!policy) return + const next = policy.grants + .filter((grant) => grant.id !== grantId) + .map((grant) => ({ id: grant.id, subject: grant.subject })) + if (await saveGrants(next)) toast.success('Access removed') + } + + if (access.error) { + return ( + + {getErrorMessage(access.error, "Couldn't load access")} + + ) + } + if (access.isPending || !policy) return null + + return ( + <> + setShowAdd(true)}> + Add access + + } + > + {policy.grants.length === 0 ? ( + No additional access grants + ) : ( +
+ {policy.grants.map((grant: CredentialGroupAccessGrant) => ( + } + iconFilled + title={labelForSubject(grant.subject)} + description={descriptionForSubject(grant.subject)} + trailing={ + void handleRemove(grant.id), + disabled: updateAccess.isPending, + }, + ]} + /> + } + /> + ))} +
+ )} +
+ + { + setShowAdd(open) + if (!open) { + setTargetId('') + setModalError(null) + } + }} + dismissDisabled={updateAccess.isPending} + size='sm' + srTitle='Add Credential Group access' + > + setShowAdd(false)}>Add access + + + { + setSubjectType(value as ManagedSubjectType) + setTargetId('') + setModalError(null) + }} + options={[...SUBJECT_TYPE_OPTIONS]} + /> + + + { + setTargetId(value) + setModalError(null) + }} + options={targetOptions} + placeholder='Select one' + /> + + {modalError} + + setShowAdd(false)} + cancelDisabled={updateAccess.isPending} + primaryAction={{ + label: updateAccess.isPending ? 'Adding...' : 'Add access', + onClick: () => void handleAdd(), + disabled: !targetId || updateAccess.isPending, + }} + /> + + + ) +} diff --git a/apps/sim/ee/credential-groups/components/credential-group-detail.tsx b/apps/sim/ee/credential-groups/components/credential-group-detail.tsx index 062b8783cd8..08e5db4a0ff 100644 --- a/apps/sim/ee/credential-groups/components/credential-group-detail.tsx +++ b/apps/sim/ee/credential-groups/components/credential-group-detail.tsx @@ -28,6 +28,7 @@ import { } from '@/app/workspace/[workspaceId]/settings/components/settings-resource-row' import { SettingsSection } from '@/app/workspace/[workspaceId]/settings/components/settings-section/settings-section' import { useSettingsUnsavedGuard } from '@/app/workspace/[workspaceId]/settings/hooks/use-settings-unsaved-guard' +import { CredentialGroupAccess } from '@/ee/credential-groups/components/credential-group-access' import { CredentialGroupDetails } from '@/ee/credential-groups/components/credential-group-details' import { CredentialGroupInviteModal } from '@/ee/credential-groups/components/credential-group-invite-modal' import { @@ -45,11 +46,12 @@ interface CredentialGroupDetailProps { onBack: () => void } -type CredentialGroupTab = 'details' | 'people' +type CredentialGroupTab = 'details' | 'people' | 'access' const CREDENTIAL_GROUP_TABS = [ { value: 'details', label: 'Details' }, { value: 'people', label: 'People' }, + { value: 'access', label: 'Access' }, ] as const interface EnrollmentConnectionsProps { @@ -165,15 +167,17 @@ export function CredentialGroupDetail({ saveDisabled: !name.trim(), saveTooltip: name.trim() ? undefined : 'Name is required', }) - : [ - { - text: 'Invite users', - icon: Plus, - variant: 'primary' as const, - onSelect: () => setShowInvite(true), - disabled: credentialGroup.status !== 'active' || !configurationReady, - }, - ]), + : activeTab === 'people' + ? [ + { + text: 'Invite users', + icon: Plus, + variant: 'primary' as const, + onSelect: () => setShowInvite(true), + disabled: credentialGroup.status !== 'active' || !configurationReady, + }, + ] + : []), { id: 'delete', text: deleteGroup.isPending ? 'Deleting...' : 'Delete', @@ -306,6 +310,10 @@ export function CredentialGroupDetail({ )} )} + + {activeTab === 'access' && ( + + )} )} diff --git a/apps/sim/executor/handlers/workflow/workflow-handler.test.ts b/apps/sim/executor/handlers/workflow/workflow-handler.test.ts index 229f19914cf..d9dc6fb4fa1 100644 --- a/apps/sim/executor/handlers/workflow/workflow-handler.test.ts +++ b/apps/sim/executor/handlers/workflow/workflow-handler.test.ts @@ -407,7 +407,13 @@ describe('WorkflowBlockHandler', () => { json: () => Promise.resolve({ data: { - deployedState: { blocks: {}, edges: [], loops: {}, parallels: {} }, + deployedState: { + blocks: {}, + edges: [], + loops: {}, + parallels: {}, + deploymentVersionId: 'deployment-version-1', + }, }, }), } @@ -599,7 +605,13 @@ describe('WorkflowBlockHandler', () => { json: () => Promise.resolve({ data: { - deployedState: { blocks: {}, edges: [], loops: {}, parallels: {} }, + deployedState: { + blocks: {}, + edges: [], + loops: {}, + parallels: {}, + deploymentVersionId: 'deployment-version-1', + }, }, }), } @@ -636,6 +648,11 @@ describe('WorkflowBlockHandler', () => { expect(executorOptions[0].contextExtensions.executorDelegationOrigin).toEqual({ workflowId: 'source-workflow-id', executionId: loggingSessionArgs[0][1], + currentWorkflow: { + workflowId: 'source-workflow-id', + mode: 'deployment', + deploymentVersionId: 'deployment-version-1', + }, principal: { kind: 'system', serviceId: 'internal', @@ -699,6 +716,7 @@ describe('WorkflowBlockHandler', () => { edges: [], loops: {}, parallels: {}, + deploymentVersionId: 'deployment-version-1', }, }, }), @@ -797,6 +815,7 @@ describe('WorkflowBlockHandler', () => { edges: [], loops: {}, parallels: {}, + deploymentVersionId: 'deployment-version-1', }, }, }), @@ -1199,7 +1218,15 @@ describe('WorkflowBlockHandler', () => { ok: true, json: () => Promise.resolve({ - data: { deployedState: { blocks: {}, edges: [], loops: {}, parallels: {} } }, + data: { + deployedState: { + blocks: {}, + edges: [], + loops: {}, + parallels: {}, + deploymentVersionId: 'deployment-version-1', + }, + }, }), } } @@ -1465,6 +1492,11 @@ describe('WorkflowBlockHandler', () => { expect(executorOptions[0].contextExtensions.executorDelegationOrigin).toEqual({ workflowId: 'source-workflow-id', executionId: executorOptions[0].contextExtensions.executionId, + currentWorkflow: { + workflowId: 'source-workflow-id', + mode: 'deployment', + deploymentVersionId: 'deployment-version-1', + }, principal: { kind: 'system', serviceId: 'internal', @@ -1791,6 +1823,7 @@ describe('WorkflowBlockHandler', () => { edges: [], loops: {}, parallels: {}, + deploymentVersionId: 'deployment-version-1', }, }, }), @@ -1844,14 +1877,18 @@ describe('WorkflowBlockHandler', () => { expect(extensions.executionId).toBe('parent-execution-id') expect(extensions.resolvedSecretTraceRegistry).toBe(registry) expect(extensions.executorDelegationOrigin).toEqual({ + subjectUserId: 'user-1', + workflowId: 'parent-workflow-id', + executionId: 'parent-execution-id', + currentWorkflow: { workflowId: 'child-workflow-id', mode: 'draft' }, + principal: { kind: 'session', userId: 'user-1', sessionId: 'session-1' }, + }) + expect(mockBuildExecutorDelegationHeaders).toHaveBeenCalledWith({ subjectUserId: 'user-1', workflowId: 'parent-workflow-id', executionId: 'parent-execution-id', principal: { kind: 'session', userId: 'user-1', sessionId: 'session-1' }, }) - expect(mockBuildExecutorDelegationHeaders).toHaveBeenCalledWith( - extensions.executorDelegationOrigin - ) expect(extensions.onStream).toBe(ctx.onStream) expect(extensions.childWorkflowContext).toBeDefined() }) @@ -1883,9 +1920,10 @@ describe('WorkflowBlockHandler', () => { await handler.execute(ctx, mockBlock, { workflowId: 'grandchild-workflow-id' }) expect(mockBuildExecutorDelegationHeaders).toHaveBeenCalledWith(ctx.executorDelegationOrigin) - expect(executorOptions[0].contextExtensions.executorDelegationOrigin).toBe( - ctx.executorDelegationOrigin - ) + expect(executorOptions[0].contextExtensions.executorDelegationOrigin).toEqual({ + ...ctx.executorDelegationOrigin, + currentWorkflow: { workflowId: 'grandchild-workflow-id', mode: 'draft' }, + }) }) }) diff --git a/apps/sim/executor/handlers/workflow/workflow-handler.ts b/apps/sim/executor/handlers/workflow/workflow-handler.ts index 2b969e3fd12..c7ab669fe01 100644 --- a/apps/sim/executor/handlers/workflow/workflow-handler.ts +++ b/apps/sim/executor/handlers/workflow/workflow-handler.ts @@ -340,6 +340,27 @@ export class WorkflowBlockHandler implements BlockHandler { throw new Error(`Child workflow ${workflowId} not found`) } + if (useDeployed && !childWorkflow.deploymentVersionId) { + throw new Error(`Deployed child workflow ${workflowId} has no deployment version`) + } + + const childWorkflowAuthority = useDeployed + ? { + workflowId, + mode: 'deployment' as const, + deploymentVersionId: childWorkflow.deploymentVersionId as string, + } + : { workflowId, mode: 'draft' as const } + if (!isCustomBlock) { + if (!childExecutorDelegationOrigin) { + throw new Error('Child workflow execution is missing its delegation origin') + } + childExecutorDelegationOrigin = { + ...childExecutorDelegationOrigin, + currentWorkflow: childWorkflowAuthority, + } + } + // Custom blocks are org-scoped and deliberately cross-workspace: the source // workflow lives in the publisher's workspace, not the consumer's. Their // boundary is the org overlay + `getCustomBlockAuthority`, so the @@ -548,6 +569,7 @@ export class WorkflowBlockHandler implements BlockHandler { workspaceId: sourceWorkspaceId, workflowId, }, + currentWorkflow: childWorkflowAuthority, } // The child no longer shares the parent's execution id, so it no longer // hears the parent's cancellation event — bridge it explicitly. @@ -1169,6 +1191,7 @@ export class WorkflowBlockHandler implements BlockHandler { return { name: workflowData.name, workspaceId: (workflowData.workspaceId ?? null) as string | null, + deploymentVersionId: undefined, serializedState: serializedWorkflow, variables: workflowVariables, workflowState: workflowStateWithVariables, @@ -1257,6 +1280,7 @@ export class WorkflowBlockHandler implements BlockHandler { return { name: childName, workspaceId: (wfData?.workspaceId ?? null) as string | null, + deploymentVersionId: deployedState.deploymentVersionId as string | undefined, serializedState: serializedWorkflow, variables: workflowVariables, workflowState: workflowStateWithVariables, diff --git a/apps/sim/executor/types.ts b/apps/sim/executor/types.ts index 51f5f20323b..07f8c28810a 100644 --- a/apps/sim/executor/types.ts +++ b/apps/sim/executor/types.ts @@ -1,4 +1,4 @@ -import type { WorkflowExecutionPrincipal } from '@sim/auth/principal' +import type { WorkflowExecutionAuthority, WorkflowExecutionPrincipal } from '@sim/auth/principal' import type { BillingAttributionSnapshot } from '@/lib/billing/core/billing-attribution' import type { TraceSpan } from '@/lib/logs/types' import type { PermissionGroupConfig } from '@/lib/permission-groups/types' @@ -361,6 +361,7 @@ export interface ExecutorDelegationOrigin { workflowId: string executionId?: string principal?: WorkflowExecutionPrincipal + currentWorkflow?: WorkflowExecutionAuthority } export interface ExecutionContext { diff --git a/apps/sim/hooks/queries/credential-groups.ts b/apps/sim/hooks/queries/credential-groups.ts index 6aa7af64f29..1fc1ceef296 100644 --- a/apps/sim/hooks/queries/credential-groups.ts +++ b/apps/sim/hooks/queries/credential-groups.ts @@ -7,10 +7,12 @@ import { createCredentialGroupContract, deleteCredentialGroupContract, deleteCredentialGroupEnrollmentContract, + getCredentialGroupAccessContract, getCredentialGroupContract, inviteCredentialGroupEnrollmentsContract, resendCredentialGroupEnrollmentContract, startSlackCredentialGroupConfigurationContract, + updateCredentialGroupAccessContract, updateCredentialGroupContract, } from '@/lib/api/contracts/credential-groups' import type { ContractJsonResponse } from '@/lib/api/contracts/types' @@ -56,6 +58,47 @@ export function useCredentialGroupDetail(workspaceId?: string, groupId?: string) }) } +export function useCredentialGroupAccess(workspaceId?: string, groupId?: string) { + return useQuery({ + queryKey: credentialGroupKeys.access(workspaceId, groupId), + queryFn: ({ signal }) => { + if (!workspaceId || !groupId) { + throw new Error('Credential Group access identifiers are required') + } + return requestJson(getCredentialGroupAccessContract, { + params: { id: workspaceId, groupId }, + signal, + }) + }, + enabled: Boolean(workspaceId && groupId), + staleTime: CREDENTIAL_GROUP_DETAIL_STALE_TIME, + retryOnMount: true, + }) +} + +export function useUpdateCredentialGroupAccess() { + const queryClient = useQueryClient() + return useMutation({ + mutationFn: async ({ + workspaceId, + groupId, + body, + }: { + workspaceId: string + groupId: string + body: ContractBodyInput + }) => + requestJson(updateCredentialGroupAccessContract, { + params: { id: workspaceId, groupId }, + body, + }), + onSettled: (_data, _error, variables) => + queryClient.invalidateQueries({ + queryKey: credentialGroupKeys.access(variables.workspaceId, variables.groupId), + }), + }) +} + export function useCreateCredentialGroup() { const queryClient = useQueryClient() return useMutation({ diff --git a/apps/sim/hooks/queries/utils/credential-group-queries.ts b/apps/sim/hooks/queries/utils/credential-group-queries.ts index 780b2f31f49..df2b07d00ae 100644 --- a/apps/sim/hooks/queries/utils/credential-group-queries.ts +++ b/apps/sim/hooks/queries/utils/credential-group-queries.ts @@ -12,6 +12,8 @@ export const credentialGroupKeys = { details: () => [...credentialGroupKeys.all, 'detail'] as const, detail: (workspaceId?: string, groupId?: string) => [...credentialGroupKeys.details(), workspaceId ?? '', groupId ?? ''] as const, + access: (workspaceId?: string, groupId?: string) => + [...credentialGroupKeys.detail(workspaceId, groupId), 'access'] as const, } export async function fetchCredentialGroupList( diff --git a/apps/sim/lib/api/contracts/credential-groups.test.ts b/apps/sim/lib/api/contracts/credential-groups.test.ts index edbea439b0f..4d94979e7df 100644 --- a/apps/sim/lib/api/contracts/credential-groups.test.ts +++ b/apps/sim/lib/api/contracts/credential-groups.test.ts @@ -5,6 +5,7 @@ import { credentialGroupEnrollmentListQuerySchema, credentialGroupSchema, inviteCredentialGroupEnrollmentsBodySchema, + updateCredentialGroupAccessBodySchema, updateCredentialGroupBodySchema, } from '@/lib/api/contracts/credential-groups' @@ -196,4 +197,38 @@ describe('credential group contracts', () => { expect(result.connections).toEqual([{ provider: 'gmail', status: 'active', count: 2 }]) }) + + it('accepts managed access subjects without exposing policy actions', () => { + const result = updateCredentialGroupAccessBodySchema.parse({ + expectedRevision: 3, + grants: [ + { subject: { type: 'workflow', workflowId: 'workflow-1' } }, + { subject: { type: 'workspace_role', minimumRole: 'admin' } }, + { + subject: { + type: 'access_control_group', + accessControlGroupId: 'permission-group-1', + }, + }, + ], + }) + + expect(result.grants).toHaveLength(3) + expect(result.grants[0]).not.toHaveProperty('actions') + }) + + it('rejects caller-supplied effects and actions', () => { + expect( + updateCredentialGroupAccessBodySchema.safeParse({ + expectedRevision: 0, + grants: [ + { + subject: { type: 'workflow', workflowId: 'workflow-1' }, + effect: 'deny', + actions: ['credential_groups.credentials.use'], + }, + ], + }).success + ).toBe(false) + }) }) diff --git a/apps/sim/lib/api/contracts/credential-groups.ts b/apps/sim/lib/api/contracts/credential-groups.ts index fc9bf7dc522..ba0ed60a9b8 100644 --- a/apps/sim/lib/api/contracts/credential-groups.ts +++ b/apps/sim/lib/api/contracts/credential-groups.ts @@ -5,6 +5,7 @@ import { CREDENTIAL_GROUP_PROVIDER_IDS, CREDENTIAL_GROUP_STANDARD_OAUTH_PROVIDER_IDS, } from '@/lib/credential-groups/providers' +import { resourcePolicySubjectSchema } from '@/lib/resource-policies/types' export const credentialGroupProviderSchema = z.enum(CREDENTIAL_GROUP_PROVIDER_IDS) export const credentialGroupStatusSchema = z.enum(['active', 'disabled']) @@ -115,6 +116,42 @@ export type CredentialGroupEnrollmentConnection = z.output< > export type CredentialGroupEnrollmentDetail = z.output +export const credentialGroupAccessGrantSchema = z + .object({ + id: z.string().min(1).max(128), + subject: resourcePolicySubjectSchema, + }) + .strict() + +export type CredentialGroupAccessGrant = z.output + +export const credentialGroupAccessPolicySchema = z + .object({ + revision: z.number().int().nonnegative(), + grants: z.array(credentialGroupAccessGrantSchema).max(100), + }) + .strict() + +export type CredentialGroupAccessPolicy = z.output + +export const updateCredentialGroupAccessBodySchema = z + .object({ + expectedRevision: z.number().int().nonnegative(), + grants: z + .array( + z + .object({ + id: z.string().min(1).max(128).optional(), + subject: resourcePolicySubjectSchema, + }) + .strict() + ) + .max(100), + }) + .strict() + +export type UpdateCredentialGroupAccessBody = z.input + export const credentialGroupWorkspaceParamsSchema = z.object({ id: workspaceIdSchema, }) @@ -382,6 +419,21 @@ export const updateCredentialGroupContract = defineRouteContract({ }, }) +export const getCredentialGroupAccessContract = defineRouteContract({ + method: 'GET', + path: '/api/workspaces/[id]/credential-groups/[groupId]/access', + params: credentialGroupDetailParamsSchema, + response: { mode: 'json', schema: credentialGroupAccessPolicySchema }, +}) + +export const updateCredentialGroupAccessContract = defineRouteContract({ + method: 'PUT', + path: '/api/workspaces/[id]/credential-groups/[groupId]/access', + params: credentialGroupDetailParamsSchema, + body: updateCredentialGroupAccessBodySchema, + response: { mode: 'json', schema: credentialGroupAccessPolicySchema }, +}) + export const startSlackCredentialGroupConfigurationContract = defineRouteContract({ method: 'POST', path: '/api/workspaces/[id]/credential-groups/[groupId]/slack-managed-users', diff --git a/apps/sim/lib/auth/internal-delegation.test.ts b/apps/sim/lib/auth/internal-delegation.test.ts index a041ec712a8..ee752ea03de 100644 --- a/apps/sim/lib/auth/internal-delegation.test.ts +++ b/apps/sim/lib/auth/internal-delegation.test.ts @@ -3,9 +3,10 @@ */ import { beforeEach, describe, expect, it, vi } from 'vitest' -const { mockResolveWorkflow, mockResolveRun } = vi.hoisted(() => ({ +const { mockResolveWorkflow, mockResolveRun, mockLoadDeployedWorkflowState } = vi.hoisted(() => ({ mockResolveWorkflow: vi.fn(), mockResolveRun: vi.fn(), + mockLoadDeployedWorkflowState: vi.fn(), })) vi.mock('@/lib/workflows/application/context', () => ({ @@ -13,6 +14,11 @@ vi.mock('@/lib/workflows/application/context', () => ({ resolveActiveWorkflowRunApplicationContext: mockResolveRun, })) +vi.mock('@/lib/workflows/persistence/utils', () => ({ + loadDeployedWorkflowState: mockLoadDeployedWorkflowState, + NoActiveDeploymentError: class NoActiveDeploymentError extends Error {}, +})) + import { bindInternalExecutorDelegation, InvalidInternalDelegationBindingError, @@ -40,6 +46,9 @@ describe('bindInternalExecutorDelegation', () => { workspaceId: 'workspace-1', runId: 'execution-1', }) + mockLoadDeployedWorkflowState.mockResolvedValue({ + deploymentVersionId: 'deployment-version-1', + }) }) it('derives workspace authority from the canonical workflow', async () => { @@ -86,6 +95,80 @@ describe('bindInternalExecutorDelegation', () => { }) }) + it('binds deployed child authority to the active deployment version', async () => { + const currentWorkflow = { + workflowId: 'child-workflow', + mode: 'deployment' as const, + deploymentVersionId: 'deployment-version-1', + } + + const principal = await bindInternalExecutorDelegation( + { ...claims, currentWorkflow }, + { audience: 'sim:credential-groups' } + ) + + expect(mockResolveWorkflow).toHaveBeenNthCalledWith(1, { workflowId: 'workflow-1' }) + expect(mockResolveWorkflow).toHaveBeenNthCalledWith(2, { workflowId: 'child-workflow' }) + expect(mockLoadDeployedWorkflowState).toHaveBeenCalledWith('child-workflow') + expect(principal.delegationContext.currentWorkflow).toEqual(currentWorkflow) + }) + + it('rejects stale deployed child authority', async () => { + mockLoadDeployedWorkflowState.mockResolvedValue({ + deploymentVersionId: 'deployment-version-2', + }) + + await expect( + bindInternalExecutorDelegation( + { + ...claims, + currentWorkflow: { + workflowId: 'child-workflow', + mode: 'deployment', + deploymentVersionId: 'deployment-version-1', + }, + }, + { audience: 'sim:credential-groups' } + ) + ).rejects.toBeInstanceOf(InvalidInternalDelegationBindingError) + }) + + it('rejects current workflow authority from another workspace', async () => { + mockResolveWorkflow + .mockResolvedValueOnce({ workflowId: 'workflow-1', workspaceId: 'workspace-1' }) + .mockResolvedValueOnce({ workflowId: 'child-workflow', workspaceId: 'workspace-2' }) + + await expect( + bindInternalExecutorDelegation( + { + ...claims, + currentWorkflow: { workflowId: 'child-workflow', mode: 'draft' }, + }, + { audience: 'sim:credential-groups' } + ) + ).rejects.toBeInstanceOf(InvalidInternalDelegationBindingError) + expect(mockLoadDeployedWorkflowState).not.toHaveBeenCalled() + }) + + it('does not disguise current-workflow infrastructure failures as invalid credentials', async () => { + const infrastructureError = new Error('deployment database unavailable') + mockLoadDeployedWorkflowState.mockRejectedValue(infrastructureError) + + await expect( + bindInternalExecutorDelegation( + { + ...claims, + currentWorkflow: { + workflowId: 'child-workflow', + mode: 'deployment', + deploymentVersionId: 'deployment-version-1', + }, + }, + { audience: 'sim:credential-groups' } + ) + ).rejects.toBe(infrastructureError) + }) + it('fails before canonical loading when the domain audience is missing', async () => { await expect(bindInternalExecutorDelegation(claims, { audience: ' ' })).rejects.toThrow( 'Internal delegation audience must not be empty' diff --git a/apps/sim/lib/auth/internal-delegation.ts b/apps/sim/lib/auth/internal-delegation.ts index b67ac79f2b4..cf25639de57 100644 --- a/apps/sim/lib/auth/internal-delegation.ts +++ b/apps/sim/lib/auth/internal-delegation.ts @@ -8,6 +8,10 @@ import { resolveActiveWorkflowApplicationContext, resolveActiveWorkflowRunApplicationContext, } from '@/lib/workflows/application/context' +import { + loadDeployedWorkflowState, + NoActiveDeploymentError, +} from '@/lib/workflows/persistence/utils' export interface BindInternalExecutorDelegationOptions { audience: string @@ -43,6 +47,36 @@ export async function bindInternalExecutorDelegation( throw error } + if (claims.currentWorkflow) { + let currentContext: Awaited> + try { + currentContext = await resolveActiveWorkflowApplicationContext({ + workflowId: claims.currentWorkflow.workflowId, + }) + } catch (error) { + if (asOrchestrationError(error)?.code === 'not_found') { + throw new InvalidInternalDelegationBindingError() + } + throw error + } + if (currentContext.workspaceId !== context.workspaceId) { + throw new InvalidInternalDelegationBindingError() + } + if (claims.currentWorkflow.mode === 'deployment') { + try { + const deployed = await loadDeployedWorkflowState(claims.currentWorkflow.workflowId) + if (deployed.deploymentVersionId !== claims.currentWorkflow.deploymentVersionId) { + throw new InvalidInternalDelegationBindingError() + } + } catch (error) { + if (error instanceof NoActiveDeploymentError) { + throw new InvalidInternalDelegationBindingError() + } + throw error + } + } + } + return { kind: 'delegated', serviceId: 'executor', @@ -58,6 +92,7 @@ export async function bindInternalExecutorDelegation( workflowId: context.workflowId, ...(claims.executionId ? { executionId: claims.executionId } : {}), ...(claims.principal ? { principal: claims.principal } : {}), + ...(claims.currentWorkflow ? { currentWorkflow: claims.currentWorkflow } : {}), }, } } diff --git a/apps/sim/lib/auth/internal.test.ts b/apps/sim/lib/auth/internal.test.ts index 9fcf4b7830b..30ed94f8842 100644 --- a/apps/sim/lib/auth/internal.test.ts +++ b/apps/sim/lib/auth/internal.test.ts @@ -135,6 +135,42 @@ describe('internal executor delegation claims', () => { }) }) + it('round-trips the currently executing deployed workflow authority', async () => { + const token = await generateInternalDelegationToken({ + subjectUserId: 'user-1', + workflowId: 'root-workflow', + executionId: 'execution-1', + currentWorkflow: { + workflowId: 'child-workflow', + mode: 'deployment', + deploymentVersionId: 'deployment-version-1', + }, + }) + + await expect(verifyInternalDelegationToken(token)).resolves.toMatchObject({ + workflowId: 'root-workflow', + currentWorkflow: { + workflowId: 'child-workflow', + mode: 'deployment', + deploymentVersionId: 'deployment-version-1', + }, + }) + }) + + it('rejects malformed workflow authority instead of dropping its fields', async () => { + await expect( + generateInternalDelegationToken({ + subjectUserId: 'user-1', + workflowId: 'root-workflow', + currentWorkflow: { + workflowId: 'child-workflow', + mode: 'draft', + unexpected: true, + } as never, + }) + ).rejects.toBeInstanceOf(InvalidInternalDelegationTokenError) + }) + it('rejects laundering actorless or external principals into a Sim user subject', async () => { await expect( generateInternalDelegationToken({ diff --git a/apps/sim/lib/auth/internal.ts b/apps/sim/lib/auth/internal.ts index 3707e04348e..c1568fd80f1 100644 --- a/apps/sim/lib/auth/internal.ts +++ b/apps/sim/lib/auth/internal.ts @@ -2,6 +2,7 @@ import { parsePrincipal, resolvePrincipalSubject, serializePrincipal, + type WorkflowExecutionAuthority, type WorkflowExecutionPrincipal, } from '@sim/auth/principal' import { createLogger } from '@sim/logger' @@ -26,6 +27,7 @@ export interface GenerateInternalDelegationTokenInput { workflowId: string executionId?: string principal?: WorkflowExecutionPrincipal + currentWorkflow?: WorkflowExecutionAuthority } export interface VerifiedInternalDelegation { @@ -34,6 +36,7 @@ export interface VerifiedInternalDelegation { workflowId: string executionId?: string principal?: WorkflowExecutionPrincipal + currentWorkflow?: WorkflowExecutionAuthority delegationId: string issuedAt: Date expiresAt: Date @@ -99,6 +102,34 @@ function requireNonEmptyDelegationClaim(value: string, name: string): string { return value } +function parseWorkflowExecutionAuthority(value: unknown): WorkflowExecutionAuthority { + if (!value || typeof value !== 'object' || Array.isArray(value)) { + throw new InvalidInternalDelegationTokenError() + } + const authority = value as Record + const workflowId = readVerifiedDelegationClaim(authority.workflowId) + if (!workflowId) throw new InvalidInternalDelegationTokenError() + if (authority.mode === 'draft') { + if (Object.keys(authority).some((key) => !['workflowId', 'mode'].includes(key))) { + throw new InvalidInternalDelegationTokenError() + } + return { workflowId, mode: 'draft' } + } + if (authority.mode === 'deployment') { + const deploymentVersionId = readVerifiedDelegationClaim(authority.deploymentVersionId) + if ( + !deploymentVersionId || + Object.keys(authority).some( + (key) => !['workflowId', 'mode', 'deploymentVersionId'].includes(key) + ) + ) { + throw new InvalidInternalDelegationTokenError() + } + return { workflowId, mode: 'deployment', deploymentVersionId } + } + throw new InvalidInternalDelegationTokenError() +} + /** Generates an executor token bound to its workflow origin and authenticated caller. */ export async function generateInternalDelegationToken( input: GenerateInternalDelegationTokenInput @@ -126,6 +157,9 @@ export async function generateInternalDelegationToken( throw new Error('Internal delegation requires a workflow principal or Sim user subject') } const workflowId = requireNonEmptyDelegationClaim(input.workflowId, 'workflowId') + const currentWorkflow = input.currentWorkflow + ? parseWorkflowExecutionAuthority(input.currentWorkflow) + : undefined const issuedAtSeconds = Math.floor(Date.now() / 1000) const executionId = input.executionId ? requireNonEmptyDelegationClaim(input.executionId, 'executionId') @@ -136,6 +170,7 @@ export async function generateInternalDelegationToken( serviceId: 'executor', workflowId, ...(input.principal ? { principal: serializePrincipal(input.principal) } : {}), + ...(currentWorkflow ? { currentWorkflow } : {}), ...(executionId ? { executionId } : {}), }) .setProtectedHeader({ alg: 'HS256' }) @@ -177,6 +212,7 @@ export async function verifyInternalDelegationToken( const delegationId = readVerifiedDelegationClaim(payload.jti) const nowSeconds = Math.floor(Date.now() / 1000) let principal: WorkflowExecutionPrincipal | undefined + let currentWorkflow: WorkflowExecutionAuthority | undefined if (payload.principal !== undefined) { try { principal = parsePrincipal(payload.principal) @@ -184,6 +220,9 @@ export async function verifyInternalDelegationToken( throw new InvalidInternalDelegationTokenError() } } + if (payload.currentWorkflow !== undefined) { + currentWorkflow = parseWorkflowExecutionAuthority(payload.currentWorkflow) + } if ( payload.type !== 'internal_delegation' || @@ -215,6 +254,7 @@ export async function verifyInternalDelegationToken( ...(subjectUserId ? { subjectUserId } : {}), workflowId, ...(principal ? { principal } : {}), + ...(currentWorkflow ? { currentWorkflow } : {}), ...(executionId ? { executionId } : {}), delegationId, issuedAt: new Date(payload.iat * 1000), diff --git a/apps/sim/lib/credential-groups/application/authorization.ts b/apps/sim/lib/credential-groups/application/authorization.ts index 553f522aeed..8420db5b9dc 100644 --- a/apps/sim/lib/credential-groups/application/authorization.ts +++ b/apps/sim/lib/credential-groups/application/authorization.ts @@ -1,6 +1,5 @@ import { type Principal, - requirePrincipalSubjectUserId, resolvePrincipalSubject, type WorkflowExecutionDelegatedPrincipal, } from '@sim/auth/principal' @@ -14,15 +13,25 @@ import { type CredentialGroupEnrollmentAccess, loadCredentialGroupEnrollmentAccessForSubject, } from '@/lib/credential-groups/credentials' +import { findResourcePolicyGrant } from '@/lib/resource-policies/authorization' +import type { ResourcePolicyAction } from '@/lib/resource-policies/types' export const CREDENTIAL_GROUP_DELEGATION_AUDIENCE = 'sim:credential-groups' +export interface CredentialGroupAuthorizationContext extends WorkspaceAuthorizationContext { + credentialGroupId: string +} + export interface CredentialGroupApplicationContext - extends WorkspaceAuthorizationContext, + extends CredentialGroupAuthorizationContext, CredentialGroupCredentialListContext { - enrollmentAccess?: CredentialGroupEnrollmentAccess + credentialAccess?: CredentialGroupCredentialAccess } +export type CredentialGroupCredentialAccess = + | ({ scope: 'enrollment' } & CredentialGroupEnrollmentAccess) + | { scope: 'all'; grantId: string } + function requireWorkflowExecutionPrincipal(principal: Principal) { if (principal.kind !== 'delegated' || principal.serviceId !== 'executor') { throw new Error('Credential Group use requires an executor delegation') @@ -36,39 +45,50 @@ function requireWorkflowExecutionPrincipal(principal: Principal) { } export function requireCredentialGroupWorkflowSubject(principal: Principal): string { - const executionPrincipal = requireWorkflowExecutionPrincipal(principal) - let subjectUserId: string - try { - subjectUserId = requirePrincipalSubjectUserId(executionPrincipal) - } catch { - throw new OrchestrationError('forbidden', 'Credential Group user access required') - } - if (principal.kind !== 'delegated' || principal.subjectUserId !== subjectUserId) { + const subject = resolvePrincipalSubject(requireWorkflowExecutionPrincipal(principal)) + if ( + subject?.kind !== 'sim_user' || + principal.kind !== 'delegated' || + principal.subjectUserId !== subject.userId + ) { throw new OrchestrationError('forbidden', 'Credential Group user access required') } - return subjectUserId + return subject.userId } -export async function requireCredentialGroupEnrollmentAccess( +export async function requireCredentialGroupCredentialAccess( principal: Principal, - credentialGroupId: string -): Promise { + context: CredentialGroupAuthorizationContext, + action: ResourcePolicyAction +): Promise { + const grant = await findResourcePolicyGrant({ + principal, + context, + resourceType: 'credential_group', + resourceId: context.credentialGroupId, + action, + }) + if (grant) return { scope: 'all', grantId: grant.id } + const executionPrincipal = requireWorkflowExecutionPrincipal(principal) const subject = resolvePrincipalSubject(executionPrincipal) if (!subject) { - throw new OrchestrationError('forbidden', 'Credential Group enrollment access required') + throw new OrchestrationError('forbidden', 'Credential Group actor access required') } if ( subject.kind === 'sim_user' && (principal.kind !== 'delegated' || principal.subjectUserId !== subject.userId) ) { - throw new OrchestrationError('forbidden', 'Credential Group enrollment access required') + throw new OrchestrationError('forbidden', 'Credential Group actor access required') } - const access = await loadCredentialGroupEnrollmentAccessForSubject(credentialGroupId, subject) + const access = await loadCredentialGroupEnrollmentAccessForSubject( + context.credentialGroupId, + subject + ) if (!access) { throw new OrchestrationError('forbidden', 'Credential Group enrollment access required') } - return access + return { scope: 'enrollment', ...access } } export const credentialGroupDelegationPolicy = { 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 32b62578b11..31a1dd200e5 100644 --- a/apps/sim/lib/credential-groups/application/list-credentials.test.ts +++ b/apps/sim/lib/credential-groups/application/list-credentials.test.ts @@ -6,6 +6,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' const mocks = vi.hoisted(() => ({ getWorkspaceOwnerSubscriptionAccess: vi.fn(), + findPolicyGrant: vi.fn(), listCredentials: vi.fn(), loadEnrollmentAccess: vi.fn(), loadGroup: vi.fn(), @@ -35,6 +36,10 @@ vi.mock('@/lib/credential-groups/credentials', () => ({ MAX_CREDENTIAL_GROUP_CREDENTIAL_PAGE_SIZE: 100, })) +vi.mock('@/lib/resource-policies/authorization', () => ({ + findResourcePolicyGrant: mocks.findPolicyGrant, +})) + vi.mock('@/lib/workspaces/application/workspace-context', () => ({ loadActiveWorkspaceApplicationContext: mocks.loadWorkspace, })) @@ -111,6 +116,7 @@ describe('listCredentialGroupCredentials', () => { mocks.resolvePermission.mockResolvedValue('read') mocks.getWorkspaceOwnerSubscriptionAccess.mockResolvedValue({ isEnterprise: true }) mocks.resolveCredentialGroupsAvailability.mockResolvedValue({ available: true }) + mocks.findPolicyGrant.mockResolvedValue(null) mocks.loadEnrollmentAccess.mockResolvedValue({ enrollmentId: 'enrollment-1', email: 'person@example.com', @@ -224,6 +230,21 @@ describe('listCredentialGroupCredentials', () => { }) }) + it('lists the entire group for a matching resource-policy grant', async () => { + mocks.findPolicyGrant.mockResolvedValueOnce({ + id: 'grant-1', + subject: { type: 'workflow', workflowId: 'workflow-1' }, + actions: ['credential_groups.credentials.list', 'credential_groups.credentials.use'], + }) + + await listCredentialGroupCredentials.execute({ principal: executorPrincipal(), input }) + + expect(mocks.loadEnrollmentAccess).not.toHaveBeenCalled() + expect(mocks.listCredentials).toHaveBeenCalledWith( + expect.objectContaining({ credentialGroupEnrollmentId: undefined }) + ) + }) + it('filters by canonical providers active in the group', async () => { await listCredentialGroupCredentials.execute({ principal: executorPrincipal(), diff --git a/apps/sim/lib/credential-groups/application/manage-access.test.ts b/apps/sim/lib/credential-groups/application/manage-access.test.ts new file mode 100644 index 00000000000..ff1cf735dd8 --- /dev/null +++ b/apps/sim/lib/credential-groups/application/manage-access.test.ts @@ -0,0 +1,166 @@ +/** + * @vitest-environment node + */ +import type { SessionPrincipal } from '@sim/auth/principal' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const mocks = vi.hoisted(() => ({ + loadPolicy: vi.fn(), + resolveGroup: vi.fn(), + resolvePermission: vi.fn(), + validateSubjects: vi.fn(), + writePolicy: vi.fn(), +})) + +vi.mock('@/lib/credential-groups/application/context', () => ({ + resolveCredentialGroupSettingsContext: mocks.resolveGroup, +})) + +vi.mock('@sim/platform-authz/workspace', () => ({ + permissionSatisfies: (permission: string | null, required: string) => + permission === 'admin' || permission === required, + resolveEffectiveWorkspacePermission: mocks.resolvePermission, +})) + +vi.mock('@/lib/resource-policies/management', () => ({ + validateResourcePolicySubjects: mocks.validateSubjects, +})) + +vi.mock('@/lib/resource-policies/repository', () => { + class ResourcePolicyRevisionConflictError extends Error {} + return { + loadResourcePolicy: mocks.loadPolicy, + ResourcePolicyRevisionConflictError, + writeResourcePolicy: mocks.writePolicy, + } +}) + +import { + readCredentialGroupAccess, + updateCredentialGroupAccess, +} from '@/lib/credential-groups/application/manage-access' +import { ResourcePolicyRevisionConflictError } from '@/lib/resource-policies/repository' + +const context = { + workspaceId: 'workspace-1', + workspaceOrganizationId: 'organization-1', + allowPersonalApiKeys: true, + billedAccountUserId: 'billing-owner-1', + credentialGroupId: 'group-1', + name: 'Support', + status: 'active' as const, + options: [], +} +const principal: SessionPrincipal = { + kind: 'session', + userId: 'admin-1', + sessionId: 'session-1', +} +const target = { + assertedWorkspaceId: 'workspace-1', + credentialGroupId: 'group-1', +} + +describe('Credential Group access policy operations', () => { + beforeEach(() => { + vi.clearAllMocks() + mocks.resolveGroup.mockResolvedValue(context) + mocks.resolvePermission.mockResolvedValue('admin') + mocks.loadPolicy.mockResolvedValue(null) + mocks.validateSubjects.mockResolvedValue(undefined) + mocks.writePolicy.mockImplementation(async ({ document }) => ({ + id: 'policy-1', + workspaceId: 'workspace-1', + revision: 2, + document, + createdAt: new Date('2026-08-20T00:00:00.000Z'), + updatedAt: new Date('2026-08-20T00:00:00.000Z'), + })) + }) + + it('returns no visible grants when only the built-in self rule applies', async () => { + await expect(readCredentialGroupAccess.execute({ principal, input: target })).resolves.toEqual({ + revision: 0, + grants: [], + }) + }) + + it('fails fast when stored policy workspace binding is corrupt', async () => { + mocks.loadPolicy.mockResolvedValue({ + id: 'policy-1', + workspaceId: 'workspace-2', + revision: 1, + document: { + version: 1, + resource: { type: 'credential_group', id: 'group-1' }, + grants: [], + }, + createdAt: new Date('2026-08-20T00:00:00.000Z'), + updatedAt: new Date('2026-08-20T00:00:00.000Z'), + }) + + await expect(readCredentialGroupAccess.execute({ principal, input: target })).rejects.toThrow( + 'wrong workspace binding' + ) + }) + + it('requires current workspace-admin permission', async () => { + mocks.resolvePermission.mockResolvedValue('write') + + await expect( + updateCredentialGroupAccess.execute({ + principal, + input: { ...target, expectedRevision: 0, grants: [] }, + }) + ).rejects.toMatchObject({ code: 'forbidden' }) + expect(mocks.writePolicy).not.toHaveBeenCalled() + }) + + it('stores one managed grant as list-and-use access to the whole group', async () => { + const subject = { type: 'workflow' as const, workflowId: 'workflow-1' } + + const result = await updateCredentialGroupAccess.execute({ + principal, + input: { + ...target, + expectedRevision: 1, + grants: [{ subject }], + }, + }) + + expect(mocks.validateSubjects).toHaveBeenCalledWith([subject], context) + expect(mocks.writePolicy).toHaveBeenCalledWith({ + workspaceId: 'workspace-1', + resourceType: 'credential_group', + resourceId: 'group-1', + expectedRevision: 1, + actorUserId: 'admin-1', + document: { + version: 1, + resource: { type: 'credential_group', id: 'group-1' }, + grants: [ + { + id: expect.any(String), + subject, + actions: ['credential_groups.credentials.list', 'credential_groups.credentials.use'], + }, + ], + }, + }) + expect(result).toEqual({ + revision: 2, + grants: [{ id: expect.any(String), subject }], + }) + }) + + it('maps optimistic-write conflicts to an application conflict', async () => { + mocks.writePolicy.mockRejectedValue(new ResourcePolicyRevisionConflictError()) + + await expect( + updateCredentialGroupAccess.execute({ + principal, + input: { ...target, expectedRevision: 1, grants: [] }, + }) + ).rejects.toMatchObject({ code: 'conflict' }) + }) +}) diff --git a/apps/sim/lib/credential-groups/application/manage-access.ts b/apps/sim/lib/credential-groups/application/manage-access.ts new file mode 100644 index 00000000000..a6143efb166 --- /dev/null +++ b/apps/sim/lib/credential-groups/application/manage-access.ts @@ -0,0 +1,117 @@ +import { AuditAction, AuditResourceType } from '@sim/audit' +import { generateId } from '@sim/utils/id' +import { defineAuthorizedWorkspaceUseCase } from '@/lib/core/application' +import { OrchestrationError } from '@/lib/core/orchestration/types' +import { resolveCredentialGroupSettingsContext } from '@/lib/credential-groups/application/context' +import { credentialGroupOperations } from '@/lib/credential-groups/application/operations' +import { validateResourcePolicySubjects } from '@/lib/resource-policies/management' +import { + loadResourcePolicy, + ResourcePolicyRevisionConflictError, + writeResourcePolicy, +} from '@/lib/resource-policies/repository' +import { + RESOURCE_POLICY_VERSION, + type ResourcePolicySubject, + resourcePolicySubjectSchema, +} from '@/lib/resource-policies/types' + +const CREDENTIAL_GROUP_ALL_ACTIONS = [ + 'credential_groups.credentials.list', + 'credential_groups.credentials.use', +] as const + +interface CredentialGroupAccessTargetInput { + assertedWorkspaceId: string + credentialGroupId: string +} + +function presentPolicy( + policy: Awaited>, + expected: { credentialGroupId: string; workspaceId: string } +) { + if (!policy) { + return { revision: 0, grants: [] } + } + if (policy.workspaceId !== expected.workspaceId) { + throw new Error('Credential Group policy has the wrong workspace binding') + } + if (policy.document.resource.id !== expected.credentialGroupId) { + throw new Error('Credential Group policy has the wrong resource binding') + } + return { + revision: policy.revision, + grants: policy.document.grants.map((grant) => ({ id: grant.id, subject: grant.subject })), + } +} + +export const readCredentialGroupAccess = defineAuthorizedWorkspaceUseCase({ + operation: credentialGroupOperations.readAccess, + resolveContext: ({ input }: { input: CredentialGroupAccessTargetInput }) => + resolveCredentialGroupSettingsContext(input.credentialGroupId, input.assertedWorkspaceId), + authorizationOptions: {}, + async execute({ context }) { + const policy = await loadResourcePolicy({ + resourceType: 'credential_group', + resourceId: context.credentialGroupId, + }) + return presentPolicy(policy, { + credentialGroupId: context.credentialGroupId, + workspaceId: context.workspaceId, + }) + }, +}) + +export interface UpdateCredentialGroupAccessInput extends CredentialGroupAccessTargetInput { + expectedRevision: number + grants: Array<{ id?: string; subject: ResourcePolicySubject }> +} + +export const updateCredentialGroupAccess = defineAuthorizedWorkspaceUseCase({ + operation: credentialGroupOperations.updateAccess, + resolveContext: ({ input }: { input: UpdateCredentialGroupAccessInput }) => + resolveCredentialGroupSettingsContext(input.credentialGroupId, input.assertedWorkspaceId), + authorizationOptions: {}, + async execute({ principal, input, context }) { + if (!Number.isInteger(input.expectedRevision) || input.expectedRevision < 0) { + throw new OrchestrationError('validation', 'Expected policy revision must be non-negative') + } + const subjects = input.grants.map((grant) => resourcePolicySubjectSchema.parse(grant.subject)) + await validateResourcePolicySubjects(subjects, context) + try { + const policy = await writeResourcePolicy({ + workspaceId: context.workspaceId, + resourceType: 'credential_group', + resourceId: context.credentialGroupId, + expectedRevision: input.expectedRevision, + actorUserId: principal.userId, + document: { + version: RESOURCE_POLICY_VERSION, + resource: { type: 'credential_group', id: context.credentialGroupId }, + grants: input.grants.map((grant, index) => ({ + id: grant.id ?? generateId(), + subject: subjects[index], + actions: [...CREDENTIAL_GROUP_ALL_ACTIONS], + })), + }, + }) + return presentPolicy(policy, { + credentialGroupId: context.credentialGroupId, + workspaceId: context.workspaceId, + }) + } catch (error) { + if (error instanceof ResourcePolicyRevisionConflictError) { + throw new OrchestrationError('conflict', error.message) + } + throw error + } + }, + projectAudit: ({ result, context }) => ({ + action: AuditAction.CREDENTIAL_GROUP_UPDATED, + resourceType: AuditResourceType.CREDENTIAL_GROUP, + resourceId: context.credentialGroupId, + resourceName: context.name, + description: 'Updated Credential Group access grants', + metadata: { revision: result.revision, grantCount: result.grants.length }, + }), +}) diff --git a/apps/sim/lib/credential-groups/application/operations.ts b/apps/sim/lib/credential-groups/application/operations.ts index fdd1b524246..35b28cdda73 100644 --- a/apps/sim/lib/credential-groups/application/operations.ts +++ b/apps/sim/lib/credential-groups/application/operations.ts @@ -25,6 +25,18 @@ export const credentialGroupOperations = { workspaceApiKey: 'deny', principalKinds: ['session'], }), + readAccess: defineWorkspaceOperation({ + id: 'credential_groups.access.read', + minimumRole: 'admin', + workspaceApiKey: 'deny', + principalKinds: ['session'], + }), + updateAccess: defineWorkspaceOperation({ + id: 'credential_groups.access.update', + minimumRole: 'admin', + workspaceApiKey: 'deny', + principalKinds: ['session'], + }), delete: defineWorkspaceOperation({ id: 'credential_groups.delete', minimumRole: 'admin', diff --git a/apps/sim/lib/credentials/application/resolve-managed-oauth-token.test.ts b/apps/sim/lib/credentials/application/resolve-managed-oauth-token.test.ts index 7828cb946e1..debaea9c3a4 100644 --- a/apps/sim/lib/credentials/application/resolve-managed-oauth-token.test.ts +++ b/apps/sim/lib/credentials/application/resolve-managed-oauth-token.test.ts @@ -6,7 +6,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' const mocks = vi.hoisted(() => ({ loadContext: vi.fn(), - requireEnrollmentAccess: vi.fn(), + requireCredentialAccess: vi.fn(), resolvePermission: vi.fn(), resolveToken: vi.fn(), recordAudit: vi.fn(), @@ -18,7 +18,7 @@ vi.mock('@/lib/credentials/managed-oauth', () => ({ })) vi.mock('@/lib/credential-groups/application/authorization', () => ({ - requireCredentialGroupEnrollmentAccess: mocks.requireEnrollmentAccess, + requireCredentialGroupCredentialAccess: mocks.requireCredentialAccess, })) vi.mock('@sim/platform-authz/workspace', () => ({ @@ -75,7 +75,8 @@ describe('resolveManagedOAuthCredentialToken', () => { vi.clearAllMocks() mocks.loadContext.mockResolvedValue(context) mocks.resolvePermission.mockResolvedValue('read') - mocks.requireEnrollmentAccess.mockResolvedValue({ + mocks.requireCredentialAccess.mockResolvedValue({ + scope: 'enrollment', enrollmentId: 'enrollment-1', email: 'person@example.com', }) @@ -125,7 +126,8 @@ describe('resolveManagedOAuthCredentialToken', () => { }) it('rejects a credential owned by another group enrollment', async () => { - mocks.requireEnrollmentAccess.mockResolvedValueOnce({ + mocks.requireCredentialAccess.mockResolvedValueOnce({ + scope: 'enrollment', enrollmentId: 'enrollment-2', email: 'other@example.com', }) @@ -138,4 +140,13 @@ describe('resolveManagedOAuthCredentialToken', () => { }) expect(mocks.resolveToken).not.toHaveBeenCalled() }) + + it('allows any credential in the group for a matching all-credentials grant', async () => { + mocks.requireCredentialAccess.mockResolvedValueOnce({ scope: 'all', grantId: 'grant-1' }) + + await expect( + resolveManagedOAuthCredentialToken.execute({ principal: executorPrincipal(), input }) + ).resolves.toEqual({ accessToken: 'access-token', refreshed: false }) + expect(mocks.resolveToken).toHaveBeenCalledOnce() + }) }) 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 90b8f0386ac..1d9c837e39b 100644 --- a/apps/sim/lib/credentials/application/resolve-managed-oauth-token.ts +++ b/apps/sim/lib/credentials/application/resolve-managed-oauth-token.ts @@ -1,7 +1,7 @@ import { AuditAction, AuditResourceType } from '@sim/audit' import { defineAuthorizedWorkspaceUseCase } from '@/lib/core/application' import { OrchestrationError } from '@/lib/core/orchestration/types' -import { requireCredentialGroupEnrollmentAccess } from '@/lib/credential-groups/application/authorization' +import { requireCredentialGroupCredentialAccess } from '@/lib/credential-groups/application/authorization' import { managedOAuthCredentialDelegationPolicy } from '@/lib/credentials/application/authorization' import { credentialOperations } from '@/lib/credentials/application/operations' import { @@ -26,11 +26,15 @@ export const resolveManagedOAuthCredentialToken = defineAuthorizedWorkspaceUseCa }, authorizationOptions: { delegation: managedOAuthCredentialDelegationPolicy }, async authorizeResource({ principal, context }) { - const access = await requireCredentialGroupEnrollmentAccess( + const access = await requireCredentialGroupCredentialAccess( principal, - context.credentialGroupId + context, + 'credential_groups.credentials.use' ) - if (access.enrollmentId !== context.credentialGroupEnrollmentId) { + if ( + access.scope === 'enrollment' && + access.enrollmentId !== context.credentialGroupEnrollmentId + ) { throw new OrchestrationError('forbidden', 'Credential Group enrollment access required') } }, diff --git a/apps/sim/lib/resource-policies/authorization.test.ts b/apps/sim/lib/resource-policies/authorization.test.ts new file mode 100644 index 00000000000..ad85a6b52b3 --- /dev/null +++ b/apps/sim/lib/resource-policies/authorization.test.ts @@ -0,0 +1,235 @@ +/** + * @vitest-environment node + */ +import type { WorkflowExecutionDelegatedPrincipal } from '@sim/auth/principal' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const mocks = vi.hoisted(() => ({ + isEnterprise: vi.fn(), + loadPolicy: vi.fn(), + resolveGroup: vi.fn(), + resolvePermission: vi.fn(), +})) + +vi.mock('@/lib/resource-policies/repository', () => ({ + loadResourcePolicy: mocks.loadPolicy, +})) + +vi.mock('@/lib/billing', () => ({ + isOrganizationOnEnterprisePlan: mocks.isEnterprise, +})) + +vi.mock('@/ee/access-control/utils/permission-check', () => ({ + resolveWorkspaceGroup: mocks.resolveGroup, +})) + +vi.mock('@sim/platform-authz/workspace', () => ({ + permissionSatisfies: (permission: string, required: string) => + permission === 'admin' || permission === required, + resolveEffectiveWorkspacePermission: mocks.resolvePermission, +})) + +import { findResourcePolicyGrant } from '@/lib/resource-policies/authorization' + +const context = { + workspaceId: 'workspace-1', + workspaceOrganizationId: 'organization-1', + allowPersonalApiKeys: true, +} + +function principal(input?: { mode?: 'draft' | 'deployment' }): WorkflowExecutionDelegatedPrincipal { + const mode = input?.mode ?? 'deployment' + return { + 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' }, + currentWorkflow: + mode === 'deployment' + ? { + workflowId: 'workflow-1', + mode, + deploymentVersionId: 'deployment-version-1', + } + : { workflowId: 'workflow-1', mode }, + }, + } +} + +function policy(subject: Record) { + return { + id: 'policy-1', + workspaceId: 'workspace-1', + revision: 1, + createdAt: new Date(), + updatedAt: new Date(), + document: { + version: 1, + resource: { type: 'credential_group', id: 'group-1' }, + grants: [ + { + id: 'grant-1', + subject, + actions: ['credential_groups.credentials.list', 'credential_groups.credentials.use'], + }, + ], + }, + } +} + +describe('findResourcePolicyGrant', () => { + beforeEach(() => { + vi.clearAllMocks() + mocks.isEnterprise.mockResolvedValue(true) + mocks.resolvePermission.mockResolvedValue('read') + }) + + it('matches workflow grants only for deployed current workflow authority', async () => { + mocks.loadPolicy.mockResolvedValue(policy({ type: 'workflow', workflowId: 'workflow-1' })) + const input = { + context, + resourceType: 'credential_group' as const, + resourceId: 'group-1', + action: 'credential_groups.credentials.list' as const, + } + + await expect( + findResourcePolicyGrant({ ...input, principal: principal() }) + ).resolves.toMatchObject({ + id: 'grant-1', + }) + await expect( + findResourcePolicyGrant({ ...input, principal: principal({ mode: 'draft' }) }) + ).resolves.toBeNull() + }) + + it('matches an exact verified external identity', async () => { + mocks.loadPolicy.mockResolvedValue( + policy({ + type: 'external_identity', + provider: 'slack', + tenantId: 'T123', + subjectId: 'U123', + }) + ) + const external = principal() + external.subjectUserId = undefined + if (!external.delegationContext) throw new Error('Test principal is missing delegation context') + external.delegationContext.principal = { + kind: 'system', + serviceId: 'webhook', + workspaceId: 'workspace-1', + workflowId: 'workflow-1', + webhookId: 'webhook-1', + provider: 'slack', + subject: { + kind: 'external_user', + provider: 'slack', + tenantId: 'T123', + subjectId: 'U123', + }, + } + + await expect( + findResourcePolicyGrant({ + principal: external, + context, + resourceType: 'credential_group', + resourceId: 'group-1', + action: 'credential_groups.credentials.use', + }) + ).resolves.toMatchObject({ id: 'grant-1' }) + }) + + it('treats absence of a policy as no additional grant', async () => { + mocks.loadPolicy.mockResolvedValue(null) + + await expect( + findResourcePolicyGrant({ + principal: principal(), + context, + resourceType: 'credential_group', + resourceId: 'group-1', + action: 'credential_groups.credentials.use', + }) + ).resolves.toBeNull() + }) + + it('matches live workspace roles and effective Access Control Groups', async () => { + mocks.loadPolicy.mockResolvedValue(policy({ type: 'workspace_role', minimumRole: 'write' })) + mocks.resolvePermission.mockResolvedValue('admin') + const input = { + principal: principal(), + context, + resourceType: 'credential_group' as const, + resourceId: 'group-1', + action: 'credential_groups.credentials.use' as const, + } + + await expect(findResourcePolicyGrant(input)).resolves.toMatchObject({ id: 'grant-1' }) + + mocks.loadPolicy.mockResolvedValue( + policy({ type: 'access_control_group', accessControlGroupId: 'access-group-1' }) + ) + mocks.resolveGroup.mockResolvedValue({ permissionGroupId: 'access-group-1' }) + + await expect(findResourcePolicyGrant(input)).resolves.toMatchObject({ id: 'grant-1' }) + expect(mocks.resolveGroup).toHaveBeenCalledWith('user-1', 'organization-1', 'workspace-1') + }) + + it('matches direct human operations without inventing workflow authority', async () => { + mocks.loadPolicy.mockResolvedValue(policy({ type: 'user', userId: 'user-1' })) + + await expect( + findResourcePolicyGrant({ + principal: { kind: 'session', userId: 'user-1', sessionId: 'session-1' }, + context, + resourceType: 'credential_group', + resourceId: 'group-1', + action: 'credential_groups.credentials.use', + }) + ).resolves.toMatchObject({ id: 'grant-1' }) + }) + + it('fails fast when executor delegation has lost its original principal', async () => { + mocks.loadPolicy.mockResolvedValue(policy({ type: 'user', userId: 'user-1' })) + const unbound = principal() + if (!unbound.delegationContext) throw new Error('Test principal is missing delegation context') + unbound.delegationContext.principal = undefined + + await expect( + findResourcePolicyGrant({ + principal: unbound, + context, + resourceType: 'credential_group', + resourceId: 'group-1', + action: 'credential_groups.credentials.use', + }) + ).rejects.toThrow('bound workflow execution principal') + }) + + it('fails fast when policy storage disagrees with the canonical workspace', async () => { + mocks.loadPolicy.mockResolvedValue({ + ...policy({ type: 'user', userId: 'user-1' }), + workspaceId: 'workspace-2', + }) + + await expect( + findResourcePolicyGrant({ + principal: principal(), + context, + resourceType: 'credential_group', + resourceId: 'group-1', + action: 'credential_groups.credentials.use', + }) + ).rejects.toThrow('workspace does not match') + }) +}) diff --git a/apps/sim/lib/resource-policies/authorization.ts b/apps/sim/lib/resource-policies/authorization.ts new file mode 100644 index 00000000000..158036b2cee --- /dev/null +++ b/apps/sim/lib/resource-policies/authorization.ts @@ -0,0 +1,123 @@ +import { type Principal, resolvePrincipalSubject } from '@sim/auth/principal' +import { + permissionSatisfies, + resolveEffectiveWorkspacePermission, +} from '@sim/platform-authz/workspace' +import { isOrganizationOnEnterprisePlan } from '@/lib/billing' +import type { WorkspaceAuthorizationContext } from '@/lib/core/application' +import { loadResourcePolicy } from '@/lib/resource-policies/repository' +import type { + ResourcePolicyAction, + ResourcePolicyGrant, + ResourcePolicyResourceType, + ResourcePolicySubject, +} from '@/lib/resource-policies/types' +import { resolveWorkspaceGroup } from '@/ee/access-control/utils/permission-check' + +interface ResourcePolicyAuthorizationInput { + principal: Principal + context: WorkspaceAuthorizationContext + resourceType: ResourcePolicyResourceType + resourceId: string + action: ResourcePolicyAction +} + +function resolveAuthorizationPrincipal(principal: Principal) { + if (principal.kind !== 'delegated' || principal.serviceId !== 'executor') { + return { invoker: principal, currentWorkflow: undefined } + } + if (!principal.delegationContext?.principal) { + throw new Error('Resource policy evaluation requires a bound workflow execution principal') + } + return { + invoker: principal.delegationContext.principal, + currentWorkflow: principal.delegationContext.currentWorkflow, + } +} + +async function matchesSubject( + subject: ResourcePolicySubject, + input: ResourcePolicyAuthorizationInput, + execution: ReturnType, + resolvers: { + workspacePermission: () => ReturnType + accessControlGroupId: () => Promise + } +): Promise { + const principalSubject = resolvePrincipalSubject(execution.invoker) + switch (subject.type) { + case 'user': + return principalSubject?.kind === 'sim_user' && principalSubject.userId === subject.userId + case 'external_identity': + return ( + principalSubject?.kind === 'external_user' && + principalSubject.provider === subject.provider && + principalSubject.tenantId === subject.tenantId && + principalSubject.subjectId === subject.subjectId + ) + case 'workflow': + return ( + execution.currentWorkflow?.mode === 'deployment' && + execution.currentWorkflow.workflowId === subject.workflowId + ) + case 'workspace_role': { + if (principalSubject?.kind !== 'sim_user') return false + const permission = await resolvers.workspacePermission() + return permission !== null && permissionSatisfies(permission, subject.minimumRole) + } + case 'access_control_group': { + return (await resolvers.accessControlGroupId()) === subject.accessControlGroupId + } + } +} + +export async function findResourcePolicyGrant( + input: ResourcePolicyAuthorizationInput +): Promise { + const policy = await loadResourcePolicy({ + resourceType: input.resourceType, + resourceId: input.resourceId, + }) + if (!policy) return null + if (policy.workspaceId !== input.context.workspaceId) { + throw new Error('Resource policy workspace does not match its canonical resource') + } + const execution = resolveAuthorizationPrincipal(input.principal) + const principalSubject = resolvePrincipalSubject(execution.invoker) + let workspacePermission: ReturnType | undefined + let accessControlGroupId: Promise | undefined + const resolvers = { + workspacePermission: () => { + if (principalSubject?.kind !== 'sim_user') return Promise.resolve(null) + workspacePermission ??= resolveEffectiveWorkspacePermission( + principalSubject.userId, + input.context.workspaceId, + input.context.workspaceOrganizationId + ) + return workspacePermission + }, + accessControlGroupId: () => { + accessControlGroupId ??= (async () => { + if ( + principalSubject?.kind !== 'sim_user' || + !input.context.workspaceOrganizationId || + !(await isOrganizationOnEnterprisePlan(input.context.workspaceOrganizationId)) + ) { + return null + } + const group = await resolveWorkspaceGroup( + principalSubject.userId, + input.context.workspaceOrganizationId, + input.context.workspaceId + ) + return group?.permissionGroupId ?? null + })() + return accessControlGroupId + }, + } + for (const grant of policy.document.grants) { + if (!grant.actions.includes(input.action)) continue + if (await matchesSubject(grant.subject, input, execution, resolvers)) return grant + } + return null +} diff --git a/apps/sim/lib/resource-policies/management.ts b/apps/sim/lib/resource-policies/management.ts new file mode 100644 index 00000000000..95f29a3bfa1 --- /dev/null +++ b/apps/sim/lib/resource-policies/management.ts @@ -0,0 +1,106 @@ +import { db } from '@sim/db' +import { permissionGroup, permissionGroupWorkspace, workflow } from '@sim/db/schema' +import { resolveEffectiveWorkspacePermission } from '@sim/platform-authz/workspace' +import { and, eq, isNull } from 'drizzle-orm' +import type { WorkspaceAuthorizationContext } from '@/lib/core/application' +import { OrchestrationError } from '@/lib/core/orchestration/types' +import type { ResourcePolicySubject } from '@/lib/resource-policies/types' + +async function requireUserSubject( + subject: Extract, + context: WorkspaceAuthorizationContext +) { + const permission = await resolveEffectiveWorkspacePermission( + subject.userId, + context.workspaceId, + context.workspaceOrganizationId + ) + if (!permission) { + throw new OrchestrationError('validation', 'Policy user does not have workspace access') + } +} + +async function requireWorkflowSubject( + subject: Extract, + context: WorkspaceAuthorizationContext +) { + const [row] = await db + .select({ id: workflow.id }) + .from(workflow) + .where( + and( + eq(workflow.id, subject.workflowId), + eq(workflow.workspaceId, context.workspaceId), + isNull(workflow.archivedAt) + ) + ) + .limit(1) + if (!row) throw new OrchestrationError('validation', 'Policy workflow was not found') +} + +async function requireAccessControlGroupSubject( + subject: Extract, + context: WorkspaceAuthorizationContext +) { + if (!context.workspaceOrganizationId) { + throw new OrchestrationError( + 'validation', + 'Access Control Group grants require an organization workspace' + ) + } + const [row] = await db + .select({ id: permissionGroup.id, isDefault: permissionGroup.isDefault }) + .from(permissionGroup) + .where( + and( + eq(permissionGroup.id, subject.accessControlGroupId), + eq(permissionGroup.organizationId, context.workspaceOrganizationId) + ) + ) + .limit(1) + if (!row) { + throw new OrchestrationError( + 'validation', + 'Access Control Group does not apply to this workspace' + ) + } + if (!row.isDefault) { + const [workspaceBinding] = await db + .select({ id: permissionGroupWorkspace.id }) + .from(permissionGroupWorkspace) + .where( + and( + eq(permissionGroupWorkspace.permissionGroupId, subject.accessControlGroupId), + eq(permissionGroupWorkspace.workspaceId, context.workspaceId) + ) + ) + .limit(1) + if (!workspaceBinding) { + throw new OrchestrationError( + 'validation', + 'Access Control Group does not apply to this workspace' + ) + } + } +} + +export async function validateResourcePolicySubjects( + subjects: ResourcePolicySubject[], + context: WorkspaceAuthorizationContext +): Promise { + await Promise.all( + subjects.map(async (subject) => { + switch (subject.type) { + case 'user': + return requireUserSubject(subject, context) + case 'workflow': + return requireWorkflowSubject(subject, context) + case 'access_control_group': + return requireAccessControlGroupSubject(subject, context) + case 'workspace_role': + case 'external_identity': + return + } + }) + ) +} diff --git a/apps/sim/lib/resource-policies/repository.ts b/apps/sim/lib/resource-policies/repository.ts new file mode 100644 index 00000000000..9dc067d6e9a --- /dev/null +++ b/apps/sim/lib/resource-policies/repository.ts @@ -0,0 +1,122 @@ +import { db } from '@sim/db' +import { resourcePolicy } from '@sim/db/schema' +import { generateId } from '@sim/utils/id' +import { and, eq } from 'drizzle-orm' +import { + parseResourcePolicyDocument, + type ResourcePolicyDocument, + type ResourcePolicyResourceType, +} from '@/lib/resource-policies/types' + +export interface StoredResourcePolicy { + id: string + workspaceId: string + revision: number + document: ResourcePolicyDocument + createdAt: Date + updatedAt: Date +} + +export class ResourcePolicyRevisionConflictError extends Error { + constructor() { + super('Resource policy changed while it was being edited') + this.name = 'ResourcePolicyRevisionConflictError' + } +} + +export async function loadResourcePolicy(input: { + resourceType: ResourcePolicyResourceType + resourceId: string +}): Promise { + const [row] = await db + .select() + .from(resourcePolicy) + .where( + and( + eq(resourcePolicy.resourceType, input.resourceType), + eq(resourcePolicy.resourceId, input.resourceId) + ) + ) + .limit(1) + if (!row) return null + return { + id: row.id, + workspaceId: row.workspaceId, + revision: row.revision, + document: parseResourcePolicyDocument(row.document, { + type: input.resourceType, + id: input.resourceId, + }), + createdAt: row.createdAt, + updatedAt: row.updatedAt, + } +} + +export async function writeResourcePolicy(input: { + workspaceId: string + resourceType: ResourcePolicyResourceType + resourceId: string + expectedRevision: number + document: ResourcePolicyDocument + actorUserId: string +}): Promise { + const document = parseResourcePolicyDocument(input.document, { + type: input.resourceType, + id: input.resourceId, + }) + const now = new Date() + if (input.expectedRevision === 0) { + const [created] = await db + .insert(resourcePolicy) + .values({ + id: generateId(), + workspaceId: input.workspaceId, + resourceType: input.resourceType, + resourceId: input.resourceId, + revision: 1, + document, + createdBy: input.actorUserId, + updatedBy: input.actorUserId, + createdAt: now, + updatedAt: now, + }) + .onConflictDoNothing() + .returning() + if (!created) throw new ResourcePolicyRevisionConflictError() + return { + id: created.id, + workspaceId: created.workspaceId, + revision: created.revision, + document, + createdAt: created.createdAt, + updatedAt: created.updatedAt, + } + } + + const [updated] = await db + .update(resourcePolicy) + .set({ + document, + revision: input.expectedRevision + 1, + updatedBy: input.actorUserId, + updatedAt: now, + }) + .where( + and( + eq(resourcePolicy.resourceType, input.resourceType), + eq(resourcePolicy.resourceId, input.resourceId), + eq(resourcePolicy.workspaceId, input.workspaceId), + eq(resourcePolicy.revision, input.expectedRevision) + ) + ) + .returning() + if (!updated) throw new ResourcePolicyRevisionConflictError() + return { + id: updated.id, + workspaceId: updated.workspaceId, + revision: updated.revision, + document, + createdAt: updated.createdAt, + updatedAt: updated.updatedAt, + } +} diff --git a/apps/sim/lib/resource-policies/types.test.ts b/apps/sim/lib/resource-policies/types.test.ts new file mode 100644 index 00000000000..aa76c85ed9e --- /dev/null +++ b/apps/sim/lib/resource-policies/types.test.ts @@ -0,0 +1,75 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it } from 'vitest' +import { parseResourcePolicyDocument } from '@/lib/resource-policies/types' + +describe('resource policy documents', () => { + it('accepts exact allow-only grants', () => { + const document = { + version: 1 as const, + resource: { type: 'credential_group' as const, id: 'group-1' }, + grants: [ + { + id: 'grant-1', + subject: { type: 'workflow' as const, workflowId: 'workflow-1' }, + actions: [ + 'credential_groups.credentials.list' as const, + 'credential_groups.credentials.use' as const, + ], + }, + ], + } + + expect( + parseResourcePolicyDocument(document, { type: 'credential_group', id: 'group-1' }) + ).toEqual(document) + }) + + it('rejects unknown fields, duplicate subjects, and wrong resource bindings', () => { + expect(() => + parseResourcePolicyDocument( + { + version: 1, + resource: { type: 'credential_group', id: 'group-1' }, + grants: [], + effect: 'allow', + }, + { type: 'credential_group', id: 'group-1' } + ) + ).toThrow() + + expect(() => + parseResourcePolicyDocument( + { + version: 1, + resource: { type: 'credential_group', id: 'group-1' }, + grants: [ + { + id: 'grant-1', + subject: { type: 'workflow', workflowId: 'workflow-1' }, + actions: ['credential_groups.credentials.use'], + }, + { + id: 'grant-2', + subject: { type: 'workflow', workflowId: 'workflow-1' }, + actions: ['credential_groups.credentials.list'], + }, + ], + }, + { type: 'credential_group', id: 'group-1' } + ) + ).toThrow('subject can only appear once') + + expect(() => + parseResourcePolicyDocument( + { + version: 1, + resource: { type: 'credential_group', id: 'group-2' }, + grants: [], + }, + { type: 'credential_group', id: 'group-1' } + ) + ).toThrow('does not match') + }) +}) diff --git a/apps/sim/lib/resource-policies/types.ts b/apps/sim/lib/resource-policies/types.ts new file mode 100644 index 00000000000..be3b9e92973 --- /dev/null +++ b/apps/sim/lib/resource-policies/types.ts @@ -0,0 +1,128 @@ +import { z } from 'zod' + +export const RESOURCE_POLICY_VERSION = 1 as const +export const RESOURCE_POLICY_RESOURCE_TYPES = ['credential_group'] as const +export const RESOURCE_POLICY_ACTIONS = [ + 'credential_groups.credentials.list', + 'credential_groups.credentials.use', +] as const +export type ResourcePolicyResourceType = (typeof RESOURCE_POLICY_RESOURCE_TYPES)[number] +export type ResourcePolicyAction = (typeof RESOURCE_POLICY_ACTIONS)[number] + +export const RESOURCE_POLICY_ACTIONS_BY_RESOURCE = { + credential_group: RESOURCE_POLICY_ACTIONS, +} as const satisfies Record + +const userSubjectSchema = z + .object({ type: z.literal('user'), userId: z.string().min(1).max(128) }) + .strict() +const workspaceRoleSubjectSchema = z + .object({ + type: z.literal('workspace_role'), + minimumRole: z.enum(['read', 'write', 'admin']), + }) + .strict() +const accessControlGroupSubjectSchema = z + .object({ + type: z.literal('access_control_group'), + accessControlGroupId: z.string().min(1).max(128), + }) + .strict() +const workflowSubjectSchema = z + .object({ type: z.literal('workflow'), workflowId: z.string().min(1).max(128) }) + .strict() +const externalIdentitySubjectSchema = z + .object({ + type: z.literal('external_identity'), + provider: z.string().min(1).max(128), + tenantId: z.string().min(1).max(256), + subjectId: z.string().min(1).max(256), + }) + .strict() + +export const resourcePolicySubjectSchema = z.discriminatedUnion('type', [ + userSubjectSchema, + workspaceRoleSubjectSchema, + accessControlGroupSubjectSchema, + workflowSubjectSchema, + externalIdentitySubjectSchema, +]) + +export type ResourcePolicySubject = z.output + +export const resourcePolicyGrantSchema = z + .object({ + id: z.string().min(1).max(128), + subject: resourcePolicySubjectSchema, + actions: z.array(z.enum(RESOURCE_POLICY_ACTIONS)).min(1).max(RESOURCE_POLICY_ACTIONS.length), + }) + .strict() + +export const resourcePolicyDocumentSchema = z + .object({ + version: z.literal(RESOURCE_POLICY_VERSION), + resource: z + .object({ + type: z.enum(RESOURCE_POLICY_RESOURCE_TYPES), + id: z.string().min(1).max(128), + }) + .strict(), + grants: z.array(resourcePolicyGrantSchema).max(100), + }) + .strict() + .superRefine((document, ctx) => { + const resourceActions: ReadonlySet = new Set( + RESOURCE_POLICY_ACTIONS_BY_RESOURCE[document.resource.type] + ) + const ids = new Set() + const subjects = new Set() + for (const [index, grant] of document.grants.entries()) { + if (ids.has(grant.id)) { + ctx.addIssue({ + code: 'custom', + path: ['grants', index, 'id'], + message: 'Resource policy grant IDs must be unique', + }) + } + ids.add(grant.id) + const subjectKey = JSON.stringify(grant.subject) + if (subjects.has(subjectKey)) { + ctx.addIssue({ + code: 'custom', + path: ['grants', index, 'subject'], + message: 'A resource policy subject can only appear once', + }) + } + subjects.add(subjectKey) + if (new Set(grant.actions).size !== grant.actions.length) { + ctx.addIssue({ + code: 'custom', + path: ['grants', index, 'actions'], + message: 'Resource policy actions must be unique within a grant', + }) + } + for (const [actionIndex, action] of grant.actions.entries()) { + if (!resourceActions.has(action)) { + ctx.addIssue({ + code: 'custom', + path: ['grants', index, 'actions', actionIndex], + message: 'Resource policy action does not apply to this resource type', + }) + } + } + } + }) + +export type ResourcePolicyGrant = z.output +export type ResourcePolicyDocument = z.output + +export function parseResourcePolicyDocument( + value: unknown, + expected: { type: ResourcePolicyResourceType; id: string } +): ResourcePolicyDocument { + const document = resourcePolicyDocumentSchema.parse(value) + if (document.resource.type !== expected.type || document.resource.id !== expected.id) { + throw new Error('Resource policy document does not match its canonical resource') + } + return document +} diff --git a/apps/sim/lib/workflows/executor/execution-core.ts b/apps/sim/lib/workflows/executor/execution-core.ts index 35ea1d09f4a..aa57b0d26ae 100644 --- a/apps/sim/lib/workflows/executor/execution-core.ts +++ b/apps/sim/lib/workflows/executor/execution-core.ts @@ -947,6 +947,9 @@ async function executeWorkflowCoreImpl( workflowId, ...(executionId ? { executionId } : {}), principal: metadata.principal, + currentWorkflow: deploymentVersionId + ? { workflowId, mode: 'deployment', deploymentVersionId } + : { workflowId, mode: 'draft' }, }, isDeployedContext: !metadata.isClientSession, enforceCredentialAccess: metadata.enforceCredentialAccess ?? false, diff --git a/design/credential-group-authorization.md b/design/credential-group-authorization.md index 06bcc60e302..0975c8003a0 100644 --- a/design/credential-group-authorization.md +++ b/design/credential-group-authorization.md @@ -1,6 +1,6 @@ # Credential Group Authorization -Status: actor-only enforcement is implemented; group-wide resource grants are proposed +Status: actor-only enforcement and group-wide resource grants are implemented ## Purpose @@ -41,7 +41,7 @@ Principal userId -> active enrollment with the same normalized email ``` -For Slack, the next step is: +For Slack, the implementation resolves: ```text verified webhook Principal subject (slack, teamId, userId) @@ -212,7 +212,7 @@ Managing the policy does not automatically grant the administrator permission to ## Current implementation delta -The branch already: +The branch: - threads the original Principal into Credential Group executor delegation; - removes caller-supplied email filtering from credential listing; @@ -220,12 +220,10 @@ The branch already: - filters list pagination by that enrollment; - rechecks the same enrollment when resolving managed OAuth tokens; - fails actorless execution instead of substituting a billing or workflow owner. +- stores and evaluates generic allow-only resource policies; +- resolves user, workspace-role, Access Control Group, external-identity, and deployed-workflow subjects; +- grants whole-group list and use access when an explicit policy matches; +- binds Slack's verified provider subject to an active enrollment; +- exposes an audited, optimistic-concurrency admin management API and structured Access tab. -Remaining work is: - -1. Add the generic resource-policy store and evaluator. -2. Resolve user, workspace-role, Access Control Group, and deployed-workflow subjects. -3. Extend list authorization to actor enrollment or a `credentials.list` grant. -4. Extend token authorization to actor ownership or a `credentials.use` grant. -5. Bind Slack's verified nested external subject to the matching enrollment. -6. Add admin policy management and audit surfaces. +Knowledge Base/table resource enforcement and provenance-aware log redaction remain separate follow-up work. diff --git a/design/principal-passing.md b/design/principal-passing.md index bc094813f90..b8084b0663e 100644 --- a/design/principal-passing.md +++ b/design/principal-passing.md @@ -197,8 +197,8 @@ Audit records can show both the invocation surface and verified subject. A Slack ## Current implementation delta -The branch already carries a versioned Principal through execution metadata, workers, snapshots, nested workflows, and tool execution. It also carries verified Slack subjects and supports executor delegation without inventing a billing-owner subject. Remaining identity work is: +The branch carries a versioned Principal through execution metadata, workers, snapshots, nested workflows, and tool execution. It also carries verified Slack subjects and supports executor delegation without inventing a billing-owner subject. -1. Promote Principal plus workflow binding into the explicit execution-identity envelope. -2. Carry canonical deployment authority into internal delegation. -3. Distinguish root and currently executing workflow authority for subworkflows. +Internal delegation now preserves immutable root causality separately from the current workflow. Deployed workflow authority is bound to the active deployment version at every internal application call, while resource policies retain the stable workflow ID as their key. + +A future cleanup can combine these currently separate trusted fields into one explicit execution-identity envelope. That is a representation cleanup, not a prerequisite for Credential Group resource-policy enforcement. diff --git a/design/resource-policies.md b/design/resource-policies.md index 47359fc5ac9..17a41667b57 100644 --- a/design/resource-policies.md +++ b/design/resource-policies.md @@ -1,6 +1,6 @@ # Resource Policies -Status: proposed reusable authorization model +Status: reusable store and Credential Group integration implemented ## Goal @@ -228,9 +228,10 @@ resource_policy workspace_id resource_type resource_id - version - document_json + revision + document created_by + updated_by created_at updated_at ``` @@ -258,6 +259,12 @@ authenticate Principal Routes, blocks, tools, Copilot adapters, and executor handlers do not query policy tables or make independent authorization decisions. +## Current implementation + +The generic policy document, strict parser, optimistic repository, subject evaluator, and subject-management validation are implemented. Credential Groups are the first resource type and currently expose one managed permission in the UI: list and use every credential in the group. + +Future resource types extend the exact resource/action registry and call the same evaluator from their application operations. Knowledge Base/table enforcement and log-provenance redaction remain separate follow-up work. + ## Provenance and logs Future protected resources attach provenance to values returned into workflow execution: diff --git a/packages/auth/src/principal.ts b/packages/auth/src/principal.ts index ddd5a6dc45f..40cfe21adec 100644 --- a/packages/auth/src/principal.ts +++ b/packages/auth/src/principal.ts @@ -77,8 +77,13 @@ export interface WorkflowExecutionDelegationContext { workflowId: string executionId?: string principal?: WorkflowExecutionPrincipal + currentWorkflow?: WorkflowExecutionAuthority } +export type WorkflowExecutionAuthority = + | { workflowId: string; mode: 'draft' } + | { workflowId: string; mode: 'deployment'; deploymentVersionId: string } + export interface WorkflowExecutionDelegatedPrincipal extends DelegatedPrincipalBase { serviceId: 'executor' subjectUserId?: string diff --git a/packages/db/schema.ts b/packages/db/schema.ts index ae99cdcf3c8..4ef59ddf770 100644 --- a/packages/db/schema.ts +++ b/packages/db/schema.ts @@ -4264,6 +4264,32 @@ export const permissionGroupMember = pgTable( }) ) +/** Versioned allow-only policy attached to one canonical workspace resource. */ +export const resourcePolicy = pgTable( + 'resource_policy', + { + id: text('id').primaryKey(), + workspaceId: text('workspace_id') + .notNull() + .references(() => workspace.id, { onDelete: 'cascade' }), + resourceType: text('resource_type').notNull(), + resourceId: text('resource_id').notNull(), + revision: integer('revision').notNull().default(1), + document: jsonb('document').$type().notNull(), + createdBy: text('created_by').references(() => user.id, { onDelete: 'set null' }), + updatedBy: text('updated_by').references(() => user.id, { onDelete: 'set null' }), + createdAt: timestamp('created_at').notNull().defaultNow(), + updatedAt: timestamp('updated_at').notNull().defaultNow(), + }, + (table) => ({ + resourceUnique: uniqueIndex('resource_policy_resource_unique').on( + table.resourceType, + table.resourceId + ), + workspaceIdx: index('resource_policy_workspace_id_idx').on(table.workspaceId), + }) +) + /** * Async Jobs - Queue for background job processing (Redis/DB backends) * Used when trigger.dev is not available for async workflow executions diff --git a/scripts/check-api-validation-contracts.ts b/scripts/check-api-validation-contracts.ts index 2e5c9b35929..e77fcb28b56 100644 --- a/scripts/check-api-validation-contracts.ts +++ b/scripts/check-api-validation-contracts.ts @@ -9,8 +9,8 @@ const QUERY_HOOKS_DIR = path.join(ROOT, 'apps/sim/hooks/queries') const SELECTOR_HOOKS_DIR = path.join(ROOT, 'apps/sim/hooks/selectors') const BASELINE = { - totalRoutes: 1141, - zodRoutes: 1141, + totalRoutes: 1142, + zodRoutes: 1142, nonZodRoutes: 0, } as const From 9fdd082e8a2c493fcd59a7c9aaf4d144a234c869 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Thu, 20 Aug 2026 13:42:37 -0700 Subject: [PATCH 2/6] fix(credential-groups): clarify empty access subjects --- .../components/credential-group-access.tsx | 56 +++++++++++-------- 1 file changed, 32 insertions(+), 24 deletions(-) diff --git a/apps/sim/ee/credential-groups/components/credential-group-access.tsx b/apps/sim/ee/credential-groups/components/credential-group-access.tsx index b7ea50f931a..e590832146c 100644 --- a/apps/sim/ee/credential-groups/components/credential-group-access.tsx +++ b/apps/sim/ee/credential-groups/components/credential-group-access.tsx @@ -3,7 +3,6 @@ import { useState } from 'react' import { Chip, - ChipDropdown, ChipModal, ChipModalBody, ChipModalError, @@ -80,6 +79,17 @@ export function CredentialGroupAccess({ workspaceId, groupId }: CredentialGroupA : subjectType === 'access_control_group' ? applicablePermissionGroups.map((group) => ({ value: group.id, label: group.name })) : [...WORKSPACE_ROLE_OPTIONS] + const targetOptionsPending = + subjectType === 'workflow' + ? workflows.isPending + : subjectType === 'access_control_group' + ? permissionConfig.isPending || permissionGroups.isPending + : false + const targetPlaceholder = targetOptionsPending + ? `Loading ${subjectType === 'workflow' ? 'workflows' : 'access control groups'}...` + : targetOptions.length === 0 + ? `No ${subjectType === 'workflow' ? 'workflows' : 'access control groups'} available` + : 'Select one' const labelForSubject = (subject: ResourcePolicySubject): string => { switch (subject.type) { @@ -229,31 +239,29 @@ export function CredentialGroupAccess({ workspaceId, groupId }: CredentialGroupA > setShowAdd(false)}>Add access - - { - setSubjectType(value as ManagedSubjectType) - setTargetId('') - setModalError(null) - }} - options={[...SUBJECT_TYPE_OPTIONS]} - /> - { + setSubjectType(value as ManagedSubjectType) + setTargetId('') + setModalError(null) + }} + options={SUBJECT_TYPE_OPTIONS} + /> + - { - setTargetId(value) - setModalError(null) - }} - options={targetOptions} - placeholder='Select one' - /> - + value={targetId} + onChange={(value) => { + setTargetId(value) + setModalError(null) + }} + options={targetOptions} + placeholder={targetPlaceholder} + disabled={targetOptionsPending || targetOptions.length === 0} + /> {modalError} Date: Thu, 20 Aug 2026 17:17:27 -0700 Subject: [PATCH 3/6] fix(resource-policies): authorize credential use only --- .../application/list-credentials.test.ts | 15 ------ .../application/manage-access.test.ts | 2 +- .../application/manage-access.ts | 7 +-- .../resource-policies/authorization.test.ts | 4 +- apps/sim/lib/resource-policies/types.test.ts | 7 +-- apps/sim/lib/resource-policies/types.ts | 5 +- design/credential-group-authorization.md | 52 +++++-------------- design/resource-policies.md | 14 ++--- 8 files changed, 25 insertions(+), 81 deletions(-) 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 31a1dd200e5..1b8d123dea5 100644 --- a/apps/sim/lib/credential-groups/application/list-credentials.test.ts +++ b/apps/sim/lib/credential-groups/application/list-credentials.test.ts @@ -230,21 +230,6 @@ describe('listCredentialGroupCredentials', () => { }) }) - it('lists the entire group for a matching resource-policy grant', async () => { - mocks.findPolicyGrant.mockResolvedValueOnce({ - id: 'grant-1', - subject: { type: 'workflow', workflowId: 'workflow-1' }, - actions: ['credential_groups.credentials.list', 'credential_groups.credentials.use'], - }) - - await listCredentialGroupCredentials.execute({ principal: executorPrincipal(), input }) - - expect(mocks.loadEnrollmentAccess).not.toHaveBeenCalled() - expect(mocks.listCredentials).toHaveBeenCalledWith( - expect.objectContaining({ credentialGroupEnrollmentId: undefined }) - ) - }) - it('filters by canonical providers active in the group', async () => { await listCredentialGroupCredentials.execute({ principal: executorPrincipal(), diff --git a/apps/sim/lib/credential-groups/application/manage-access.test.ts b/apps/sim/lib/credential-groups/application/manage-access.test.ts index ff1cf735dd8..4b0618a9065 100644 --- a/apps/sim/lib/credential-groups/application/manage-access.test.ts +++ b/apps/sim/lib/credential-groups/application/manage-access.test.ts @@ -142,7 +142,7 @@ describe('Credential Group access policy operations', () => { { id: expect.any(String), subject, - actions: ['credential_groups.credentials.list', 'credential_groups.credentials.use'], + actions: ['credential_groups.credentials.use'], }, ], }, diff --git a/apps/sim/lib/credential-groups/application/manage-access.ts b/apps/sim/lib/credential-groups/application/manage-access.ts index a6143efb166..5952473bf1e 100644 --- a/apps/sim/lib/credential-groups/application/manage-access.ts +++ b/apps/sim/lib/credential-groups/application/manage-access.ts @@ -16,10 +16,7 @@ import { resourcePolicySubjectSchema, } from '@/lib/resource-policies/types' -const CREDENTIAL_GROUP_ALL_ACTIONS = [ - 'credential_groups.credentials.list', - 'credential_groups.credentials.use', -] as const +const CREDENTIAL_GROUP_ACCESS_ACTIONS = ['credential_groups.credentials.use'] as const interface CredentialGroupAccessTargetInput { assertedWorkspaceId: string @@ -91,7 +88,7 @@ export const updateCredentialGroupAccess = defineAuthorizedWorkspaceUseCase({ grants: input.grants.map((grant, index) => ({ id: grant.id ?? generateId(), subject: subjects[index], - actions: [...CREDENTIAL_GROUP_ALL_ACTIONS], + actions: [...CREDENTIAL_GROUP_ACCESS_ACTIONS], })), }, }) diff --git a/apps/sim/lib/resource-policies/authorization.test.ts b/apps/sim/lib/resource-policies/authorization.test.ts index ad85a6b52b3..a585a14c5b8 100644 --- a/apps/sim/lib/resource-policies/authorization.test.ts +++ b/apps/sim/lib/resource-policies/authorization.test.ts @@ -78,7 +78,7 @@ function policy(subject: Record) { { id: 'grant-1', subject, - actions: ['credential_groups.credentials.list', 'credential_groups.credentials.use'], + actions: ['credential_groups.credentials.use'], }, ], }, @@ -98,7 +98,7 @@ describe('findResourcePolicyGrant', () => { context, resourceType: 'credential_group' as const, resourceId: 'group-1', - action: 'credential_groups.credentials.list' as const, + action: 'credential_groups.credentials.use' as const, } await expect( diff --git a/apps/sim/lib/resource-policies/types.test.ts b/apps/sim/lib/resource-policies/types.test.ts index aa76c85ed9e..18164b91e58 100644 --- a/apps/sim/lib/resource-policies/types.test.ts +++ b/apps/sim/lib/resource-policies/types.test.ts @@ -13,10 +13,7 @@ describe('resource policy documents', () => { { id: 'grant-1', subject: { type: 'workflow' as const, workflowId: 'workflow-1' }, - actions: [ - 'credential_groups.credentials.list' as const, - 'credential_groups.credentials.use' as const, - ], + actions: ['credential_groups.credentials.use' as const], }, ], } @@ -53,7 +50,7 @@ describe('resource policy documents', () => { { id: 'grant-2', subject: { type: 'workflow', workflowId: 'workflow-1' }, - actions: ['credential_groups.credentials.list'], + actions: ['credential_groups.credentials.use'], }, ], }, diff --git a/apps/sim/lib/resource-policies/types.ts b/apps/sim/lib/resource-policies/types.ts index be3b9e92973..c8bea9702f6 100644 --- a/apps/sim/lib/resource-policies/types.ts +++ b/apps/sim/lib/resource-policies/types.ts @@ -2,10 +2,7 @@ import { z } from 'zod' export const RESOURCE_POLICY_VERSION = 1 as const export const RESOURCE_POLICY_RESOURCE_TYPES = ['credential_group'] as const -export const RESOURCE_POLICY_ACTIONS = [ - 'credential_groups.credentials.list', - 'credential_groups.credentials.use', -] as const +export const RESOURCE_POLICY_ACTIONS = ['credential_groups.credentials.use'] as const export type ResourcePolicyResourceType = (typeof RESOURCE_POLICY_RESOURCE_TYPES)[number] export type ResourcePolicyAction = (typeof RESOURCE_POLICY_ACTIONS)[number] diff --git a/design/credential-group-authorization.md b/design/credential-group-authorization.md index 0975c8003a0..d11443821bd 100644 --- a/design/credential-group-authorization.md +++ b/design/credential-group-authorization.md @@ -8,7 +8,7 @@ Credential Groups collect external users' managed credentials and let workflows The default is simple: -> An execution with a verified actor may list and use only that actor's credentials. +> An execution may list every non-secret credential reference, but it may use only the verified actor's credential. Explicit resource-policy grants can expand a user, Access Control Group, workspace role, or deployed workflow to every credential in the Credential Group. @@ -60,7 +60,7 @@ List Credentials Its contract is: -> Return every credential that this execution is currently authorized to use. +> Return every active credential reference in the group; selecting a reference does not authorize its use. Inputs may select the Credential Group, provider option, and pagination. Inputs never select identity or authorization scope: @@ -83,28 +83,7 @@ The input does not contain: ## List authorization -The application operation loads the canonical Credential Group, verifies workspace access, and evaluates the current resource policy. - -```text -matching credentials.list grant - -> list all active credentials in the group - -otherwise verified actor with active enrollment - -> list only credentials for that exact enrollment - -otherwise - -> fail -``` - -The internal result is a database query constraint, not persisted policy state: - -```ts -type CredentialListAuthorization = - | { enrollmentId: string } - | { grantId: string } -``` - -The exact enrollment constraint is applied to cursor validation and every page query. A credential ID returned from an earlier call is not an authorization capability. +The application operation loads the canonical Credential Group, verifies executor delegation, workspace binding, group scope, status, and entitlement, then returns a bounded page of all active credential references in the group. The result contains opaque IDs and account metadata but no token material, and a returned credential ID is never an authorization capability. ## Credential use authorization @@ -131,7 +110,6 @@ Tokens and refresh tokens never appear in block output or execution logs. List r The Credential Group resource policy can grant: ```text -credential_groups.credentials.list credential_groups.credentials.use ``` @@ -144,10 +122,7 @@ Example workflow grant: "type": "workflow", "workflowId": "wf_support" }, - "actions": [ - "credential_groups.credentials.list", - "credential_groups.credentials.use" - ] + "actions": ["credential_groups.credentials.use"] } ``` @@ -162,10 +137,7 @@ Example Access Control Group grant: "type": "access_control_group", "accessControlGroupId": "pg_support_admins" }, - "actions": [ - "credential_groups.credentials.list", - "credential_groups.credentials.use" - ] + "actions": ["credential_groups.credentials.use"] } ``` @@ -173,7 +145,9 @@ Membership is evaluated at operation time. Removing a user from the Access Contr ## Execution behavior -| Execution | Result | +All executions that pass the list operation's workspace and group checks can see the same credential references. Using a selected credential resolves as follows: + +| Execution | Credential use result | | --- | --- | | Manual actor without an explicit grant | Actor's enrollment only | | Manual actor in a granted Access Control Group | All credentials | @@ -184,7 +158,7 @@ Membership is evaluated at operation time. Removing a user from the Access Contr | Actorless schedule without a workflow grant | Fail | | Actor with no enrollment and no explicit grant | Fail | -There is no silent fallback from a requested all-credentials mode because the block has no caller-controlled access mode. It simply returns the set authorized for the current execution. +There is no silent fallback from a requested all-credentials mode because the block has no caller-controlled access mode. Authorization happens when the selected credential is assumed. Manual testing exercises the actor path with the tester's credential. Full group-wide behavior is tested through a deployed execution, preferably against a staging Credential Group in a forked workspace. @@ -197,7 +171,7 @@ Slack signature and installation verified -> webhook Principal with (teamId, userId) subject -> workflow execution -> Credential Group enrollment resolution - -> actor-scoped credential list/use + -> actor-scoped credential use ``` The Slack trigger subscription credential only receives events. It is not the external user's downstream credential. @@ -208,7 +182,7 @@ Bot events and events without a verified human subject have no actor. They requi Credential Group creation, options, invitations, enrollment lifecycle, and resource-policy changes remain control-plane operations requiring current workspace-admin authorization and audit. -Managing the policy does not automatically grant the administrator permission to list or use credentials. Data-plane access still requires actor ownership or an explicit grant. +Managing the policy does not automatically grant the administrator permission to use credentials. Data-plane credential use still requires actor ownership or an explicit grant. ## Current implementation delta @@ -217,12 +191,12 @@ The branch: - threads the original Principal into Credential Group executor delegation; - removes caller-supplied email filtering from credential listing; - resolves a verified Sim user to an exact active enrollment; -- filters list pagination by that enrollment; +- lists bounded non-secret references for every active credential in the group; - rechecks the same enrollment when resolving managed OAuth tokens; - fails actorless execution instead of substituting a billing or workflow owner. - stores and evaluates generic allow-only resource policies; - resolves user, workspace-role, Access Control Group, external-identity, and deployed-workflow subjects; -- grants whole-group list and use access when an explicit policy matches; +- grants whole-group credential use when an explicit policy matches; - binds Slack's verified provider subject to an active enrollment; - exposes an audited, optimistic-concurrency admin management API and structured Access tab. diff --git a/design/resource-policies.md b/design/resource-policies.md index 17a41667b57..938700eaa86 100644 --- a/design/resource-policies.md +++ b/design/resource-policies.md @@ -100,10 +100,7 @@ Policies store stable subject references, never session IDs, API keys, delegatio "type": "workflow", "workflowId": "wf_support" }, - "actions": [ - "credential_groups.credentials.list", - "credential_groups.credentials.use" - ] + "actions": ["credential_groups.credentials.use"] }, { "id": "support-admins", @@ -111,10 +108,7 @@ Policies store stable subject references, never session IDs, API keys, delegatio "type": "access_control_group", "accessControlGroupId": "pg_support_admins" }, - "actions": [ - "credential_groups.credentials.list", - "credential_groups.credentials.use" - ] + "actions": ["credential_groups.credentials.use"] } ] } @@ -261,7 +255,7 @@ Routes, blocks, tools, Copilot adapters, and executor handlers do not query poli ## Current implementation -The generic policy document, strict parser, optimistic repository, subject evaluator, and subject-management validation are implemented. Credential Groups are the first resource type and currently expose one managed permission in the UI: list and use every credential in the group. +The generic policy document, strict parser, optimistic repository, subject evaluator, and subject-management validation are implemented. Credential Groups are the first resource type and currently expose one managed permission in the UI: use every credential in the group. Future resource types extend the exact resource/action registry and call the same evaluator from their application operations. Knowledge Base/table enforcement and log-provenance redaction remain separate follow-up work. @@ -288,7 +282,7 @@ Provenance and viewer-specific log shielding are a later phase. They use the sam 1. Add strict policy types, validation, storage, and evaluator. 2. Add admin policy read/write application operations and audit. 3. Add deployed-workflow authority to execution identity. -4. Integrate Credential Group list and use authorization. +4. Integrate Credential Group credential-use authorization. 5. Add user, workspace-role, and Access Control Group subject resolution. 6. Add policy management UI on Credential Groups. 7. Extend the same evaluator to Knowledge Bases and tables. From 8a5605023cfcd8fed578c309c40b0ed60bdb77f2 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Thu, 20 Aug 2026 21:45:36 -0700 Subject: [PATCH 4/6] fix(db): renumber resource policy migration --- .../migrations/0298_sparkling_hemingway.sql | 18 + .../db/migrations/meta/0298_snapshot.json | 20214 ++++++++++++++++ packages/db/migrations/meta/_journal.json | 7 + 3 files changed, 20239 insertions(+) create mode 100644 packages/db/migrations/0298_sparkling_hemingway.sql create mode 100644 packages/db/migrations/meta/0298_snapshot.json diff --git a/packages/db/migrations/0298_sparkling_hemingway.sql b/packages/db/migrations/0298_sparkling_hemingway.sql new file mode 100644 index 00000000000..6502a9b9991 --- /dev/null +++ b/packages/db/migrations/0298_sparkling_hemingway.sql @@ -0,0 +1,18 @@ +CREATE TABLE "resource_policy" ( + "id" text PRIMARY KEY NOT NULL, + "workspace_id" text NOT NULL, + "resource_type" text NOT NULL, + "resource_id" text NOT NULL, + "revision" integer DEFAULT 1 NOT NULL, + "document" jsonb NOT NULL, + "created_by" text, + "updated_by" text, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "resource_policy" ADD CONSTRAINT "resource_policy_workspace_id_workspace_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspace"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "resource_policy" ADD CONSTRAINT "resource_policy_created_by_user_id_fk" FOREIGN KEY ("created_by") REFERENCES "public"."user"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "resource_policy" ADD CONSTRAINT "resource_policy_updated_by_user_id_fk" FOREIGN KEY ("updated_by") REFERENCES "public"."user"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +CREATE UNIQUE INDEX "resource_policy_resource_unique" ON "resource_policy" USING btree ("resource_type","resource_id");--> statement-breakpoint +CREATE INDEX "resource_policy_workspace_id_idx" ON "resource_policy" USING btree ("workspace_id"); \ No newline at end of file diff --git a/packages/db/migrations/meta/0298_snapshot.json b/packages/db/migrations/meta/0298_snapshot.json new file mode 100644 index 00000000000..b5aee6d44e6 --- /dev/null +++ b/packages/db/migrations/meta/0298_snapshot.json @@ -0,0 +1,20214 @@ +{ + "id": "3a255e90-555a-4dac-8ef3-7e78d2735725", + "prevId": "c9c21e6c-7324-484b-b303-ab7b4fd9ab6d", + "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_number_idx": { + "name": "academy_certificate_number_idx", + "columns": [ + { + "expression": "certificate_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "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 + }, + "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_tool_call_id_idx": { + "name": "copilot_async_tool_calls_tool_call_id_idx", + "columns": [ + { + "expression": "tool_call_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 + }, + "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_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" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_feedback": { + "name": "copilot_feedback", + "schema": "", + "columns": { + "feedback_id": { + "name": "feedback_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_query": { + "name": "user_query", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_response": { + "name": "agent_response", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_positive": { + "name": "is_positive", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "feedback": { + "name": "feedback", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_yaml": { + "name": "workflow_yaml", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_feedback_user_id_idx": { + "name": "copilot_feedback_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_chat_id_idx": { + "name": "copilot_feedback_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_user_chat_idx": { + "name": "copilot_feedback_user_chat_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_is_positive_idx": { + "name": "copilot_feedback_is_positive_idx", + "columns": [ + { + "expression": "is_positive", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_created_at_idx": { + "name": "copilot_feedback_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_feedback_user_id_user_id_fk": { + "name": "copilot_feedback_user_id_user_id_fk", + "tableFrom": "copilot_feedback", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_feedback_chat_id_copilot_chats_id_fk": { + "name": "copilot_feedback_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_feedback", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.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": true + }, + "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 + }, + "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 + }, + "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 + }, + "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 + }, + "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": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_workspace_id_idx": { + "name": "credential_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_type_idx": { + "name": "credential_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_provider_id_idx": { + "name": "credential_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_account_id_idx": { + "name": "credential_account_id_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_env_owner_user_id_idx": { + "name": "credential_env_owner_user_id_idx", + "columns": [ + { + "expression": "env_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_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_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_workspace_account_unique": { + "name": "credential_workspace_account_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "account_id IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_env_unique": { + "name": "credential_workspace_env_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "type = 'env_workspace'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_personal_env_unique": { + "name": "credential_workspace_personal_env_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "type = 'env_personal'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_workspace_id_workspace_id_fk": { + "name": "credential_workspace_id_workspace_id_fk", + "tableFrom": "credential", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_account_id_account_id_fk": { + "name": "credential_account_id_account_id_fk", + "tableFrom": "credential", + "tableTo": "account", + "columnsFrom": ["account_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_env_owner_user_id_user_id_fk": { + "name": "credential_env_owner_user_id_user_id_fk", + "tableFrom": "credential", + "tableTo": "user", + "columnsFrom": ["env_owner_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_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_created_by_user_id_fk": { + "name": "credential_created_by_user_id_fk", + "tableFrom": "credential", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "credential_oauth_source_check": { + "name": "credential_oauth_source_check", + "value": "(type <> 'oauth') OR (account_id IS NOT NULL AND provider_id IS NOT NULL)" + }, + "credential_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 cardinality(granted_scopes) > 0\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_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": true + }, + "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_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_status_idx": { + "name": "credential_group_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": {} + }, + "credential_group_workspace_name_unique": { + "name": "credential_group_workspace_name_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"name\")", + "asc": true, + "isExpression": 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_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": {}, + "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 + }, + "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_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_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 + }, + "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_queued_at": { + "name": "processing_queued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_completed_at": { + "name": "processing_completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_error": { + "name": "processing_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "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 + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "doc_kb_id_idx": { + "name": "doc_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_filename_idx": { + "name": "doc_filename_idx", + "columns": [ + { + "expression": "filename", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_processing_status_idx": { + "name": "doc_processing_status_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "processing_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_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_id_idx": { + "name": "doc_connector_id_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "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_tag1_idx": { + "name": "doc_tag1_idx", + "columns": [ + { + "expression": "tag1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag2_idx": { + "name": "doc_tag2_idx", + "columns": [ + { + "expression": "tag2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag3_idx": { + "name": "doc_tag3_idx", + "columns": [ + { + "expression": "tag3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag4_idx": { + "name": "doc_tag4_idx", + "columns": [ + { + "expression": "tag4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag5_idx": { + "name": "doc_tag5_idx", + "columns": [ + { + "expression": "tag5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag6_idx": { + "name": "doc_tag6_idx", + "columns": [ + { + "expression": "tag6", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag7_idx": { + "name": "doc_tag7_idx", + "columns": [ + { + "expression": "tag7", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number1_idx": { + "name": "doc_number1_idx", + "columns": [ + { + "expression": "number1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number2_idx": { + "name": "doc_number2_idx", + "columns": [ + { + "expression": "number2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number3_idx": { + "name": "doc_number3_idx", + "columns": [ + { + "expression": "number3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number4_idx": { + "name": "doc_number4_idx", + "columns": [ + { + "expression": "number4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number5_idx": { + "name": "doc_number5_idx", + "columns": [ + { + "expression": "number5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_date1_idx": { + "name": "doc_date1_idx", + "columns": [ + { + "expression": "date1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_date2_idx": { + "name": "doc_date2_idx", + "columns": [ + { + "expression": "date2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean1_idx": { + "name": "doc_boolean1_idx", + "columns": [ + { + "expression": "boolean1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean2_idx": { + "name": "doc_boolean2_idx", + "columns": [ + { + "expression": "boolean2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean3_idx": { + "name": "doc_boolean3_idx", + "columns": [ + { + "expression": "boolean3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "document_knowledge_base_id_knowledge_base_id_fk": { + "name": "document_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "document", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "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": {}, + "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_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "start_offset": { + "name": "start_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "end_offset": { + "name": "end_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number1": { + "name": "number1", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number2": { + "name": "number2", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number3": { + "name": "number3", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number4": { + "name": "number4", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number5": { + "name": "number5", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "date1": { + "name": "date1", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "date2": { + "name": "date2", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "boolean1": { + "name": "boolean1", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean2": { + "name": "boolean2", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean3": { + "name": "boolean3", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "content_tsv": { + "name": "content_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"embedding\".\"content\")", + "type": "stored" + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "emb_kb_id_idx": { + "name": "emb_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_id_idx": { + "name": "emb_doc_id_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_chunk_idx": { + "name": "emb_doc_chunk_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chunk_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_model_idx": { + "name": "emb_kb_model_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_enabled_idx": { + "name": "emb_kb_enabled_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_enabled_idx": { + "name": "emb_doc_enabled_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "embedding_vector_hnsw_idx": { + "name": "embedding_vector_hnsw_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "emb_tag1_idx": { + "name": "emb_tag1_idx", + "columns": [ + { + "expression": "tag1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag2_idx": { + "name": "emb_tag2_idx", + "columns": [ + { + "expression": "tag2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag3_idx": { + "name": "emb_tag3_idx", + "columns": [ + { + "expression": "tag3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag4_idx": { + "name": "emb_tag4_idx", + "columns": [ + { + "expression": "tag4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag5_idx": { + "name": "emb_tag5_idx", + "columns": [ + { + "expression": "tag5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag6_idx": { + "name": "emb_tag6_idx", + "columns": [ + { + "expression": "tag6", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag7_idx": { + "name": "emb_tag7_idx", + "columns": [ + { + "expression": "tag7", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number1_idx": { + "name": "emb_number1_idx", + "columns": [ + { + "expression": "number1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number2_idx": { + "name": "emb_number2_idx", + "columns": [ + { + "expression": "number2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number3_idx": { + "name": "emb_number3_idx", + "columns": [ + { + "expression": "number3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number4_idx": { + "name": "emb_number4_idx", + "columns": [ + { + "expression": "number4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number5_idx": { + "name": "emb_number5_idx", + "columns": [ + { + "expression": "number5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_date1_idx": { + "name": "emb_date1_idx", + "columns": [ + { + "expression": "date1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_date2_idx": { + "name": "emb_date2_idx", + "columns": [ + { + "expression": "date2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean1_idx": { + "name": "emb_boolean1_idx", + "columns": [ + { + "expression": "boolean1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean2_idx": { + "name": "emb_boolean2_idx", + "columns": [ + { + "expression": "boolean2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean3_idx": { + "name": "emb_boolean3_idx", + "columns": [ + { + "expression": "boolean3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_content_fts_idx": { + "name": "emb_content_fts_idx", + "columns": [ + { + "expression": "content_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "embedding_knowledge_base_id_knowledge_base_id_fk": { + "name": "embedding_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "embedding", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "embedding_document_id_document_id_fk": { + "name": "embedding_document_id_document_id_fk", + "tableFrom": "embedding", + "tableTo": "document", + "columnsFrom": ["document_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "embedding_not_null_check": { + "name": "embedding_not_null_check", + "value": "\"embedding\" IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.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 + }, + "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 + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "embedding_dimension": { + "name": "embedding_dimension", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1536 + }, + "chunking_config": { + "name": "chunking_config", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{\"maxSize\": 1024, \"minSize\": 1, \"overlap\": 200}'" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kb_user_id_idx": { + "name": "kb_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_id_idx": { + "name": "kb_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_user_workspace_idx": { + "name": "kb_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_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": {} + } + }, + "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_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": {}, + "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 + }, + "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 + }, + "next_sync_at": { + "name": "next_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "consecutive_failures": { + "name": "consecutive_failures", + "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()" + }, + "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": {} + } + }, + "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" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "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_failed": { + "name": "docs_failed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "kcsl_connector_id_idx": { + "name": "kcsl_connector_id_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "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.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": true + }, + "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" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_servers": { + "name": "mcp_servers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "transport": { + "name": "transport", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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_workspace_enabled_idx": { + "name": "mcp_servers_workspace_enabled_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_servers_workspace_deleted_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_created_by_user_id_fk": { + "name": "mcp_servers_created_by_user_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.member": { + "name": "member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "member_user_id_unique": { + "name": "member_user_id_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "member_organization_id_idx": { + "name": "member_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.memory": { + "name": "memory", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "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": { + "mothership_settings_workspace_id_idx": { + "name": "mothership_settings_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "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.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.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": true + }, + "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 + }, + "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_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_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": {}, + "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 + } + }, + "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_user_entity_idx": { + "name": "permissions_user_entity_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_unique_constraint": { + "name": "permissions_unique_constraint", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permissions_user_id_user_id_fk": { + "name": "permissions_user_id_user_id_fk", + "tableFrom": "permissions", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.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 + }, + "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": true + }, + "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_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_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": {}, + "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.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": {} + }, + "session_token_idx": { + "name": "session_token_idx", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_active_organization_id_organization_id_fk": { + "name": "session_active_organization_id_organization_id_fk", + "tableFrom": "session", + "tableTo": "organization", + "columnsFrom": ["active_organization_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'system'" + }, + "auto_connect": { + "name": "auto_connect", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "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 + } + }, + "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 + } + }, + "indexes": { + "subscription_reference_status_idx": { + "name": "subscription_reference_status_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "check_enterprise_metadata": { + "name": "check_enterprise_metadata", + "value": "plan != 'enterprise' OR metadata IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.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 + }, + "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" + } + }, + "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 + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "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": {} + } + }, + "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" + } + }, + "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_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 + } + }, + "indexes": {}, + "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_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_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_key_idx": { + "name": "workspace_file_key_idx", + "columns": [ + { + "expression": "key", + "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_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')" + } + }, + "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 + }, + "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 + }, + "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_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_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": {}, + "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", "env_workspace", "env_personal", "service_account"] + }, + "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"] + }, + "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" + ] + }, + "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", + "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 e755c8150b7..3857d8850e8 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -2080,6 +2080,13 @@ "when": 1787276752722, "tag": "0297_eminent_sinister_six", "breakpoints": true + }, + { + "idx": 298, + "version": "7", + "when": 1787287520465, + "tag": "0298_sparkling_hemingway", + "breakpoints": true } ] } From ede2abac66e80d58a8b11a03309225a0149874bc Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Thu, 20 Aug 2026 21:46:35 -0700 Subject: [PATCH 5/6] test(resource-policies): cover actor grant resolution --- .../application/authorization.test.ts | 60 +++++++++++++++++-- 1 file changed, 54 insertions(+), 6 deletions(-) diff --git a/apps/sim/lib/credential-groups/application/authorization.test.ts b/apps/sim/lib/credential-groups/application/authorization.test.ts index c07b7c6b032..3b06f02ba52 100644 --- a/apps/sim/lib/credential-groups/application/authorization.test.ts +++ b/apps/sim/lib/credential-groups/application/authorization.test.ts @@ -5,6 +5,7 @@ import type { WorkflowExecutionDelegatedPrincipal } from '@sim/auth/principal' import { beforeEach, describe, expect, it, vi } from 'vitest' const mocks = vi.hoisted(() => ({ + findPolicyGrant: vi.fn(), loadEnrollmentAccess: vi.fn(), })) @@ -12,7 +13,18 @@ vi.mock('@/lib/credential-groups/credentials', () => ({ loadCredentialGroupEnrollmentAccessForSubject: mocks.loadEnrollmentAccess, })) -import { requireCredentialGroupEnrollmentAccess } from '@/lib/credential-groups/application/authorization' +vi.mock('@/lib/resource-policies/authorization', () => ({ + findResourcePolicyGrant: mocks.findPolicyGrant, +})) + +import { requireCredentialGroupCredentialAccess } from '@/lib/credential-groups/application/authorization' + +const context = { + workspaceId: 'workspace-1', + workspaceOrganizationId: null, + allowPersonalApiKeys: true, + credentialGroupId: 'group-1', +} function executorPrincipal(): WorkflowExecutionDelegatedPrincipal { return { @@ -44,9 +56,10 @@ function executorPrincipal(): WorkflowExecutionDelegatedPrincipal { } } -describe('requireCredentialGroupEnrollmentAccess', () => { +describe('requireCredentialGroupCredentialAccess', () => { beforeEach(() => { vi.clearAllMocks() + mocks.findPolicyGrant.mockResolvedValue(null) mocks.loadEnrollmentAccess.mockResolvedValue({ enrollmentId: 'enrollment-1', email: 'person@example.com', @@ -56,7 +69,14 @@ describe('requireCredentialGroupEnrollmentAccess', () => { it('resolves an external workflow actor to their enrollment', async () => { const principal = executorPrincipal() - await expect(requireCredentialGroupEnrollmentAccess(principal, 'group-1')).resolves.toEqual({ + await expect( + requireCredentialGroupCredentialAccess( + principal, + context, + 'credential_groups.credentials.use' + ) + ).resolves.toEqual({ + scope: 'enrollment', enrollmentId: 'enrollment-1', email: 'person@example.com', }) @@ -78,10 +98,14 @@ describe('requireCredentialGroupEnrollmentAccess', () => { } await expect( - requireCredentialGroupEnrollmentAccess(principal, 'group-1') + requireCredentialGroupCredentialAccess( + principal, + context, + 'credential_groups.credentials.use' + ) ).rejects.toMatchObject({ code: 'forbidden', - message: 'Credential Group enrollment access required', + message: 'Credential Group actor access required', }) expect(mocks.loadEnrollmentAccess).not.toHaveBeenCalled() }) @@ -96,8 +120,32 @@ describe('requireCredentialGroupEnrollmentAccess', () => { } await expect( - requireCredentialGroupEnrollmentAccess(principal, 'group-1') + requireCredentialGroupCredentialAccess( + principal, + context, + 'credential_groups.credentials.use' + ) ).rejects.toMatchObject({ code: 'forbidden' }) expect(mocks.loadEnrollmentAccess).not.toHaveBeenCalled() }) + + it('allows an actorless workflow when an explicit policy grant matches', async () => { + const principal = executorPrincipal() + principal.delegationContext!.principal = { + kind: 'system', + serviceId: 'schedule', + workspaceId: 'workspace-1', + workflowId: 'workflow-1', + } + mocks.findPolicyGrant.mockResolvedValue({ id: 'grant-1' }) + + await expect( + requireCredentialGroupCredentialAccess( + principal, + context, + 'credential_groups.credentials.use' + ) + ).resolves.toEqual({ scope: 'all', grantId: 'grant-1' }) + expect(mocks.loadEnrollmentAccess).not.toHaveBeenCalled() + }) }) From fab4e470936f17642518a37fff505c19036dd10e Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Sun, 23 Aug 2026 13:08:33 -0700 Subject: [PATCH 6/6] feat(resource-policies): add statement evaluator --- .../[groupId]/access/route.test.ts | 73 +- .../[groupId]/access/route.ts | 4 +- .../credential-group-access.test.tsx | 157 +++++ .../components/credential-group-access.tsx | 365 ++++------ .../components/credential-group-detail.tsx | 32 +- .../api/contracts/credential-groups.test.ts | 55 +- .../lib/api/contracts/credential-groups.ts | 28 +- .../application/authorization.test.ts | 97 +-- .../application/authorization.ts | 58 +- .../application/list-credentials.test.ts | 6 - .../application/manage-access.test.ts | 145 ++-- .../application/manage-access.ts | 111 ++-- .../sim/lib/credential-groups/service.test.ts | 87 ++- apps/sim/lib/credential-groups/service.ts | 73 +- .../resolve-managed-oauth-token.test.ts | 29 +- .../resolve-managed-oauth-token.ts | 8 +- .../resource-policies/authorization.test.ts | 234 ++++--- .../lib/resource-policies/authorization.ts | 201 ++++-- apps/sim/lib/resource-policies/defaults.ts | 41 ++ .../lib/resource-policies/evaluator.test.ts | 162 +++++ apps/sim/lib/resource-policies/evaluator.ts | 179 +++++ apps/sim/lib/resource-policies/management.ts | 51 +- apps/sim/lib/resource-policies/registry.ts | 114 ++++ .../lib/resource-policies/repository.test.ts | 153 +++++ apps/sim/lib/resource-policies/repository.ts | 124 ++-- apps/sim/lib/resource-policies/types.test.ts | 182 ++++- apps/sim/lib/resource-policies/types.ts | 313 +++++++-- design/credential-group-authorization.md | 248 ++++--- design/resource-policies.md | 370 ++++++----- .../migrations/0298_sparkling_hemingway.sql | 47 +- packages/db/schema.ts | 2 +- ...rations-paused-billing-attribution.test.ts | 1 + ...credential_group_resource_policies.test.ts | 287 ++++++++ ...fill_credential_group_resource_policies.ts | 624 ++++++++++++++++++ packages/db/script-migrations/index.ts | 2 + packages/testing/src/mocks/schema.mock.ts | 12 + 36 files changed, 3551 insertions(+), 1124 deletions(-) create mode 100644 apps/sim/ee/credential-groups/components/credential-group-access.test.tsx create mode 100644 apps/sim/lib/resource-policies/defaults.ts create mode 100644 apps/sim/lib/resource-policies/evaluator.test.ts create mode 100644 apps/sim/lib/resource-policies/evaluator.ts create mode 100644 apps/sim/lib/resource-policies/registry.ts create mode 100644 apps/sim/lib/resource-policies/repository.test.ts create mode 100644 packages/db/script-migrations/0006_backfill_credential_group_resource_policies.test.ts create mode 100644 packages/db/script-migrations/0006_backfill_credential_group_resource_policies.ts diff --git a/apps/sim/app/api/workspaces/[id]/credential-groups/[groupId]/access/route.test.ts b/apps/sim/app/api/workspaces/[id]/credential-groups/[groupId]/access/route.test.ts index 5dc109ea52a..336cdad0a5b 100644 --- a/apps/sim/app/api/workspaces/[id]/credential-groups/[groupId]/access/route.test.ts +++ b/apps/sim/app/api/workspaces/[id]/credential-groups/[groupId]/access/route.test.ts @@ -30,6 +30,19 @@ const WORKSPACE_ID = '11111111-1111-4111-8111-111111111111' const GROUP_ID = 'group-1' const url = `http://localhost:3000/api/workspaces/${WORKSPACE_ID}/credential-groups/${GROUP_ID}/access` const context = { params: Promise.resolve({ id: WORKSPACE_ID, groupId: GROUP_ID }) } +const document = { + version: 1, + resource: { type: 'credential_group', id: GROUP_ID }, + statements: [ + { + sid: 'WorkflowAccess', + effect: 'allow', + actions: ['credential_groups.credentials.use'], + principals: [{ type: 'workflow', workflowId: 'workflow-1' }], + condition: { StringEquals: { 'sim:WorkflowMode': 'deployment' } }, + }, + ], +} describe('Credential Group access route', () => { beforeEach(() => { @@ -38,24 +51,16 @@ describe('Credential Group access route', () => { user: { id: 'admin-1' }, session: { id: 'session-1' }, }) - mocks.read.mockResolvedValue({ revision: 0, grants: [] }) - mocks.update.mockResolvedValue({ - revision: 1, - grants: [ - { - id: 'grant-1', - subject: { type: 'workflow', workflowId: 'workflow-1' }, - }, - ], - }) + mocks.read.mockResolvedValue({ revision: 1, document }) + mocks.update.mockResolvedValue({ revision: 2, document }) }) - it('reads the managed policy without exposing the built-in actor rule', async () => { + it('reads the exact managed policy without exposing the built-in actor rule', async () => { const request = new NextRequest(url) const response = await GET(request, context) expect(response.status).toBe(200) - expect(await response.json()).toEqual({ revision: 0, grants: [] }) + expect(await response.json()).toEqual({ revision: 1, document }) expect(mocks.read).toHaveBeenCalledWith({ principal: { kind: 'session', userId: 'admin-1', sessionId: 'session-1' }, input: { assertedWorkspaceId: WORKSPACE_ID, credentialGroupId: GROUP_ID }, @@ -63,11 +68,8 @@ describe('Credential Group access route', () => { }) }) - it('updates exact managed subjects with optimistic revision input', async () => { - const body = { - expectedRevision: 0, - grants: [{ subject: { type: 'workflow', workflowId: 'workflow-1' } }], - } + it('updates the full allow/deny policy document with optimistic revision input', async () => { + const body = { expectedRevision: 1, document } const request = new NextRequest(url, { method: 'PUT', body: JSON.stringify(body), @@ -101,18 +103,20 @@ describe('Credential Group access route', () => { expect(mocks.update).not.toHaveBeenCalled() }) - it('rejects caller-supplied effects and actions at the HTTP boundary', async () => { + it('rejects recursive conditions at the HTTP boundary', async () => { const request = new NextRequest(url, { method: 'PUT', body: JSON.stringify({ - expectedRevision: 0, - grants: [ - { - subject: { type: 'workflow', workflowId: 'workflow-1' }, - effect: 'allow', - actions: ['credential_groups.credentials.use'], - }, - ], + expectedRevision: 1, + document: { + ...document, + statements: [ + { + ...document.statements[0], + condition: { all: [{ StringEquals: { 'sim:WorkflowMode': 'deployment' } }] }, + }, + ], + }, }), headers: { 'content-type': 'application/json' }, }) @@ -122,4 +126,21 @@ describe('Credential Group access route', () => { expect(response.status).toBe(400) expect(mocks.update).not.toHaveBeenCalled() }) + + it('rejects an oversized policy before parsing it', async () => { + const body = JSON.stringify({ expectedRevision: 1, document, padding: 'x'.repeat(300_000) }) + const request = new NextRequest(url, { + method: 'PUT', + body, + headers: { + 'content-length': String(Buffer.byteLength(body)), + 'content-type': 'application/json', + }, + }) + + const response = await PUT(request, context) + + expect(response.status).toBe(413) + expect(mocks.update).not.toHaveBeenCalled() + }) }) diff --git a/apps/sim/app/api/workspaces/[id]/credential-groups/[groupId]/access/route.ts b/apps/sim/app/api/workspaces/[id]/credential-groups/[groupId]/access/route.ts index 735a2b2e608..d7bc12ee07a 100644 --- a/apps/sim/app/api/workspaces/[id]/credential-groups/[groupId]/access/route.ts +++ b/apps/sim/app/api/workspaces/[id]/credential-groups/[groupId]/access/route.ts @@ -17,6 +17,7 @@ import { createCredentialGroupInternalErrorPolicy } from '@/app/api/workspaces/[ const rateLimit = internalRateLimits.none({ reason: 'Credential Group access changes are workspace-admin control-plane operations', }) +const MAX_RESOURCE_POLICY_BODY_BYTES = 256 * 1024 export const GET = defineInternalJsonRoute({ contract: getCredentialGroupAccessContract, @@ -37,11 +38,12 @@ export const PUT = defineInternalJsonRoute({ operation: credentialGroupOperations.updateAccess, rateLimit, errorPolicy: createCredentialGroupInternalErrorPolicy('Failed to update Credential Group access'), + parseOptions: { maxBodyBytes: MAX_RESOURCE_POLICY_BODY_BYTES }, mapInput: ({ params, body }) => ({ assertedWorkspaceId: params.id, credentialGroupId: params.groupId, expectedRevision: body.expectedRevision, - grants: body.grants, + document: body.document, }), useCase: updateCredentialGroupAccess, }) diff --git a/apps/sim/ee/credential-groups/components/credential-group-access.test.tsx b/apps/sim/ee/credential-groups/components/credential-group-access.test.tsx new file mode 100644 index 00000000000..6e940bad767 --- /dev/null +++ b/apps/sim/ee/credential-groups/components/credential-group-access.test.tsx @@ -0,0 +1,157 @@ +/** + * @vitest-environment jsdom + */ +import { act } from 'react' +import { createRoot, type Root } from 'react-dom/client' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import type { ResourcePolicyDocument } from '@/lib/resource-policies/types' + +const mocks = vi.hoisted(() => ({ + mutationError: null as Error | null, + mutateAsync: vi.fn(), + reset: vi.fn(), + toastError: vi.fn(), + toastSuccess: vi.fn(), + useAccess: vi.fn(), +})) + +vi.mock('@sim/emcn', () => ({ + ChipTextarea: () => null, + Info: () => null, + Label: () => null, + toast: { error: mocks.toastError, success: mocks.toastSuccess }, +})) + +vi.mock('@/hooks/queries/credential-groups', () => ({ + useCredentialGroupAccess: mocks.useAccess, + useUpdateCredentialGroupAccess: () => ({ + error: mocks.mutationError, + isPending: false, + mutateAsync: mocks.mutateAsync, + reset: mocks.reset, + }), +})) + +import { useCredentialGroupAccessEditor } from '@/ee/credential-groups/components/credential-group-access' + +const GROUP_ID = 'group-1' +const EMPTY_DOCUMENT: ResourcePolicyDocument = { + version: 1, + resource: { type: 'credential_group', id: GROUP_ID }, + statements: [], +} +const EDITED_DOCUMENT: ResourcePolicyDocument = { + ...EMPTY_DOCUMENT, + statements: [ + { + sid: 'SupportWorkflow', + effect: 'allow', + actions: ['credential_groups.credentials.use'], + principals: [{ type: 'workflow', workflowId: 'workflow-1' }], + }, + ], +} + +const mountedRoots: Root[] = [] + +function renderEditor() { + ;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true + const container = document.createElement('div') + const root = createRoot(container) + mountedRoots.push(root) + let result: ReturnType | undefined + + function Probe() { + result = useCredentialGroupAccessEditor({ workspaceId: 'workspace-1', groupId: GROUP_ID }) + return null + } + + const render = () => { + act(() => root.render()) + } + render() + + return { + getResult: () => { + if (!result) throw new Error('Editor hook did not render') + return result + }, + rerender: render, + } +} + +beforeEach(() => { + vi.clearAllMocks() + mocks.mutationError = null + mocks.reset.mockImplementation(() => { + mocks.mutationError = null + }) + mocks.useAccess.mockReturnValue({ + data: { revision: 3, document: EMPTY_DOCUMENT }, + error: null, + isPending: false, + }) + mocks.mutateAsync.mockResolvedValue({ revision: 4, document: EDITED_DOCUMENT }) +}) + +afterEach(() => { + act(() => { + for (const root of mountedRoots.splice(0)) root.unmount() + }) +}) + +describe('Credential Group access editor', () => { + it('pretty-prints the policy and rejects invalid JSON before mutation', async () => { + const editor = renderEditor() + + expect(editor.getResult().value).toBe(JSON.stringify(EMPTY_DOCUMENT, null, 2)) + expect(editor.getResult().dirty).toBe(false) + + act(() => editor.getResult().setValue('{')) + + expect(editor.getResult().dirty).toBe(true) + expect(editor.getResult().validationError).toBe('Policy must be valid JSON') + await act(async () => editor.getResult().save()) + expect(mocks.mutateAsync).not.toHaveBeenCalled() + }) + + it('pins the revision and preserves the draft when a concurrent update conflicts', async () => { + const editor = renderEditor() + const editedValue = JSON.stringify(EDITED_DOCUMENT, null, 2) + act(() => editor.getResult().setValue(editedValue)) + + mocks.useAccess.mockReturnValue({ + data: { revision: 4, document: EMPTY_DOCUMENT }, + error: null, + isPending: false, + }) + const conflict = new Error('Resource policy changed while being edited') + mocks.mutateAsync.mockImplementation(async () => { + mocks.mutationError = conflict + throw conflict + }) + editor.rerender() + + await act(async () => editor.getResult().save()) + editor.rerender() + + expect(mocks.mutateAsync).toHaveBeenCalledWith({ + workspaceId: 'workspace-1', + groupId: GROUP_ID, + body: { expectedRevision: 3, document: EDITED_DOCUMENT }, + }) + expect(editor.getResult().value).toBe(editedValue) + expect(editor.getResult().dirty).toBe(true) + expect(editor.getResult().error).toBe('Resource policy changed while being edited') + }) + + it('discards the local draft back to the query document', () => { + const editor = renderEditor() + act(() => editor.getResult().setValue(JSON.stringify(EDITED_DOCUMENT, null, 2))) + + act(() => editor.getResult().discard()) + + expect(editor.getResult().dirty).toBe(false) + expect(editor.getResult().value).toBe(JSON.stringify(EMPTY_DOCUMENT, null, 2)) + }) +}) diff --git a/apps/sim/ee/credential-groups/components/credential-group-access.tsx b/apps/sim/ee/credential-groups/components/credential-group-access.tsx index e590832146c..4a693f78029 100644 --- a/apps/sim/ee/credential-groups/components/credential-group-access.tsx +++ b/apps/sim/ee/credential-groups/components/credential-group-access.tsx @@ -1,279 +1,182 @@ 'use client' import { useState } from 'react' -import { - Chip, - ChipModal, - ChipModalBody, - ChipModalError, - ChipModalField, - ChipModalFooter, - ChipModalHeader, - toast, -} from '@sim/emcn' -import { Plus, ShieldCheck } from '@sim/emcn/icons' +import { ChipTextarea, toast } from '@sim/emcn' import { getErrorMessage } from '@sim/utils/errors' -import type { CredentialGroupAccessGrant } from '@/lib/api/contracts/credential-groups' -import type { ResourcePolicySubject } from '@/lib/resource-policies/types' -import { RowActionsMenu } from '@/app/workspace/[workspaceId]/settings/components/row-actions-menu' +import type { ResourcePolicyDocument } from '@/lib/resource-policies/types' +import { resourcePolicyDocumentSchema } from '@/lib/resource-policies/types' import { SettingsEmptyState } from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state' -import { - RESOURCE_LIST_STACK, - SettingsResourceRow, -} from '@/app/workspace/[workspaceId]/settings/components/settings-resource-row' import { SettingsSection } from '@/app/workspace/[workspaceId]/settings/components/settings-section/settings-section' -import { - usePermissionGroups, - useUserPermissionConfig, -} from '@/ee/access-control/hooks/permission-groups' +import { SettingRow } from '@/ee/components/setting-row' import { useCredentialGroupAccess, useUpdateCredentialGroupAccess, } from '@/hooks/queries/credential-groups' -import { useWorkflows } from '@/hooks/queries/workflows' -interface CredentialGroupAccessProps { +interface PolicyDraft { + value: string + baseline: string + expectedRevision: number +} + +interface PolicyValidation { + document: ResourcePolicyDocument | null + error: string | null +} + +interface UseCredentialGroupAccessEditorProps { workspaceId: string groupId: string } -type ManagedSubjectType = 'workflow' | 'access_control_group' | 'workspace_role' - -const SUBJECT_TYPE_OPTIONS = [ - { value: 'workflow', label: 'Workflow' }, - { value: 'access_control_group', label: 'Access Control Group' }, - { value: 'workspace_role', label: 'Workspace role' }, -] as const +function serializePolicy(document: ResourcePolicyDocument): string { + return JSON.stringify(document, null, 2) +} -const WORKSPACE_ROLE_OPTIONS = [ - { value: 'read', label: 'Readers and above' }, - { value: 'write', label: 'Writers and admins' }, - { value: 'admin', label: 'Admins only' }, -] as const +function validatePolicy(value: string, groupId: string): PolicyValidation { + let input: unknown + try { + input = JSON.parse(value) as unknown + } catch { + return { document: null, error: 'Policy must be valid JSON' } + } -function subjectKey(subject: ResourcePolicySubject): string { - return JSON.stringify(subject) + const parsed = resourcePolicyDocumentSchema.safeParse(input) + if (!parsed.success) { + const issue = parsed.error.issues[0] + const path = issue?.path.join('.') + return { + document: null, + error: issue ? `${path ? `${path}: ` : ''}${issue.message}` : 'Policy is invalid', + } + } + if (parsed.data.resource.type !== 'credential_group' || parsed.data.resource.id !== groupId) { + return { + document: null, + error: 'Policy resource must match this Credential Group', + } + } + return { document: parsed.data, error: null } } -export function CredentialGroupAccess({ workspaceId, groupId }: CredentialGroupAccessProps) { +export function useCredentialGroupAccessEditor({ + workspaceId, + groupId, +}: UseCredentialGroupAccessEditorProps) { const access = useCredentialGroupAccess(workspaceId, groupId) const updateAccess = useUpdateCredentialGroupAccess() - const workflows = useWorkflows(workspaceId) - const permissionConfig = useUserPermissionConfig(workspaceId) - const permissionGroups = usePermissionGroups(permissionConfig.data?.organizationId ?? undefined) - const [showAdd, setShowAdd] = useState(false) - const [subjectType, setSubjectType] = useState('workflow') - const [targetId, setTargetId] = useState('') - const [modalError, setModalError] = useState(null) - - const policy = access.data - const applicablePermissionGroups = (permissionGroups.data ?? []).filter( - (group) => group.isDefault || group.workspaces.some((workspace) => workspace.id === workspaceId) - ) - const targetOptions = - subjectType === 'workflow' - ? (workflows.data ?? []).map((workflow) => ({ - value: workflow.id, - label: `${workflow.name}${workflow.isDeployed ? '' : ' (not deployed)'}`, - })) - : subjectType === 'access_control_group' - ? applicablePermissionGroups.map((group) => ({ value: group.id, label: group.name })) - : [...WORKSPACE_ROLE_OPTIONS] - const targetOptionsPending = - subjectType === 'workflow' - ? workflows.isPending - : subjectType === 'access_control_group' - ? permissionConfig.isPending || permissionGroups.isPending - : false - const targetPlaceholder = targetOptionsPending - ? `Loading ${subjectType === 'workflow' ? 'workflows' : 'access control groups'}...` - : targetOptions.length === 0 - ? `No ${subjectType === 'workflow' ? 'workflows' : 'access control groups'} available` - : 'Select one' - - const labelForSubject = (subject: ResourcePolicySubject): string => { - switch (subject.type) { - case 'workflow': - return ( - workflows.data?.find((workflow) => workflow.id === subject.workflowId)?.name ?? - subject.workflowId - ) - case 'access_control_group': - return ( - permissionGroups.data?.find((group) => group.id === subject.accessControlGroupId)?.name ?? - subject.accessControlGroupId - ) - case 'workspace_role': - return ( - WORKSPACE_ROLE_OPTIONS.find((role) => role.value === subject.minimumRole)?.label ?? - subject.minimumRole - ) - case 'user': - return `User ${subject.userId}` - case 'external_identity': - return `${subject.provider} user ${subject.subjectId}` - } + const [draft, setDraft] = useState(null) + + const persistedValue = access.data ? serializePolicy(access.data.document) : '' + const value = draft?.value ?? persistedValue + const validation = access.data ? validatePolicy(value, groupId) : { document: null, error: null } + const dirty = draft !== null && draft.value !== draft.baseline + + const setValue = (nextValue: string) => { + if (!access.data) return + updateAccess.reset() + setDraft((current) => { + const baseline = current?.baseline ?? persistedValue + if (nextValue === baseline) return null + return { + value: nextValue, + baseline, + expectedRevision: current?.expectedRevision ?? access.data.revision, + } + }) } - const descriptionForSubject = (subject: ResourcePolicySubject): string => { - if (subject.type === 'workflow') return 'Use all credentials in deployed runs' - return 'Use all credentials in this group' + const discard = () => { + setDraft(null) + updateAccess.reset() } - const saveGrants = async ( - grants: Array<{ id?: string; subject: ResourcePolicySubject }> - ): Promise => { - if (!policy) return false + const save = async () => { + if (!draft || !validation.document) return try { await updateAccess.mutateAsync({ workspaceId, groupId, - body: { expectedRevision: policy.revision, grants }, + body: { + expectedRevision: draft.expectedRevision, + document: validation.document, + }, }) - return true + setDraft(null) + toast.success('Access policy saved') } catch (error) { - toast.error(getErrorMessage(error, 'Could not update access')) - return false + const message = getErrorMessage(error, 'Could not update access policy') + toast.error(message) } } - const handleAdd = async () => { - if (!targetId || !policy) return - let subject: ResourcePolicySubject - if (subjectType === 'workflow') { - subject = { type: 'workflow', workflowId: targetId } - } else if (subjectType === 'access_control_group') { - subject = { type: 'access_control_group', accessControlGroupId: targetId } - } else { - subject = { - type: 'workspace_role', - minimumRole: targetId as 'read' | 'write' | 'admin', - } - } - if (policy.grants.some((grant) => subjectKey(grant.subject) === subjectKey(subject))) { - setModalError('This subject already has access') - return - } - if ( - await saveGrants([ - ...policy.grants.map((grant) => ({ id: grant.id, subject: grant.subject })), - { subject }, - ]) - ) { - setShowAdd(false) - setTargetId('') - setModalError(null) - toast.success('Access added') - } + return { + value, + setValue, + discard, + save, + dirty, + validationError: validation.error, + error: + validation.error ?? + (updateAccess.error + ? getErrorMessage(updateAccess.error, 'Could not update access policy') + : null), + isPending: access.isPending && !access.data, + loadError: access.data ? null : access.error, + isReady: Boolean(access.data), + saving: updateAccess.isPending, } +} - const handleRemove = async (grantId: string) => { - if (!policy) return - const next = policy.grants - .filter((grant) => grant.id !== grantId) - .map((grant) => ({ id: grant.id, subject: grant.subject })) - if (await saveGrants(next)) toast.success('Access removed') - } +interface CredentialGroupAccessProps { + value: string + onChange: (value: string) => void + error: string | null + isPending: boolean + loadError: unknown + saving: boolean +} - if (access.error) { +export function CredentialGroupAccess({ + value, + onChange, + error, + isPending, + loadError, + saving, +}: CredentialGroupAccessProps) { + if (loadError) { return ( - {getErrorMessage(access.error, "Couldn't load access")} + {getErrorMessage(loadError, "Couldn't load access policy")} ) } - if (access.isPending || !policy) return null + if (isPending) return null return ( - <> - setShowAdd(true)}> - Add access - - } - > - {policy.grants.length === 0 ? ( - No additional access grants - ) : ( -
- {policy.grants.map((grant: CredentialGroupAccessGrant) => ( - } - iconFilled - title={labelForSubject(grant.subject)} - description={descriptionForSubject(grant.subject)} - trailing={ - void handleRemove(grant.id), - disabled: updateAccess.isPending, - }, - ]} - /> - } - /> - ))} -
- )} -
- - { - setShowAdd(open) - if (!open) { - setTargetId('') - setModalError(null) - } - }} - dismissDisabled={updateAccess.isPending} - size='sm' - srTitle='Add Credential Group access' + + - setShowAdd(false)}>Add access - - { - setSubjectType(value as ManagedSubjectType) - setTargetId('') - setModalError(null) - }} - options={SUBJECT_TYPE_OPTIONS} - /> - { - setTargetId(value) - setModalError(null) - }} - options={targetOptions} - placeholder={targetPlaceholder} - disabled={targetOptionsPending || targetOptions.length === 0} - /> - {modalError} - - setShowAdd(false)} - cancelDisabled={updateAccess.isPending} - primaryAction={{ - label: updateAccess.isPending ? 'Adding...' : 'Add access', - onClick: () => void handleAdd(), - disabled: !targetId || updateAccess.isPending, - }} + onChange(event.target.value)} + error={Boolean(error)} + rows={18} + resizable + spellCheck={false} + autoCapitalize='off' + autoCorrect='off' + readOnly={saving} + className='font-mono' /> - - + + ) } diff --git a/apps/sim/ee/credential-groups/components/credential-group-detail.tsx b/apps/sim/ee/credential-groups/components/credential-group-detail.tsx index 08e5db4a0ff..fb20dd41708 100644 --- a/apps/sim/ee/credential-groups/components/credential-group-detail.tsx +++ b/apps/sim/ee/credential-groups/components/credential-group-detail.tsx @@ -28,7 +28,10 @@ import { } from '@/app/workspace/[workspaceId]/settings/components/settings-resource-row' import { SettingsSection } from '@/app/workspace/[workspaceId]/settings/components/settings-section/settings-section' import { useSettingsUnsavedGuard } from '@/app/workspace/[workspaceId]/settings/hooks/use-settings-unsaved-guard' -import { CredentialGroupAccess } from '@/ee/credential-groups/components/credential-group-access' +import { + CredentialGroupAccess, + useCredentialGroupAccessEditor, +} from '@/ee/credential-groups/components/credential-group-access' import { CredentialGroupDetails } from '@/ee/credential-groups/components/credential-group-details' import { CredentialGroupInviteModal } from '@/ee/credential-groups/components/credential-group-invite-modal' import { @@ -99,6 +102,7 @@ export function CredentialGroupDetail({ const deleteEnrollment = useDeleteCredentialGroupEnrollment() const updateGroup = useUpdateCredentialGroup() const deleteGroup = useDeleteCredentialGroup() + const accessEditor = useCredentialGroupAccessEditor({ workspaceId, groupId }) const [activeTab, setActiveTab] = useQueryState(credentialGroupTabParam.key, { ...credentialGroupTabParam.parser, ...credentialGroupTabUrlKeys, @@ -130,7 +134,7 @@ export function CredentialGroupDetail({ (name.trim() !== credentialGroup.name || normalizedDescription !== credentialGroup.description) ) - const guard = useSettingsUnsavedGuard({ isDirty: detailsDirty }) + const guard = useSettingsUnsavedGuard({ isDirty: detailsDirty || accessEditor.dirty }) const discardDetails = () => { setDraftName(null) @@ -152,10 +156,6 @@ export function CredentialGroupDetail({ } } - /** - * Each tab owns its own primary action: Details commits the edited name and - * description, People invites more users. Delete is available from both. - */ const actions: SettingsAction[] = credentialGroup ? [ ...(activeTab === 'details' @@ -177,7 +177,16 @@ export function CredentialGroupDetail({ disabled: credentialGroup.status !== 'active' || !configurationReady, }, ] - : []), + : saveDiscardActions({ + dirty: accessEditor.dirty, + saving: accessEditor.saving, + onSave: () => void accessEditor.save(), + onDiscard: accessEditor.discard, + saveDisabled: Boolean(accessEditor.validationError) || !accessEditor.isReady, + saveTooltip: + accessEditor.validationError ?? + (!accessEditor.isReady ? 'Access policy is unavailable' : undefined), + })), { id: 'delete', text: deleteGroup.isPending ? 'Deleting...' : 'Delete', @@ -312,7 +321,14 @@ export function CredentialGroupDetail({ )} {activeTab === 'access' && ( - + )} )} diff --git a/apps/sim/lib/api/contracts/credential-groups.test.ts b/apps/sim/lib/api/contracts/credential-groups.test.ts index 4d94979e7df..25fa07a6173 100644 --- a/apps/sim/lib/api/contracts/credential-groups.test.ts +++ b/apps/sim/lib/api/contracts/credential-groups.test.ts @@ -198,36 +198,51 @@ describe('credential group contracts', () => { expect(result.connections).toEqual([{ provider: 'gmail', status: 'active', count: 2 }]) }) - it('accepts managed access subjects without exposing policy actions', () => { + it('accepts the complete resource policy document', () => { const result = updateCredentialGroupAccessBodySchema.parse({ expectedRevision: 3, - grants: [ - { subject: { type: 'workflow', workflowId: 'workflow-1' } }, - { subject: { type: 'workspace_role', minimumRole: 'admin' } }, - { - subject: { - type: 'access_control_group', - accessControlGroupId: 'permission-group-1', + document: { + version: 1, + resource: { type: 'credential_group', id: 'group-1' }, + statements: [ + { + sid: 'WorkflowAccess', + effect: 'allow', + actions: ['credential_groups.credentials.use'], + principals: [{ type: 'workflow', workflowId: 'workflow-1' }], + condition: { StringEquals: { 'sim:WorkflowMode': 'deployment' } }, }, - }, - ], + { + sid: 'BlockedUser', + effect: 'deny', + actions: ['credential_groups.credentials.use'], + principals: [{ type: 'user', userId: 'user-1' }], + }, + ], + }, }) - expect(result.grants).toHaveLength(3) - expect(result.grants[0]).not.toHaveProperty('actions') + expect(result.document.statements).toHaveLength(2) + expect(result.document.statements[1].effect).toBe('deny') }) - it('rejects caller-supplied effects and actions', () => { + it('rejects revision zero and recursive conditions', () => { expect( updateCredentialGroupAccessBodySchema.safeParse({ expectedRevision: 0, - grants: [ - { - subject: { type: 'workflow', workflowId: 'workflow-1' }, - effect: 'deny', - actions: ['credential_groups.credentials.use'], - }, - ], + document: { + version: 1, + resource: { type: 'credential_group', id: 'group-1' }, + statements: [ + { + sid: 'Recursive', + effect: 'allow', + actions: ['credential_groups.credentials.use'], + principals: [{ type: 'any' }], + condition: { all: [] }, + }, + ], + }, }).success ).toBe(false) }) diff --git a/apps/sim/lib/api/contracts/credential-groups.ts b/apps/sim/lib/api/contracts/credential-groups.ts index ba0ed60a9b8..495b656fc9f 100644 --- a/apps/sim/lib/api/contracts/credential-groups.ts +++ b/apps/sim/lib/api/contracts/credential-groups.ts @@ -5,7 +5,7 @@ import { CREDENTIAL_GROUP_PROVIDER_IDS, CREDENTIAL_GROUP_STANDARD_OAUTH_PROVIDER_IDS, } from '@/lib/credential-groups/providers' -import { resourcePolicySubjectSchema } from '@/lib/resource-policies/types' +import { resourcePolicyDocumentSchema } from '@/lib/resource-policies/types' export const credentialGroupProviderSchema = z.enum(CREDENTIAL_GROUP_PROVIDER_IDS) export const credentialGroupStatusSchema = z.enum(['active', 'disabled']) @@ -116,19 +116,10 @@ export type CredentialGroupEnrollmentConnection = z.output< > export type CredentialGroupEnrollmentDetail = z.output -export const credentialGroupAccessGrantSchema = z - .object({ - id: z.string().min(1).max(128), - subject: resourcePolicySubjectSchema, - }) - .strict() - -export type CredentialGroupAccessGrant = z.output - export const credentialGroupAccessPolicySchema = z .object({ - revision: z.number().int().nonnegative(), - grants: z.array(credentialGroupAccessGrantSchema).max(100), + revision: z.number().int().positive(), + document: resourcePolicyDocumentSchema, }) .strict() @@ -136,17 +127,8 @@ export type CredentialGroupAccessPolicy = z.output ({ - findPolicyGrant: vi.fn(), + evaluatePolicy: vi.fn(), loadEnrollmentAccess: vi.fn(), })) @@ -14,7 +14,7 @@ vi.mock('@/lib/credential-groups/credentials', () => ({ })) vi.mock('@/lib/resource-policies/authorization', () => ({ - findResourcePolicyGrant: mocks.findPolicyGrant, + evaluateResourcePolicy: mocks.evaluatePolicy, })) import { requireCredentialGroupCredentialAccess } from '@/lib/credential-groups/application/authorization' @@ -24,6 +24,7 @@ const context = { workspaceOrganizationId: null, allowPersonalApiKeys: true, credentialGroupId: 'group-1', + credentialGroupEnrollmentId: 'enrollment-1', } function executorPrincipal(): WorkflowExecutionDelegatedPrincipal { @@ -59,36 +60,45 @@ function executorPrincipal(): WorkflowExecutionDelegatedPrincipal { describe('requireCredentialGroupCredentialAccess', () => { beforeEach(() => { vi.clearAllMocks() - mocks.findPolicyGrant.mockResolvedValue(null) + mocks.evaluatePolicy.mockResolvedValue({ + decision: 'allow', + statementSid: 'sim:CredentialGroupActorCredential', + }) mocks.loadEnrollmentAccess.mockResolvedValue({ enrollmentId: 'enrollment-1', email: 'person@example.com', }) }) - it('resolves an external workflow actor to their enrollment', async () => { + it('adds an external actor enrollment to the trusted policy context', async () => { const principal = executorPrincipal() - await expect( - requireCredentialGroupCredentialAccess( - principal, - context, - 'credential_groups.credentials.use' - ) - ).resolves.toEqual({ - scope: 'enrollment', - enrollmentId: 'enrollment-1', - email: 'person@example.com', - }) + await requireCredentialGroupCredentialAccess( + principal, + context, + 'credential_groups.credentials.use' + ) + expect(mocks.loadEnrollmentAccess).toHaveBeenCalledWith('group-1', { kind: 'external_user', provider: 'slack', tenantId: 'T123', subjectId: 'U123', }) + expect(mocks.evaluatePolicy).toHaveBeenCalledWith({ + principal, + context, + resourceType: 'credential_group', + resourceId: 'group-1', + action: 'credential_groups.credentials.use', + resourceContext: { + 'credential_group:CredentialEnrollmentId': 'enrollment-1', + 'sim:PrincipalCredentialGroupEnrollmentId': 'enrollment-1', + }, + }) }) - it('rejects an actorless workflow principal', async () => { + it('allows an actorless workflow only when a managed policy statement allows it', async () => { const principal = executorPrincipal() principal.delegationContext!.principal = { kind: 'system', @@ -96,6 +106,7 @@ describe('requireCredentialGroupCredentialAccess', () => { workspaceId: 'workspace-1', workflowId: 'workflow-1', } + mocks.evaluatePolicy.mockResolvedValue({ decision: 'allow', statementSid: 'ScheduledWorkflow' }) await expect( requireCredentialGroupCredentialAccess( @@ -103,41 +114,51 @@ describe('requireCredentialGroupCredentialAccess', () => { context, 'credential_groups.credentials.use' ) - ).rejects.toMatchObject({ - code: 'forbidden', - message: 'Credential Group actor access required', - }) + ).resolves.toBeUndefined() expect(mocks.loadEnrollmentAccess).not.toHaveBeenCalled() + expect(mocks.evaluatePolicy).toHaveBeenCalledWith( + expect.objectContaining({ + resourceContext: { + 'credential_group:CredentialEnrollmentId': 'enrollment-1', + }, + }) + ) }) - it('rejects an executor delegation whose Sim subject does not match the workflow actor', async () => { - const principal = executorPrincipal() - principal.subjectUserId = 'user-2' - principal.delegationContext!.principal = { - kind: 'session', - userId: 'user-1', - sessionId: 'session-1', - } + it('rejects implicit and explicit policy denials', async () => { + mocks.evaluatePolicy.mockResolvedValueOnce({ decision: 'implicit_deny' }) + await expect( + requireCredentialGroupCredentialAccess( + executorPrincipal(), + context, + 'credential_groups.credentials.use' + ) + ).rejects.toMatchObject({ + code: 'forbidden', + message: 'Credential Group credential access denied', + }) + mocks.evaluatePolicy.mockResolvedValueOnce({ + decision: 'deny', + statementSid: 'BlockedActor', + }) await expect( requireCredentialGroupCredentialAccess( - principal, + executorPrincipal(), context, 'credential_groups.credentials.use' ) ).rejects.toMatchObject({ code: 'forbidden' }) - expect(mocks.loadEnrollmentAccess).not.toHaveBeenCalled() }) - it('allows an actorless workflow when an explicit policy grant matches', async () => { + it('rejects an executor delegation whose Sim subject differs from the workflow actor', async () => { const principal = executorPrincipal() + principal.subjectUserId = 'user-2' principal.delegationContext!.principal = { - kind: 'system', - serviceId: 'schedule', - workspaceId: 'workspace-1', - workflowId: 'workflow-1', + kind: 'session', + userId: 'user-1', + sessionId: 'session-1', } - mocks.findPolicyGrant.mockResolvedValue({ id: 'grant-1' }) await expect( requireCredentialGroupCredentialAccess( @@ -145,7 +166,7 @@ describe('requireCredentialGroupCredentialAccess', () => { context, 'credential_groups.credentials.use' ) - ).resolves.toEqual({ scope: 'all', grantId: 'grant-1' }) - expect(mocks.loadEnrollmentAccess).not.toHaveBeenCalled() + ).rejects.toMatchObject({ code: 'forbidden' }) + expect(mocks.evaluatePolicy).not.toHaveBeenCalled() }) }) diff --git a/apps/sim/lib/credential-groups/application/authorization.ts b/apps/sim/lib/credential-groups/application/authorization.ts index 8420db5b9dc..b31024d330d 100644 --- a/apps/sim/lib/credential-groups/application/authorization.ts +++ b/apps/sim/lib/credential-groups/application/authorization.ts @@ -9,11 +9,8 @@ import type { } from '@/lib/core/application' import { OrchestrationError } from '@/lib/core/orchestration/types' import type { CredentialGroupCredentialListContext } from '@/lib/credential-groups/credentials' -import { - type CredentialGroupEnrollmentAccess, - loadCredentialGroupEnrollmentAccessForSubject, -} from '@/lib/credential-groups/credentials' -import { findResourcePolicyGrant } from '@/lib/resource-policies/authorization' +import { loadCredentialGroupEnrollmentAccessForSubject } from '@/lib/credential-groups/credentials' +import { evaluateResourcePolicy } from '@/lib/resource-policies/authorization' import type { ResourcePolicyAction } from '@/lib/resource-policies/types' export const CREDENTIAL_GROUP_DELEGATION_AUDIENCE = 'sim:credential-groups' @@ -24,13 +21,7 @@ export interface CredentialGroupAuthorizationContext extends WorkspaceAuthorizat export interface CredentialGroupApplicationContext extends CredentialGroupAuthorizationContext, - CredentialGroupCredentialListContext { - credentialAccess?: CredentialGroupCredentialAccess -} - -export type CredentialGroupCredentialAccess = - | ({ scope: 'enrollment' } & CredentialGroupEnrollmentAccess) - | { scope: 'all'; grantId: string } + CredentialGroupCredentialListContext {} function requireWorkflowExecutionPrincipal(principal: Principal) { if (principal.kind !== 'delegated' || principal.serviceId !== 'executor') { @@ -58,37 +49,36 @@ export function requireCredentialGroupWorkflowSubject(principal: Principal): str export async function requireCredentialGroupCredentialAccess( principal: Principal, - context: CredentialGroupAuthorizationContext, + context: CredentialGroupAuthorizationContext & { credentialGroupEnrollmentId: string }, action: ResourcePolicyAction -): Promise { - const grant = await findResourcePolicyGrant({ - principal, - context, - resourceType: 'credential_group', - resourceId: context.credentialGroupId, - action, - }) - if (grant) return { scope: 'all', grantId: grant.id } - +): Promise { const executionPrincipal = requireWorkflowExecutionPrincipal(principal) const subject = resolvePrincipalSubject(executionPrincipal) - if (!subject) { - throw new OrchestrationError('forbidden', 'Credential Group actor access required') - } if ( - subject.kind === 'sim_user' && + subject?.kind === 'sim_user' && (principal.kind !== 'delegated' || principal.subjectUserId !== subject.userId) ) { throw new OrchestrationError('forbidden', 'Credential Group actor access required') } - const access = await loadCredentialGroupEnrollmentAccessForSubject( - context.credentialGroupId, - subject - ) - if (!access) { - throw new OrchestrationError('forbidden', 'Credential Group enrollment access required') + const actorAccess = subject + ? await loadCredentialGroupEnrollmentAccessForSubject(context.credentialGroupId, subject) + : null + const decision = await evaluateResourcePolicy({ + principal, + context, + resourceType: 'credential_group', + resourceId: context.credentialGroupId, + action, + resourceContext: { + 'credential_group:CredentialEnrollmentId': context.credentialGroupEnrollmentId, + ...(actorAccess + ? { 'sim:PrincipalCredentialGroupEnrollmentId': actorAccess.enrollmentId } + : {}), + }, + }) + if (decision.decision !== 'allow') { + throw new OrchestrationError('forbidden', 'Credential Group credential access denied') } - return { scope: 'enrollment', ...access } } export const credentialGroupDelegationPolicy = { 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 1b8d123dea5..32b62578b11 100644 --- a/apps/sim/lib/credential-groups/application/list-credentials.test.ts +++ b/apps/sim/lib/credential-groups/application/list-credentials.test.ts @@ -6,7 +6,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' const mocks = vi.hoisted(() => ({ getWorkspaceOwnerSubscriptionAccess: vi.fn(), - findPolicyGrant: vi.fn(), listCredentials: vi.fn(), loadEnrollmentAccess: vi.fn(), loadGroup: vi.fn(), @@ -36,10 +35,6 @@ vi.mock('@/lib/credential-groups/credentials', () => ({ MAX_CREDENTIAL_GROUP_CREDENTIAL_PAGE_SIZE: 100, })) -vi.mock('@/lib/resource-policies/authorization', () => ({ - findResourcePolicyGrant: mocks.findPolicyGrant, -})) - vi.mock('@/lib/workspaces/application/workspace-context', () => ({ loadActiveWorkspaceApplicationContext: mocks.loadWorkspace, })) @@ -116,7 +111,6 @@ describe('listCredentialGroupCredentials', () => { mocks.resolvePermission.mockResolvedValue('read') mocks.getWorkspaceOwnerSubscriptionAccess.mockResolvedValue({ isEnterprise: true }) mocks.resolveCredentialGroupsAvailability.mockResolvedValue({ available: true }) - mocks.findPolicyGrant.mockResolvedValue(null) mocks.loadEnrollmentAccess.mockResolvedValue({ enrollmentId: 'enrollment-1', email: 'person@example.com', diff --git a/apps/sim/lib/credential-groups/application/manage-access.test.ts b/apps/sim/lib/credential-groups/application/manage-access.test.ts index 4b0618a9065..a051c1e43d8 100644 --- a/apps/sim/lib/credential-groups/application/manage-access.test.ts +++ b/apps/sim/lib/credential-groups/application/manage-access.test.ts @@ -5,10 +5,10 @@ import type { SessionPrincipal } from '@sim/auth/principal' import { beforeEach, describe, expect, it, vi } from 'vitest' const mocks = vi.hoisted(() => ({ - loadPolicy: vi.fn(), + requirePolicy: vi.fn(), resolveGroup: vi.fn(), resolvePermission: vi.fn(), - validateSubjects: vi.fn(), + validatePrincipals: vi.fn(), writePolicy: vi.fn(), })) @@ -23,13 +23,15 @@ vi.mock('@sim/platform-authz/workspace', () => ({ })) vi.mock('@/lib/resource-policies/management', () => ({ - validateResourcePolicySubjects: mocks.validateSubjects, + validateResourcePolicyPrincipals: mocks.validatePrincipals, })) vi.mock('@/lib/resource-policies/repository', () => { class ResourcePolicyRevisionConflictError extends Error {} + class ResourcePolicyNotFoundError extends Error {} return { - loadResourcePolicy: mocks.loadPolicy, + requireResourcePolicy: mocks.requirePolicy, + ResourcePolicyNotFoundError, ResourcePolicyRevisionConflictError, writeResourcePolicy: mocks.writePolicy, } @@ -39,7 +41,10 @@ import { readCredentialGroupAccess, updateCredentialGroupAccess, } from '@/lib/credential-groups/application/manage-access' -import { ResourcePolicyRevisionConflictError } from '@/lib/resource-policies/repository' +import { + ResourcePolicyNotFoundError, + ResourcePolicyRevisionConflictError, +} from '@/lib/resource-policies/repository' const context = { workspaceId: 'workspace-1', @@ -60,48 +65,65 @@ const target = { assertedWorkspaceId: 'workspace-1', credentialGroupId: 'group-1', } +const document = { + version: 1 as const, + resource: { type: 'credential_group' as const, id: 'group-1' }, + statements: [ + { + sid: 'ScheduledWorkflow', + effect: 'allow' as const, + actions: ['credential_groups.credentials.use' as const], + principals: [{ type: 'workflow' as const, workflowId: 'workflow-1' }], + condition: { StringEquals: { 'sim:WorkflowMode': 'deployment' } }, + }, + { + sid: 'BlockedUser', + effect: 'deny' as const, + actions: ['credential_groups.credentials.use' as const], + principals: [{ type: 'user' as const, userId: 'user-2' }], + }, + ], +} -describe('Credential Group access policy operations', () => { +function storedPolicy(revision = 1) { + return { + id: 'policy-1', + workspaceId: 'workspace-1', + revision, + document, + createdAt: new Date('2026-08-20T00:00:00.000Z'), + updatedAt: new Date('2026-08-20T00:00:00.000Z'), + } +} + +describe('Credential Group resource policy operations', () => { beforeEach(() => { vi.clearAllMocks() mocks.resolveGroup.mockResolvedValue(context) mocks.resolvePermission.mockResolvedValue('admin') - mocks.loadPolicy.mockResolvedValue(null) - mocks.validateSubjects.mockResolvedValue(undefined) - mocks.writePolicy.mockImplementation(async ({ document }) => ({ - id: 'policy-1', - workspaceId: 'workspace-1', - revision: 2, - document, - createdAt: new Date('2026-08-20T00:00:00.000Z'), - updatedAt: new Date('2026-08-20T00:00:00.000Z'), - })) + mocks.requirePolicy.mockResolvedValue(storedPolicy()) + mocks.validatePrincipals.mockResolvedValue(undefined) + mocks.writePolicy.mockResolvedValue(storedPolicy(2)) }) - it('returns no visible grants when only the built-in self rule applies', async () => { + it('returns the exact managed policy document without the hidden system rule', async () => { await expect(readCredentialGroupAccess.execute({ principal, input: target })).resolves.toEqual({ - revision: 0, - grants: [], - }) - }) - - it('fails fast when stored policy workspace binding is corrupt', async () => { - mocks.loadPolicy.mockResolvedValue({ - id: 'policy-1', - workspaceId: 'workspace-2', revision: 1, - document: { - version: 1, - resource: { type: 'credential_group', id: 'group-1' }, - grants: [], - }, - createdAt: new Date('2026-08-20T00:00:00.000Z'), - updatedAt: new Date('2026-08-20T00:00:00.000Z'), + document, + }) + expect(mocks.requirePolicy).toHaveBeenCalledWith({ + workspaceId: 'workspace-1', + resourceType: 'credential_group', + resourceId: 'group-1', }) + }) - await expect(readCredentialGroupAccess.execute({ principal, input: target })).rejects.toThrow( - 'wrong workspace binding' + it('fails fast rather than synthesizing an empty document when policy storage is missing', async () => { + mocks.requirePolicy.mockRejectedValue( + new ResourcePolicyNotFoundError('credential_group', 'group-1') ) + + await expect(readCredentialGroupAccess.execute({ principal, input: target })).rejects.toThrow() }) it('requires current workspace-admin permission', async () => { @@ -110,47 +132,48 @@ describe('Credential Group access policy operations', () => { await expect( updateCredentialGroupAccess.execute({ principal, - input: { ...target, expectedRevision: 0, grants: [] }, + input: { ...target, expectedRevision: 1, document }, }) ).rejects.toMatchObject({ code: 'forbidden' }) expect(mocks.writePolicy).not.toHaveBeenCalled() }) - it('stores one managed grant as list-and-use access to the whole group', async () => { - const subject = { type: 'workflow' as const, workflowId: 'workflow-1' } - + it('validates every principal and persists the exact allow/deny document', async () => { const result = await updateCredentialGroupAccess.execute({ principal, - input: { - ...target, - expectedRevision: 1, - grants: [{ subject }], - }, + input: { ...target, expectedRevision: 1, document }, }) - expect(mocks.validateSubjects).toHaveBeenCalledWith([subject], context) + expect(mocks.validatePrincipals).toHaveBeenCalledWith( + [ + { type: 'workflow', workflowId: 'workflow-1' }, + { type: 'user', userId: 'user-2' }, + ], + context + ) expect(mocks.writePolicy).toHaveBeenCalledWith({ workspaceId: 'workspace-1', resourceType: 'credential_group', resourceId: 'group-1', expectedRevision: 1, actorUserId: 'admin-1', - document: { - version: 1, - resource: { type: 'credential_group', id: 'group-1' }, - grants: [ - { - id: expect.any(String), - subject, - actions: ['credential_groups.credentials.use'], - }, - ], - }, - }) - expect(result).toEqual({ - revision: 2, - grants: [{ id: expect.any(String), subject }], + document, }) + expect(result).toEqual({ revision: 2, document }) + }) + + it('rejects a document bound to another resource before validating references', async () => { + await expect( + updateCredentialGroupAccess.execute({ + principal, + input: { + ...target, + expectedRevision: 1, + document: { ...document, resource: { ...document.resource, id: 'group-2' } }, + }, + }) + ).rejects.toMatchObject({ code: 'validation' }) + expect(mocks.validatePrincipals).not.toHaveBeenCalled() }) it('maps optimistic-write conflicts to an application conflict', async () => { @@ -159,7 +182,7 @@ describe('Credential Group access policy operations', () => { await expect( updateCredentialGroupAccess.execute({ principal, - input: { ...target, expectedRevision: 1, grants: [] }, + input: { ...target, expectedRevision: 1, document }, }) ).rejects.toMatchObject({ code: 'conflict' }) }) diff --git a/apps/sim/lib/credential-groups/application/manage-access.ts b/apps/sim/lib/credential-groups/application/manage-access.ts index 5952473bf1e..d7e1f903a5a 100644 --- a/apps/sim/lib/credential-groups/application/manage-access.ts +++ b/apps/sim/lib/credential-groups/application/manage-access.ts @@ -1,45 +1,29 @@ import { AuditAction, AuditResourceType } from '@sim/audit' -import { generateId } from '@sim/utils/id' import { defineAuthorizedWorkspaceUseCase } from '@/lib/core/application' import { OrchestrationError } from '@/lib/core/orchestration/types' import { resolveCredentialGroupSettingsContext } from '@/lib/credential-groups/application/context' import { credentialGroupOperations } from '@/lib/credential-groups/application/operations' -import { validateResourcePolicySubjects } from '@/lib/resource-policies/management' +import { validateResourcePolicyPrincipals } from '@/lib/resource-policies/management' import { - loadResourcePolicy, ResourcePolicyRevisionConflictError, + requireResourcePolicy, writeResourcePolicy, } from '@/lib/resource-policies/repository' import { - RESOURCE_POLICY_VERSION, - type ResourcePolicySubject, - resourcePolicySubjectSchema, + parseResourcePolicyDocument, + type ResourcePolicyDocument, } from '@/lib/resource-policies/types' -const CREDENTIAL_GROUP_ACCESS_ACTIONS = ['credential_groups.credentials.use'] as const - interface CredentialGroupAccessTargetInput { assertedWorkspaceId: string credentialGroupId: string } -function presentPolicy( - policy: Awaited>, - expected: { credentialGroupId: string; workspaceId: string } -) { - if (!policy) { - return { revision: 0, grants: [] } - } - if (policy.workspaceId !== expected.workspaceId) { - throw new Error('Credential Group policy has the wrong workspace binding') - } - if (policy.document.resource.id !== expected.credentialGroupId) { - throw new Error('Credential Group policy has the wrong resource binding') - } - return { - revision: policy.revision, - grants: policy.document.grants.map((grant) => ({ id: grant.id, subject: grant.subject })), - } +function presentPolicy(policy: Awaited>): { + revision: number + document: ResourcePolicyDocument +} { + return { revision: policy.revision, document: policy.document } } export const readCredentialGroupAccess = defineAuthorizedWorkspaceUseCase({ @@ -48,20 +32,19 @@ export const readCredentialGroupAccess = defineAuthorizedWorkspaceUseCase({ resolveCredentialGroupSettingsContext(input.credentialGroupId, input.assertedWorkspaceId), authorizationOptions: {}, async execute({ context }) { - const policy = await loadResourcePolicy({ - resourceType: 'credential_group', - resourceId: context.credentialGroupId, - }) - return presentPolicy(policy, { - credentialGroupId: context.credentialGroupId, - workspaceId: context.workspaceId, - }) + return presentPolicy( + await requireResourcePolicy({ + workspaceId: context.workspaceId, + resourceType: 'credential_group', + resourceId: context.credentialGroupId, + }) + ) }, }) export interface UpdateCredentialGroupAccessInput extends CredentialGroupAccessTargetInput { expectedRevision: number - grants: Array<{ id?: string; subject: ResourcePolicySubject }> + document: ResourcePolicyDocument } export const updateCredentialGroupAccess = defineAuthorizedWorkspaceUseCase({ @@ -70,32 +53,37 @@ export const updateCredentialGroupAccess = defineAuthorizedWorkspaceUseCase({ resolveCredentialGroupSettingsContext(input.credentialGroupId, input.assertedWorkspaceId), authorizationOptions: {}, async execute({ principal, input, context }) { - if (!Number.isInteger(input.expectedRevision) || input.expectedRevision < 0) { - throw new OrchestrationError('validation', 'Expected policy revision must be non-negative') + if (!Number.isInteger(input.expectedRevision) || input.expectedRevision < 1) { + throw new OrchestrationError('validation', 'Expected policy revision must be positive') + } + if ( + input.document.resource.type !== 'credential_group' || + input.document.resource.id !== context.credentialGroupId + ) { + throw new OrchestrationError( + 'validation', + 'Policy resource must match the canonical Credential Group' + ) } - const subjects = input.grants.map((grant) => resourcePolicySubjectSchema.parse(grant.subject)) - await validateResourcePolicySubjects(subjects, context) + const document = parseResourcePolicyDocument(input.document, { + type: 'credential_group', + id: context.credentialGroupId, + }) + await validateResourcePolicyPrincipals( + document.statements.flatMap((statement) => statement.principals), + context + ) try { - const policy = await writeResourcePolicy({ - workspaceId: context.workspaceId, - resourceType: 'credential_group', - resourceId: context.credentialGroupId, - expectedRevision: input.expectedRevision, - actorUserId: principal.userId, - document: { - version: RESOURCE_POLICY_VERSION, - resource: { type: 'credential_group', id: context.credentialGroupId }, - grants: input.grants.map((grant, index) => ({ - id: grant.id ?? generateId(), - subject: subjects[index], - actions: [...CREDENTIAL_GROUP_ACCESS_ACTIONS], - })), - }, - }) - return presentPolicy(policy, { - credentialGroupId: context.credentialGroupId, - workspaceId: context.workspaceId, - }) + return presentPolicy( + await writeResourcePolicy({ + workspaceId: context.workspaceId, + resourceType: 'credential_group', + resourceId: context.credentialGroupId, + expectedRevision: input.expectedRevision, + actorUserId: principal.userId, + document, + }) + ) } catch (error) { if (error instanceof ResourcePolicyRevisionConflictError) { throw new OrchestrationError('conflict', error.message) @@ -108,7 +96,10 @@ export const updateCredentialGroupAccess = defineAuthorizedWorkspaceUseCase({ resourceType: AuditResourceType.CREDENTIAL_GROUP, resourceId: context.credentialGroupId, resourceName: context.name, - description: 'Updated Credential Group access grants', - metadata: { revision: result.revision, grantCount: result.grants.length }, + description: 'Updated Credential Group resource policy', + metadata: { + revision: result.revision, + statementCount: result.document.statements.length, + }, }), }) diff --git a/apps/sim/lib/credential-groups/service.test.ts b/apps/sim/lib/credential-groups/service.test.ts index c73afcc2bee..4f6a69be682 100644 --- a/apps/sim/lib/credential-groups/service.test.ts +++ b/apps/sim/lib/credential-groups/service.test.ts @@ -18,7 +18,11 @@ vi.mock('@/lib/credential-groups/provider-registry', () => ({ getCredentialGroupProviderAdapter: () => ({ getPolicy: mockGetPolicy }), })) -import { updateCredentialGroup } from '@/lib/credential-groups/service' +import { + createCredentialGroup, + deleteCredentialGroup, + updateCredentialGroup, +} from '@/lib/credential-groups/service' describe('Credential Group service', () => { beforeEach(() => { @@ -86,4 +90,85 @@ describe('Credential Group service', () => { } ) }) + + it('creates a group only when its trigger-created default policy is present', async () => { + const created = { + id: 'group-1', + workspaceId: 'workspace-1', + publicId: 'public-1', + name: 'Support accounts', + description: null, + options: [], + encryptedProviderConfiguration: null, + status: 'active' as const, + createdBy: 'user-1', + createdAt: new Date('2026-08-20T00:00:00.000Z'), + updatedAt: new Date('2026-08-20T00:00:00.000Z'), + } + dbChainMockFns.returning.mockResolvedValueOnce([created]) + queueTableRows(schemaMock.resourcePolicy, [ + { + id: 'policy-1', + workspaceId: 'workspace-1', + resourceType: 'credential_group', + resourceId: 'group-1', + revision: 1, + document: { + version: 1, + resource: { type: 'credential_group', id: 'group-1' }, + statements: [], + }, + createdAt: created.createdAt, + updatedAt: created.updatedAt, + }, + ]) + + await expect( + createCredentialGroup('workspace-1', 'user-1', { + name: 'Support accounts', + description: '', + options: [], + }) + ).resolves.toMatchObject({ id: 'group-1', workspaceId: 'workspace-1' }) + expect(dbChainMockFns.transaction).toHaveBeenCalledOnce() + }) + + it('rolls back group creation when the required policy is missing', async () => { + dbChainMockFns.returning.mockResolvedValueOnce([ + { + id: 'group-1', + workspaceId: 'workspace-1', + publicId: 'public-1', + name: 'Support accounts', + description: null, + options: [], + encryptedProviderConfiguration: null, + status: 'active', + createdBy: 'user-1', + createdAt: new Date('2026-08-20T00:00:00.000Z'), + updatedAt: new Date('2026-08-20T00:00:00.000Z'), + }, + ]) + + await expect( + createCredentialGroup('workspace-1', 'user-1', { + name: 'Support accounts', + description: '', + options: [], + }) + ).rejects.toThrow('Required resource policy is missing') + }) + + it('deletes the policy and group in one locked transaction', async () => { + queueTableRows(schemaMock.credentialGroup, [{ id: 'group-1' }]) + dbChainMockFns.returning + .mockResolvedValueOnce([{ id: 'policy-1' }]) + .mockResolvedValueOnce([{ id: 'group-1' }]) + + await expect(deleteCredentialGroup('workspace-1', 'group-1')).resolves.toBe(true) + + expect(dbChainMockFns.for).toHaveBeenCalledWith('update') + expect(dbChainMockFns.delete).toHaveBeenCalledTimes(2) + expect(dbChainMockFns.transaction).toHaveBeenCalledOnce() + }) }) diff --git a/apps/sim/lib/credential-groups/service.ts b/apps/sim/lib/credential-groups/service.ts index 1ff48a9a7b1..e18fdc0088f 100644 --- a/apps/sim/lib/credential-groups/service.ts +++ b/apps/sim/lib/credential-groups/service.ts @@ -19,6 +19,10 @@ import type { UpdateCredentialGroupInput, } from '@/lib/credential-groups/types' import type { DbOrTx } from '@/lib/db/types' +import { + deleteResourcePolicyForResource, + requireDefaultResourcePolicyForNewResource, +} from '@/lib/resource-policies/repository' function scopesEqual(left: string[], right: string[]): boolean { const normalizedLeft = [...new Set(left)].sort() @@ -166,35 +170,60 @@ export async function createCredentialGroup( ): Promise { const now = new Date() const options = await Promise.all(body.options.map((option) => buildOption(workspaceId, option))) - const [created] = await db - .insert(credentialGroup) - .values({ - id: generateId(), - workspaceId, - publicId: generateId(), - name: body.name, - description: body.description || null, - options, - status: 'active', - createdBy: userId, - createdAt: now, - updatedAt: now, - }) - .returning() + return db.transaction(async (tx) => { + const [created] = await tx + .insert(credentialGroup) + .values({ + id: generateId(), + workspaceId, + publicId: generateId(), + name: body.name, + description: body.description || null, + options, + status: 'active', + createdBy: userId, + createdAt: now, + updatedAt: now, + }) + .returning() - if (!created) throw new Error('Credential group insert returned no row') - return toCredentialGroup(created) + if (!created) throw new Error('Credential group insert returned no row') + await requireDefaultResourcePolicyForNewResource( + { + workspaceId, + resourceType: 'credential_group', + resourceId: created.id, + }, + tx + ) + return toCredentialGroup(created) + }) } export async function deleteCredentialGroup( workspaceId: string, groupId: string ): Promise { - const deleted = await db - .delete(credentialGroup) - .where(and(eq(credentialGroup.id, groupId), eq(credentialGroup.workspaceId, workspaceId))) - .returning({ id: credentialGroup.id }) - return deleted.length > 0 + return db.transaction(async (tx) => { + const [existing] = await tx + .select({ id: credentialGroup.id }) + .from(credentialGroup) + .where(and(eq(credentialGroup.id, groupId), eq(credentialGroup.workspaceId, workspaceId))) + .limit(1) + .for('update') + if (!existing) return false + + await deleteResourcePolicyForResource( + { workspaceId, resourceType: 'credential_group', resourceId: groupId }, + tx + ) + const deleted = await tx + .delete(credentialGroup) + .where(and(eq(credentialGroup.id, groupId), eq(credentialGroup.workspaceId, workspaceId))) + .returning({ id: credentialGroup.id }) + if (deleted.length !== 1) throw new Error('Locked Credential Group delete returned no row') + return true + }) } export async function updateCredentialGroup( diff --git a/apps/sim/lib/credentials/application/resolve-managed-oauth-token.test.ts b/apps/sim/lib/credentials/application/resolve-managed-oauth-token.test.ts index debaea9c3a4..6312ac7f4c0 100644 --- a/apps/sim/lib/credentials/application/resolve-managed-oauth-token.test.ts +++ b/apps/sim/lib/credentials/application/resolve-managed-oauth-token.test.ts @@ -75,11 +75,7 @@ describe('resolveManagedOAuthCredentialToken', () => { vi.clearAllMocks() mocks.loadContext.mockResolvedValue(context) mocks.resolvePermission.mockResolvedValue('read') - mocks.requireCredentialAccess.mockResolvedValue({ - scope: 'enrollment', - enrollmentId: 'enrollment-1', - email: 'person@example.com', - }) + mocks.requireCredentialAccess.mockResolvedValue(undefined) mocks.resolveToken.mockResolvedValue({ accessToken: 'access-token', refreshed: false }) }) @@ -107,14 +103,20 @@ describe('resolveManagedOAuthCredentialToken', () => { }) it('resolves the token only after current workspace authorization', async () => { + const principal = executorPrincipal() const result = await resolveManagedOAuthCredentialToken.execute({ - principal: executorPrincipal(), + principal, input, }) expect(mocks.resolvePermission).toHaveBeenCalledWith('user-1', 'workspace-1', null, undefined, { forUpdate: undefined, }) + expect(mocks.requireCredentialAccess).toHaveBeenCalledWith( + principal, + context, + 'credential_groups.credentials.use' + ) expect(mocks.resolveToken).toHaveBeenCalledWith({ credentialId: 'credential-1', workspaceId: 'workspace-1', @@ -125,24 +127,23 @@ describe('resolveManagedOAuthCredentialToken', () => { expect(mocks.recordAudit).toHaveBeenCalledOnce() }) - it('rejects a credential owned by another group enrollment', async () => { - mocks.requireCredentialAccess.mockResolvedValueOnce({ - scope: 'enrollment', - enrollmentId: 'enrollment-2', - email: 'other@example.com', + it('does not resolve token material when the resource policy denies access', async () => { + mocks.requireCredentialAccess.mockRejectedValueOnce({ + code: 'forbidden', + message: 'Credential Group credential access denied', }) await expect( resolveManagedOAuthCredentialToken.execute({ principal: executorPrincipal(), input }) ).rejects.toMatchObject({ code: 'forbidden', - message: 'Credential Group enrollment access required', + message: 'Credential Group credential access denied', }) expect(mocks.resolveToken).not.toHaveBeenCalled() }) - it('allows any credential in the group for a matching all-credentials grant', async () => { - mocks.requireCredentialAccess.mockResolvedValueOnce({ scope: 'all', grantId: 'grant-1' }) + it('allows token resolution after any policy allow, including workflow-wide access', async () => { + mocks.requireCredentialAccess.mockResolvedValueOnce(undefined) await expect( resolveManagedOAuthCredentialToken.execute({ principal: executorPrincipal(), input }) 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 1d9c837e39b..1e18083511a 100644 --- a/apps/sim/lib/credentials/application/resolve-managed-oauth-token.ts +++ b/apps/sim/lib/credentials/application/resolve-managed-oauth-token.ts @@ -26,17 +26,11 @@ export const resolveManagedOAuthCredentialToken = defineAuthorizedWorkspaceUseCa }, authorizationOptions: { delegation: managedOAuthCredentialDelegationPolicy }, async authorizeResource({ principal, context }) { - const access = await requireCredentialGroupCredentialAccess( + await requireCredentialGroupCredentialAccess( principal, context, 'credential_groups.credentials.use' ) - if ( - access.scope === 'enrollment' && - access.enrollmentId !== context.credentialGroupEnrollmentId - ) { - throw new OrchestrationError('forbidden', 'Credential Group enrollment access required') - } }, execute: async ({ input, context }): Promise => resolveManagedOAuthToken({ diff --git a/apps/sim/lib/resource-policies/authorization.test.ts b/apps/sim/lib/resource-policies/authorization.test.ts index a585a14c5b8..204808a92fb 100644 --- a/apps/sim/lib/resource-policies/authorization.test.ts +++ b/apps/sim/lib/resource-policies/authorization.test.ts @@ -6,13 +6,13 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' const mocks = vi.hoisted(() => ({ isEnterprise: vi.fn(), - loadPolicy: vi.fn(), + requirePolicy: vi.fn(), resolveGroup: vi.fn(), resolvePermission: vi.fn(), })) vi.mock('@/lib/resource-policies/repository', () => ({ - loadResourcePolicy: mocks.loadPolicy, + requireResourcePolicy: mocks.requirePolicy, })) vi.mock('@/lib/billing', () => ({ @@ -29,7 +29,11 @@ vi.mock('@sim/platform-authz/workspace', () => ({ resolveEffectiveWorkspacePermission: mocks.resolvePermission, })) -import { findResourcePolicyGrant } from '@/lib/resource-policies/authorization' +import { evaluateResourcePolicy } from '@/lib/resource-policies/authorization' +import type { + ResourcePolicyCondition, + ResourcePolicyPrincipal, +} from '@/lib/resource-policies/types' const context = { workspaceId: 'workspace-1', @@ -64,7 +68,11 @@ function principal(input?: { mode?: 'draft' | 'deployment' }): WorkflowExecution } } -function policy(subject: Record) { +function policy(input: { + policyPrincipal: ResourcePolicyPrincipal + effect?: 'allow' | 'deny' + condition?: ResourcePolicyCondition +}) { return { id: 'policy-1', workspaceId: 'workspace-1', @@ -72,58 +80,113 @@ function policy(subject: Record) { createdAt: new Date(), updatedAt: new Date(), document: { - version: 1, - resource: { type: 'credential_group', id: 'group-1' }, - grants: [ + version: 1 as const, + resource: { type: 'credential_group' as const, id: 'group-1' }, + statements: [ { - id: 'grant-1', - subject, - actions: ['credential_groups.credentials.use'], + sid: 'ManagedStatement', + effect: input.effect ?? ('allow' as const), + principals: [input.policyPrincipal], + actions: ['credential_groups.credentials.use' as const], + ...(input.condition ? { condition: input.condition } : {}), }, ], }, } } -describe('findResourcePolicyGrant', () => { +const baseInput = { + context, + resourceType: 'credential_group' as const, + resourceId: 'group-1', + action: 'credential_groups.credentials.use' as const, + resourceContext: { 'credential_group:CredentialEnrollmentId': 'enrollment-1' }, +} + +describe('evaluateResourcePolicy', () => { beforeEach(() => { vi.clearAllMocks() mocks.isEnterprise.mockResolvedValue(true) mocks.resolvePermission.mockResolvedValue('read') }) - it('matches workflow grants only for deployed current workflow authority', async () => { - mocks.loadPolicy.mockResolvedValue(policy({ type: 'workflow', workflowId: 'workflow-1' })) - const input = { - context, - resourceType: 'credential_group' as const, - resourceId: 'group-1', - action: 'credential_groups.credentials.use' as const, - } + it('matches a workflow with an explicit deployment condition only when deployed', async () => { + mocks.requirePolicy.mockResolvedValue( + policy({ + policyPrincipal: { type: 'workflow', workflowId: 'workflow-1' }, + condition: { StringEquals: { 'sim:WorkflowMode': 'deployment' } }, + }) + ) + + await expect(evaluateResourcePolicy({ ...baseInput, principal: principal() })).resolves.toEqual( + { decision: 'allow', statementSid: 'ManagedStatement' } + ) + await expect( + evaluateResourcePolicy({ ...baseInput, principal: principal({ mode: 'draft' }) }) + ).resolves.toEqual({ decision: 'implicit_deny' }) + }) + + it('applies the hidden actor-own statement to only the matching enrollment', async () => { + const emptyPolicy = policy({ policyPrincipal: { type: 'any' } }) + emptyPolicy.document.statements = [] + mocks.requirePolicy.mockResolvedValue(emptyPolicy) await expect( - findResourcePolicyGrant({ ...input, principal: principal() }) - ).resolves.toMatchObject({ - id: 'grant-1', + evaluateResourcePolicy({ + ...baseInput, + principal: principal(), + resourceContext: { + 'credential_group:CredentialEnrollmentId': 'enrollment-1', + 'sim:PrincipalCredentialGroupEnrollmentId': 'enrollment-1', + }, + }) + ).resolves.toEqual({ + decision: 'allow', + statementSid: 'sim:CredentialGroupActorCredential', }) await expect( - findResourcePolicyGrant({ ...input, principal: principal({ mode: 'draft' }) }) - ).resolves.toBeNull() + evaluateResourcePolicy({ + ...baseInput, + principal: principal(), + resourceContext: { + 'credential_group:CredentialEnrollmentId': 'enrollment-2', + 'sim:PrincipalCredentialGroupEnrollmentId': 'enrollment-1', + }, + }) + ).resolves.toEqual({ decision: 'implicit_deny' }) + }) + + it('lets a managed explicit deny override actor-own access', async () => { + mocks.requirePolicy.mockResolvedValue( + policy({ policyPrincipal: { type: 'any' }, effect: 'deny' }) + ) + + await expect( + evaluateResourcePolicy({ + ...baseInput, + principal: principal(), + resourceContext: { + 'credential_group:CredentialEnrollmentId': 'enrollment-1', + 'sim:PrincipalCredentialGroupEnrollmentId': 'enrollment-1', + }, + }) + ).resolves.toEqual({ decision: 'deny', statementSid: 'ManagedStatement' }) }) it('matches an exact verified external identity', async () => { - mocks.loadPolicy.mockResolvedValue( + mocks.requirePolicy.mockResolvedValue( policy({ - type: 'external_identity', - provider: 'slack', - tenantId: 'T123', - subjectId: 'U123', + policyPrincipal: { + type: 'external_identity', + provider: 'slack', + tenantId: 'T123', + subjectId: 'U123', + }, }) ) const external = principal() external.subjectUserId = undefined - if (!external.delegationContext) throw new Error('Test principal is missing delegation context') - external.delegationContext.principal = { + external.delegationContext!.principal = { kind: 'system', serviceId: 'webhook', workspaceId: 'workspace-1', @@ -138,98 +201,77 @@ describe('findResourcePolicyGrant', () => { }, } - await expect( - findResourcePolicyGrant({ - principal: external, - context, - resourceType: 'credential_group', - resourceId: 'group-1', - action: 'credential_groups.credentials.use', - }) - ).resolves.toMatchObject({ id: 'grant-1' }) + await expect(evaluateResourcePolicy({ ...baseInput, principal: external })).resolves.toEqual({ + decision: 'allow', + statementSid: 'ManagedStatement', + }) }) - it('treats absence of a policy as no additional grant', async () => { - mocks.loadPolicy.mockResolvedValue(null) + it('fails fast when the required policy row is missing', async () => { + mocks.requirePolicy.mockRejectedValue( + new Error('Required resource policy is missing for credential_group group-1') + ) - await expect( - findResourcePolicyGrant({ - principal: principal(), - context, - resourceType: 'credential_group', - resourceId: 'group-1', - action: 'credential_groups.credentials.use', - }) - ).resolves.toBeNull() + await expect(evaluateResourcePolicy({ ...baseInput, principal: principal() })).rejects.toThrow( + 'Required resource policy is missing' + ) }) it('matches live workspace roles and effective Access Control Groups', async () => { - mocks.loadPolicy.mockResolvedValue(policy({ type: 'workspace_role', minimumRole: 'write' })) + mocks.requirePolicy.mockResolvedValue( + policy({ policyPrincipal: { type: 'workspace_role', minimumRole: 'write' } }) + ) mocks.resolvePermission.mockResolvedValue('admin') - const input = { - principal: principal(), - context, - resourceType: 'credential_group' as const, - resourceId: 'group-1', - action: 'credential_groups.credentials.use' as const, - } - await expect(findResourcePolicyGrant(input)).resolves.toMatchObject({ id: 'grant-1' }) + await expect( + evaluateResourcePolicy({ ...baseInput, principal: principal() }) + ).resolves.toMatchObject({ decision: 'allow' }) - mocks.loadPolicy.mockResolvedValue( - policy({ type: 'access_control_group', accessControlGroupId: 'access-group-1' }) + mocks.requirePolicy.mockResolvedValue( + policy({ + policyPrincipal: { + type: 'access_control_group', + accessControlGroupId: 'access-group-1', + }, + }) ) mocks.resolveGroup.mockResolvedValue({ permissionGroupId: 'access-group-1' }) - await expect(findResourcePolicyGrant(input)).resolves.toMatchObject({ id: 'grant-1' }) + await expect( + evaluateResourcePolicy({ ...baseInput, principal: principal() }) + ).resolves.toMatchObject({ decision: 'allow' }) expect(mocks.resolveGroup).toHaveBeenCalledWith('user-1', 'organization-1', 'workspace-1') }) it('matches direct human operations without inventing workflow authority', async () => { - mocks.loadPolicy.mockResolvedValue(policy({ type: 'user', userId: 'user-1' })) + mocks.requirePolicy.mockResolvedValue( + policy({ policyPrincipal: { type: 'user', userId: 'user-1' } }) + ) await expect( - findResourcePolicyGrant({ + evaluateResourcePolicy({ + ...baseInput, principal: { kind: 'session', userId: 'user-1', sessionId: 'session-1' }, - context, - resourceType: 'credential_group', - resourceId: 'group-1', - action: 'credential_groups.credentials.use', }) - ).resolves.toMatchObject({ id: 'grant-1' }) + ).resolves.toMatchObject({ decision: 'allow' }) }) it('fails fast when executor delegation has lost its original principal', async () => { - mocks.loadPolicy.mockResolvedValue(policy({ type: 'user', userId: 'user-1' })) + mocks.requirePolicy.mockResolvedValue( + policy({ policyPrincipal: { type: 'user', userId: 'user-1' } }) + ) const unbound = principal() - if (!unbound.delegationContext) throw new Error('Test principal is missing delegation context') - unbound.delegationContext.principal = undefined + unbound.delegationContext!.principal = undefined - await expect( - findResourcePolicyGrant({ - principal: unbound, - context, - resourceType: 'credential_group', - resourceId: 'group-1', - action: 'credential_groups.credentials.use', - }) - ).rejects.toThrow('bound workflow execution principal') + await expect(evaluateResourcePolicy({ ...baseInput, principal: unbound })).rejects.toThrow( + 'bound workflow execution principal' + ) }) - it('fails fast when policy storage disagrees with the canonical workspace', async () => { - mocks.loadPolicy.mockResolvedValue({ - ...policy({ type: 'user', userId: 'user-1' }), - workspaceId: 'workspace-2', - }) - + it('fails fast when the resource adapter omits required action context', async () => { await expect( - findResourcePolicyGrant({ - principal: principal(), - context, - resourceType: 'credential_group', - resourceId: 'group-1', - action: 'credential_groups.credentials.use', - }) - ).rejects.toThrow('workspace does not match') + evaluateResourcePolicy({ ...baseInput, principal: principal(), resourceContext: {} }) + ).rejects.toThrow('requires context key credential_group:CredentialEnrollmentId') + expect(mocks.requirePolicy).not.toHaveBeenCalled() }) }) diff --git a/apps/sim/lib/resource-policies/authorization.ts b/apps/sim/lib/resource-policies/authorization.ts index 158036b2cee..82fb82bef66 100644 --- a/apps/sim/lib/resource-policies/authorization.ts +++ b/apps/sim/lib/resource-policies/authorization.ts @@ -5,21 +5,31 @@ import { } from '@sim/platform-authz/workspace' import { isOrganizationOnEnterprisePlan } from '@/lib/billing' import type { WorkspaceAuthorizationContext } from '@/lib/core/application' -import { loadResourcePolicy } from '@/lib/resource-policies/repository' +import { getResourcePolicySystemStatements } from '@/lib/resource-policies/defaults' +import { + evaluateResourcePolicyStatements, + type ResourcePolicyContextValue, + type ResourcePolicyDecision, +} from '@/lib/resource-policies/evaluator' +import { + getResourcePolicyConditionKeyDefinition, + getResourcePolicyDefinition, +} from '@/lib/resource-policies/registry' +import { requireResourcePolicy } from '@/lib/resource-policies/repository' import type { ResourcePolicyAction, - ResourcePolicyGrant, + ResourcePolicyPrincipal, ResourcePolicyResourceType, - ResourcePolicySubject, } from '@/lib/resource-policies/types' import { resolveWorkspaceGroup } from '@/ee/access-control/utils/permission-check' -interface ResourcePolicyAuthorizationInput { +export interface ResourcePolicyAuthorizationInput { principal: Principal context: WorkspaceAuthorizationContext resourceType: ResourcePolicyResourceType resourceId: string action: ResourcePolicyAction + resourceContext: Readonly> } function resolveAuthorizationPrincipal(principal: Principal) { @@ -35,89 +45,138 @@ function resolveAuthorizationPrincipal(principal: Principal) { } } -async function matchesSubject( - subject: ResourcePolicySubject, - input: ResourcePolicyAuthorizationInput, - execution: ReturnType, - resolvers: { - workspacePermission: () => ReturnType - accessControlGroupId: () => Promise +function serviceIdForPrincipal(principal: Principal): string | undefined { + if (principal.kind === 'delegated' || principal.kind === 'system') return principal.serviceId + return undefined +} + +function keyIdForPrincipal(principal: Principal): string | undefined { + if (principal.kind === 'personal_api_key' || principal.kind === 'workspace_api_key') { + return principal.keyId } -): Promise { - const principalSubject = resolvePrincipalSubject(execution.invoker) - switch (subject.type) { - case 'user': - return principalSubject?.kind === 'sim_user' && principalSubject.userId === subject.userId - case 'external_identity': - return ( - principalSubject?.kind === 'external_user' && - principalSubject.provider === subject.provider && - principalSubject.tenantId === subject.tenantId && - principalSubject.subjectId === subject.subjectId - ) - case 'workflow': - return ( - execution.currentWorkflow?.mode === 'deployment' && - execution.currentWorkflow.workflowId === subject.workflowId - ) - case 'workspace_role': { - if (principalSubject?.kind !== 'sim_user') return false - const permission = await resolvers.workspacePermission() - return permission !== null && permissionSatisfies(permission, subject.minimumRole) + return undefined +} + +function valueMatchesType( + value: ResourcePolicyContextValue, + type: 'string' | 'string_list' | 'boolean' +): boolean { + switch (type) { + case 'string': + return typeof value === 'string' + case 'string_list': + return Array.isArray(value) && value.every((entry) => typeof entry === 'string') + case 'boolean': + return typeof value === 'boolean' + } +} + +function requireResourceContext(input: ResourcePolicyAuthorizationInput): void { + const definition = getResourcePolicyDefinition(input.resourceType) + for (const [key, value] of Object.entries(input.resourceContext)) { + if (value === undefined) continue + if (!Object.hasOwn(definition.conditionKeys, key)) { + throw new Error(`Resource policy adapter supplied unsupported context key ${key}`) } - case 'access_control_group': { - return (await resolvers.accessControlGroupId()) === subject.accessControlGroupId + const keyDefinition = getResourcePolicyConditionKeyDefinition(input.resourceType, key) + if (!keyDefinition || !valueMatchesType(value, keyDefinition.valueType)) { + throw new Error(`Resource policy adapter supplied invalid context value for ${key}`) + } + } + for (const [key, keyDefinition] of Object.entries(definition.conditionKeys)) { + if (!keyDefinition.requiredForActions?.includes(input.action)) continue + if (input.resourceContext[key] === undefined) { + throw new Error(`Resource policy action ${input.action} requires context key ${key}`) } } } -export async function findResourcePolicyGrant( +export async function evaluateResourcePolicy( input: ResourcePolicyAuthorizationInput -): Promise { - const policy = await loadResourcePolicy({ +): Promise { + if (!input.resourceId.trim()) throw new Error('Resource policy evaluation requires a resource ID') + requireResourceContext(input) + const stored = await requireResourcePolicy({ + workspaceId: input.context.workspaceId, resourceType: input.resourceType, resourceId: input.resourceId, }) - if (!policy) return null - if (policy.workspaceId !== input.context.workspaceId) { - throw new Error('Resource policy workspace does not match its canonical resource') - } const execution = resolveAuthorizationPrincipal(input.principal) const principalSubject = resolvePrincipalSubject(execution.invoker) let workspacePermission: ReturnType | undefined let accessControlGroupId: Promise | undefined - const resolvers = { - workspacePermission: () => { - if (principalSubject?.kind !== 'sim_user') return Promise.resolve(null) - workspacePermission ??= resolveEffectiveWorkspacePermission( - principalSubject.userId, - input.context.workspaceId, - input.context.workspaceOrganizationId - ) - return workspacePermission - }, - accessControlGroupId: () => { - accessControlGroupId ??= (async () => { - if ( - principalSubject?.kind !== 'sim_user' || - !input.context.workspaceOrganizationId || - !(await isOrganizationOnEnterprisePlan(input.context.workspaceOrganizationId)) - ) { - return null - } - const group = await resolveWorkspaceGroup( + + const matchesPrincipal = async (principal: ResourcePolicyPrincipal): Promise => { + switch (principal.type) { + case 'any': + return true + case 'user': + return principalSubject?.kind === 'sim_user' && principalSubject.userId === principal.userId + case 'external_identity': + return ( + principalSubject?.kind === 'external_user' && + principalSubject.provider === principal.provider && + principalSubject.tenantId === principal.tenantId && + principalSubject.subjectId === principal.subjectId + ) + case 'workflow': + return execution.currentWorkflow?.workflowId === principal.workflowId + case 'workspace_role': { + if (principalSubject?.kind !== 'sim_user') return false + workspacePermission ??= resolveEffectiveWorkspacePermission( principalSubject.userId, - input.context.workspaceOrganizationId, - input.context.workspaceId + input.context.workspaceId, + input.context.workspaceOrganizationId ) - return group?.permissionGroupId ?? null - })() - return accessControlGroupId - }, + const permission = await workspacePermission + return permission !== null && permissionSatisfies(permission, principal.minimumRole) + } + case 'access_control_group': { + accessControlGroupId ??= (async () => { + if ( + principalSubject?.kind !== 'sim_user' || + !input.context.workspaceOrganizationId || + !(await isOrganizationOnEnterprisePlan(input.context.workspaceOrganizationId)) + ) { + return null + } + const group = await resolveWorkspaceGroup( + principalSubject.userId, + input.context.workspaceOrganizationId, + input.context.workspaceId + ) + return group?.permissionGroupId ?? null + })() + return (await accessControlGroupId) === principal.accessControlGroupId + } + } } - for (const grant of policy.document.grants) { - if (!grant.actions.includes(input.action)) continue - if (await matchesSubject(grant.subject, input, execution, resolvers)) return grant + + const contextValues: Record = { + 'sim:PrincipalKind': execution.invoker.kind, + 'sim:PrincipalUserId': + principalSubject?.kind === 'sim_user' ? principalSubject.userId : undefined, + 'sim:PrincipalKeyId': keyIdForPrincipal(execution.invoker), + 'sim:PrincipalServiceId': serviceIdForPrincipal(execution.invoker), + 'sim:PrincipalExternalProvider': + principalSubject?.kind === 'external_user' ? principalSubject.provider : undefined, + 'sim:PrincipalExternalTenantId': + principalSubject?.kind === 'external_user' ? principalSubject.tenantId : undefined, + 'sim:PrincipalExternalSubjectId': + principalSubject?.kind === 'external_user' ? principalSubject.subjectId : undefined, + 'sim:WorkflowId': execution.currentWorkflow?.workflowId, + 'sim:WorkflowMode': execution.currentWorkflow?.mode, + 'sim:WorkspaceId': input.context.workspaceId, + ...input.resourceContext, } - return null + + return evaluateResourcePolicyStatements({ + statements: [ + ...getResourcePolicySystemStatements(input.resourceType), + ...stored.document.statements, + ], + action: input.action, + contextValues, + matchesPrincipal, + }) } diff --git a/apps/sim/lib/resource-policies/defaults.ts b/apps/sim/lib/resource-policies/defaults.ts new file mode 100644 index 00000000000..3ebe43dd23a --- /dev/null +++ b/apps/sim/lib/resource-policies/defaults.ts @@ -0,0 +1,41 @@ +import type { ResourcePolicyResourceType } from '@/lib/resource-policies/registry' +import { + parseResourcePolicySystemStatements, + type ResourcePolicyDocument, + type ResourcePolicyStatement, +} from '@/lib/resource-policies/types' + +const CREDENTIAL_GROUP_SYSTEM_STATEMENTS = parseResourcePolicySystemStatements('credential_group', [ + { + sid: 'sim:CredentialGroupActorCredential', + effect: 'allow', + actions: ['credential_groups.credentials.use'], + principals: [{ type: 'any' }], + condition: { + StringEquals: { + 'credential_group:CredentialEnrollmentId': `\${sim:PrincipalCredentialGroupEnrollmentId}`, + }, + }, + }, +]) + +export function createDefaultResourcePolicyDocument(input: { + type: ResourcePolicyResourceType + id: string +}): ResourcePolicyDocument { + if (!input.id.trim()) throw new Error('Resource policy requires a canonical resource ID') + return { + version: 1, + resource: input, + statements: [], + } +} + +export function getResourcePolicySystemStatements( + resourceType: ResourcePolicyResourceType +): readonly ResourcePolicyStatement[] { + switch (resourceType) { + case 'credential_group': + return CREDENTIAL_GROUP_SYSTEM_STATEMENTS + } +} diff --git a/apps/sim/lib/resource-policies/evaluator.test.ts b/apps/sim/lib/resource-policies/evaluator.test.ts new file mode 100644 index 00000000000..c060ae53979 --- /dev/null +++ b/apps/sim/lib/resource-policies/evaluator.test.ts @@ -0,0 +1,162 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it } from 'vitest' +import { evaluateResourcePolicyStatements } from '@/lib/resource-policies/evaluator' +import type { + ResourcePolicyPrincipal, + ResourcePolicyStatement, +} from '@/lib/resource-policies/types' + +const ACTION = 'credential_groups.credentials.use' as const + +function statement( + input: Partial & Pick +): ResourcePolicyStatement { + return { + sid: input.sid, + effect: input.effect ?? 'allow', + actions: input.actions ?? [ACTION], + principals: input.principals ?? [{ type: 'any' }], + ...(input.condition ? { condition: input.condition } : {}), + } +} + +async function evaluate( + statements: ResourcePolicyStatement[], + contextValues: Record = {}, + matchesPrincipal: (principal: ResourcePolicyPrincipal) => boolean = () => true +) { + return evaluateResourcePolicyStatements({ + statements, + action: ACTION, + contextValues, + matchesPrincipal: async (principal) => matchesPrincipal(principal), + }) +} + +describe('resource policy statement evaluation', () => { + it('gives any matching deny precedence over matching allows', async () => { + await expect( + evaluate([ + statement({ sid: 'AllowWorkflow' }), + statement({ sid: 'DenyWorkflow', effect: 'deny' }), + ]) + ).resolves.toEqual({ decision: 'deny', statementSid: 'DenyWorkflow' }) + }) + + it('uses OR across principals and statements, then implicitly denies no match', async () => { + const principals: ResourcePolicyPrincipal[] = [ + { type: 'user', userId: 'user-1' }, + { type: 'workflow', workflowId: 'workflow-1' }, + ] + await expect( + evaluate( + [statement({ sid: 'AllowWorkflow', principals })], + {}, + (principal) => principal.type === 'workflow' + ) + ).resolves.toEqual({ decision: 'allow', statementSid: 'AllowWorkflow' }) + await expect( + evaluate([statement({ sid: 'NoMatch', principals })], {}, () => false) + ).resolves.toEqual({ decision: 'implicit_deny' }) + }) + + it('ANDs condition operators and keys while ORing values for one key', async () => { + await expect( + evaluate( + [ + statement({ + sid: 'ConditionalAllow', + condition: { + StringEquals: { + 'sim:WorkflowMode': ['draft', 'deployment'], + 'sim:PrincipalKind': 'session', + }, + Bool: { 'test:Boolean': true }, + }, + }), + ], + { + 'sim:WorkflowMode': 'deployment', + 'sim:PrincipalKind': 'session', + 'test:Boolean': true, + } + ) + ).resolves.toEqual({ decision: 'allow', statementSid: 'ConditionalAllow' }) + }) + + it('supports negated matching and missing-key checks without recursive conditions', async () => { + await expect( + evaluate( + [ + statement({ + sid: 'NegativeAllow', + condition: { + StringNotEquals: { 'sim:PrincipalServiceId': 'schedule' }, + Null: { 'sim:PrincipalUserId': true }, + }, + }), + ], + {} + ) + ).resolves.toEqual({ decision: 'allow', statementSid: 'NegativeAllow' }) + }) + + it('resolves exact policy variables and treats missing variables as no match', async () => { + const actorStatement = statement({ + sid: 'ActorCredential', + condition: { + StringEquals: { + 'credential_group:CredentialEnrollmentId': `\${sim:PrincipalCredentialGroupEnrollmentId}`, + }, + }, + }) + await expect( + evaluate([actorStatement], { + 'credential_group:CredentialEnrollmentId': 'enrollment-1', + 'sim:PrincipalCredentialGroupEnrollmentId': 'enrollment-1', + }) + ).resolves.toEqual({ decision: 'allow', statementSid: 'ActorCredential' }) + await expect( + evaluate([actorStatement], { + 'credential_group:CredentialEnrollmentId': 'enrollment-1', + }) + ).resolves.toEqual({ decision: 'implicit_deny' }) + }) + + it('supports AWS-style wildcard and multivalue string operators', async () => { + await expect( + evaluate( + [ + statement({ + sid: 'TaggedResource', + condition: { + StringLike: { 'sim:PrincipalExternalSubjectId': 'U-*' }, + 'ForAnyValue:StringEquals': { + 'test:Tags': ['finance', 'legal'], + }, + }, + }), + ], + { + 'sim:PrincipalExternalSubjectId': 'U-123', + 'test:Tags': ['support', 'finance'], + } + ) + ).resolves.toEqual({ decision: 'allow', statementSid: 'TaggedResource' }) + }) + + it('fails fast instead of treating an empty operator map as unconditional', async () => { + await expect( + evaluate([ + statement({ + sid: 'InvalidConditionalAllow', + condition: { StringEquals: {} }, + }), + ]) + ).rejects.toThrow( + 'Resource policy condition operator StringEquals must contain at least one key' + ) + }) +}) diff --git a/apps/sim/lib/resource-policies/evaluator.ts b/apps/sim/lib/resource-policies/evaluator.ts new file mode 100644 index 00000000000..1cf8d6b6803 --- /dev/null +++ b/apps/sim/lib/resource-policies/evaluator.ts @@ -0,0 +1,179 @@ +import type { + ResourcePolicyAction, + ResourcePolicyCondition, + ResourcePolicyPrincipal, + ResourcePolicyStatement, +} from '@/lib/resource-policies/types' + +export type ResourcePolicyContextValue = string | readonly string[] | boolean +export type ResourcePolicyContextValues = Readonly< + Record +> + +export type ResourcePolicyDecision = + | { decision: 'allow'; statementSid: string } + | { decision: 'deny'; statementSid: string } + | { decision: 'implicit_deny' } + +interface EvaluateResourcePolicyStatementsInput { + statements: readonly ResourcePolicyStatement[] + action: ResourcePolicyAction + contextValues: ResourcePolicyContextValues + matchesPrincipal(principal: ResourcePolicyPrincipal): Promise +} + +const POLICY_VARIABLE_PATTERN = /^\$\{([^}]+)\}$/ + +function asArray(value: T | readonly T[]): readonly T[] { + return Array.isArray(value) ? (value as readonly T[]) : [value as T] +} + +function resolveExpectedStrings( + rawValue: string | string[], + contextValues: ResourcePolicyContextValues +): string[] | null { + const resolved: string[] = [] + for (const value of asArray(rawValue)) { + const variable = POLICY_VARIABLE_PATTERN.exec(value)?.[1] + if (!variable) { + resolved.push(value) + continue + } + const contextValue = contextValues[variable] + if (typeof contextValue !== 'string') return null + resolved.push(contextValue) + } + return resolved +} + +function wildcardMatches(actual: string, pattern: string): boolean { + let source = '' + for (const character of pattern) { + if (character === '*') source += '.*' + else if (character === '?') source += '.' + else source += character.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&') + } + return new RegExp(`^${source}$`, 'u').test(actual) +} + +function evaluateStringEntry(input: { + operator: 'StringEquals' | 'StringNotEquals' | 'StringLike' | 'StringNotLike' + actual: ResourcePolicyContextValue | undefined + expected: string[] +}): boolean { + const { operator, actual, expected } = input + if (actual !== undefined && typeof actual !== 'string') { + throw new Error(`${operator} received a non-string context value`) + } + const like = operator === 'StringLike' || operator === 'StringNotLike' + const matches = + typeof actual === 'string' && + expected.some((candidate) => (like ? wildcardMatches(actual, candidate) : actual === candidate)) + return operator === 'StringNotEquals' || operator === 'StringNotLike' ? !matches : matches +} + +function evaluateSetEntry(input: { + operator: 'ForAnyValue:StringEquals' | 'ForAllValues:StringEquals' + actual: ResourcePolicyContextValue | undefined + expected: string[] +}): boolean { + const { operator, actual, expected } = input + if ( + actual !== undefined && + (!Array.isArray(actual) || actual.some((value) => typeof value !== 'string')) + ) { + throw new Error(`${operator} received a non-string-list context value`) + } + const actualValues = (actual ?? []) as readonly string[] + if (operator === 'ForAnyValue:StringEquals') { + return actualValues.some((value) => expected.includes(value)) + } + return actualValues.every((value) => expected.includes(value)) +} + +function evaluateCondition( + condition: ResourcePolicyCondition, + contextValues: ResourcePolicyContextValues +): boolean { + for (const [operator, entries] of Object.entries(condition)) { + if (!entries) continue + if (Object.keys(entries).length === 0) { + throw new Error( + `Resource policy condition operator ${operator} must contain at least one key` + ) + } + for (const [key, rawExpected] of Object.entries(entries)) { + const actual = contextValues[key] + switch (operator) { + case 'StringEquals': + case 'StringNotEquals': + case 'StringLike': + case 'StringNotLike': { + const expected = resolveExpectedStrings(rawExpected as string | string[], contextValues) + if (!expected || !evaluateStringEntry({ operator, actual, expected })) return false + break + } + case 'ForAnyValue:StringEquals': + case 'ForAllValues:StringEquals': { + const expected = resolveExpectedStrings(rawExpected as string | string[], contextValues) + if (!expected || !evaluateSetEntry({ operator, actual, expected })) return false + break + } + case 'Bool': { + if (actual !== undefined && typeof actual !== 'boolean') { + throw new Error('Bool received a non-boolean context value') + } + if ( + typeof actual !== 'boolean' || + !asArray(rawExpected as boolean | boolean[]).includes(actual) + ) { + return false + } + break + } + case 'Null': { + const expectsMissing = rawExpected as boolean + if ((actual === undefined) !== expectsMissing) return false + break + } + default: + throw new Error(`Unsupported resource policy condition operator ${operator}`) + } + } + } + return true +} + +async function statementMatches( + statement: ResourcePolicyStatement, + input: EvaluateResourcePolicyStatementsInput +): Promise { + if (!statement.actions.includes(input.action)) return false + let principalMatches = false + for (const principal of statement.principals) { + if (await input.matchesPrincipal(principal)) { + principalMatches = true + break + } + } + if (!principalMatches) return false + return statement.condition ? evaluateCondition(statement.condition, input.contextValues) : true +} + +export async function evaluateResourcePolicyStatements( + input: EvaluateResourcePolicyStatementsInput +): Promise { + for (const statement of input.statements) { + if (statement.effect !== 'deny') continue + if (await statementMatches(statement, input)) { + return { decision: 'deny', statementSid: statement.sid } + } + } + for (const statement of input.statements) { + if (statement.effect !== 'allow') continue + if (await statementMatches(statement, input)) { + return { decision: 'allow', statementSid: statement.sid } + } + } + return { decision: 'implicit_deny' } +} diff --git a/apps/sim/lib/resource-policies/management.ts b/apps/sim/lib/resource-policies/management.ts index 95f29a3bfa1..ad1fd3a89b7 100644 --- a/apps/sim/lib/resource-policies/management.ts +++ b/apps/sim/lib/resource-policies/management.ts @@ -4,14 +4,17 @@ import { resolveEffectiveWorkspacePermission } from '@sim/platform-authz/workspa import { and, eq, isNull } from 'drizzle-orm' import type { WorkspaceAuthorizationContext } from '@/lib/core/application' import { OrchestrationError } from '@/lib/core/orchestration/types' -import type { ResourcePolicySubject } from '@/lib/resource-policies/types' +import { mapWithConcurrency } from '@/lib/core/utils/concurrency' +import type { ResourcePolicyPrincipal } from '@/lib/resource-policies/types' -async function requireUserSubject( - subject: Extract, +const POLICY_REFERENCE_VALIDATION_CONCURRENCY = 10 + +async function requireUserPrincipal( + principal: Extract, context: WorkspaceAuthorizationContext ) { const permission = await resolveEffectiveWorkspacePermission( - subject.userId, + principal.userId, context.workspaceId, context.workspaceOrganizationId ) @@ -20,8 +23,8 @@ async function requireUserSubject( } } -async function requireWorkflowSubject( - subject: Extract, +async function requireWorkflowPrincipal( + principal: Extract, context: WorkspaceAuthorizationContext ) { const [row] = await db @@ -29,7 +32,7 @@ async function requireWorkflowSubject( .from(workflow) .where( and( - eq(workflow.id, subject.workflowId), + eq(workflow.id, principal.workflowId), eq(workflow.workspaceId, context.workspaceId), isNull(workflow.archivedAt) ) @@ -38,14 +41,14 @@ async function requireWorkflowSubject( if (!row) throw new OrchestrationError('validation', 'Policy workflow was not found') } -async function requireAccessControlGroupSubject( - subject: Extract, +async function requireAccessControlGroupPrincipal( + principal: Extract, context: WorkspaceAuthorizationContext ) { if (!context.workspaceOrganizationId) { throw new OrchestrationError( 'validation', - 'Access Control Group grants require an organization workspace' + 'Access Control Group principals require an organization workspace' ) } const [row] = await db @@ -53,7 +56,7 @@ async function requireAccessControlGroupSubject( .from(permissionGroup) .where( and( - eq(permissionGroup.id, subject.accessControlGroupId), + eq(permissionGroup.id, principal.accessControlGroupId), eq(permissionGroup.organizationId, context.workspaceOrganizationId) ) ) @@ -70,7 +73,7 @@ async function requireAccessControlGroupSubject( .from(permissionGroupWorkspace) .where( and( - eq(permissionGroupWorkspace.permissionGroupId, subject.accessControlGroupId), + eq(permissionGroupWorkspace.permissionGroupId, principal.accessControlGroupId), eq(permissionGroupWorkspace.workspaceId, context.workspaceId) ) ) @@ -84,23 +87,29 @@ async function requireAccessControlGroupSubject( } } -export async function validateResourcePolicySubjects( - subjects: ResourcePolicySubject[], +export async function validateResourcePolicyPrincipals( + principals: ResourcePolicyPrincipal[], context: WorkspaceAuthorizationContext ): Promise { - await Promise.all( - subjects.map(async (subject) => { - switch (subject.type) { + const uniquePrincipals = [ + ...new Map(principals.map((principal) => [JSON.stringify(principal), principal])).values(), + ] + await mapWithConcurrency( + uniquePrincipals, + POLICY_REFERENCE_VALIDATION_CONCURRENCY, + async (principal) => { + switch (principal.type) { case 'user': - return requireUserSubject(subject, context) + return requireUserPrincipal(principal, context) case 'workflow': - return requireWorkflowSubject(subject, context) + return requireWorkflowPrincipal(principal, context) case 'access_control_group': - return requireAccessControlGroupSubject(subject, context) + return requireAccessControlGroupPrincipal(principal, context) + case 'any': case 'workspace_role': case 'external_identity': return } - }) + } ) } diff --git a/apps/sim/lib/resource-policies/registry.ts b/apps/sim/lib/resource-policies/registry.ts new file mode 100644 index 00000000000..1d505eb770d --- /dev/null +++ b/apps/sim/lib/resource-policies/registry.ts @@ -0,0 +1,114 @@ +export const RESOURCE_POLICY_RESOURCE_TYPES = ['credential_group'] as const + +export const RESOURCE_POLICY_ACTIONS = ['credential_groups.credentials.use'] as const + +export type ResourcePolicyResourceType = (typeof RESOURCE_POLICY_RESOURCE_TYPES)[number] +export type ResourcePolicyAction = (typeof RESOURCE_POLICY_ACTIONS)[number] +export type ResourcePolicyContextValueType = 'string' | 'string_list' | 'boolean' + +interface ResourcePolicyConditionKeyDefinition { + valueType: ResourcePolicyContextValueType + requiredForActions?: readonly ResourcePolicyAction[] +} + +interface ResourcePolicyResourceDefinition { + actions: readonly ResourcePolicyAction[] + conditionKeys: Readonly> +} + +export const GLOBAL_RESOURCE_POLICY_CONDITION_KEYS = { + 'sim:PrincipalKind': { valueType: 'string' }, + 'sim:PrincipalUserId': { valueType: 'string' }, + 'sim:PrincipalKeyId': { valueType: 'string' }, + 'sim:PrincipalServiceId': { valueType: 'string' }, + 'sim:PrincipalExternalProvider': { valueType: 'string' }, + 'sim:PrincipalExternalTenantId': { valueType: 'string' }, + 'sim:PrincipalExternalSubjectId': { valueType: 'string' }, + 'sim:WorkflowId': { valueType: 'string' }, + 'sim:WorkflowMode': { valueType: 'string' }, + 'sim:WorkspaceId': { valueType: 'string' }, +} as const satisfies Readonly> + +export const RESOURCE_POLICY_DEFINITIONS = { + credential_group: { + actions: RESOURCE_POLICY_ACTIONS, + conditionKeys: { + 'credential_group:CredentialEnrollmentId': { + valueType: 'string', + requiredForActions: RESOURCE_POLICY_ACTIONS, + }, + 'sim:PrincipalCredentialGroupEnrollmentId': { valueType: 'string' }, + }, + }, +} as const satisfies Record + +function assertResourcePolicyRegistry(): void { + const registeredActions = new Set() + const definitions: Readonly> = + RESOURCE_POLICY_DEFINITIONS + for (const [resourceType, definition] of Object.entries(definitions)) { + if (definition.actions.length === 0) { + throw new Error(`Resource policy type ${resourceType} must register at least one action`) + } + if (new Set(definition.actions).size !== definition.actions.length) { + throw new Error(`Resource policy type ${resourceType} contains duplicate actions`) + } + for (const action of definition.actions) { + if (!RESOURCE_POLICY_ACTIONS.includes(action)) { + throw new Error(`Resource policy type ${resourceType} registered unknown action ${action}`) + } + if (registeredActions.has(action)) { + throw new Error(`Resource policy action ${action} is registered by multiple resource types`) + } + registeredActions.add(action) + } + for (const [key, keyDefinition] of Object.entries(definition.conditionKeys)) { + if (!key.trim() || Object.hasOwn(GLOBAL_RESOURCE_POLICY_CONDITION_KEYS, key)) { + throw new Error( + `Resource policy type ${resourceType} registered invalid condition key ${key}` + ) + } + for (const action of keyDefinition.requiredForActions ?? []) { + if (!definition.actions.includes(action)) { + throw new Error( + `Resource policy condition key ${key} requires action ${action} from another resource type` + ) + } + } + } + } + for (const action of RESOURCE_POLICY_ACTIONS) { + if (!registeredActions.has(action)) { + throw new Error(`Resource policy action ${action} is not registered by a resource type`) + } + } +} + +assertResourcePolicyRegistry() + +export type GlobalResourcePolicyConditionKey = keyof typeof GLOBAL_RESOURCE_POLICY_CONDITION_KEYS +export type ResourcePolicySpecificConditionKey = + keyof (typeof RESOURCE_POLICY_DEFINITIONS)[ResourcePolicyResourceType]['conditionKeys'] +export type ResourcePolicyConditionKey = + | GlobalResourcePolicyConditionKey + | ResourcePolicySpecificConditionKey + +export function getResourcePolicyDefinition( + resourceType: ResourcePolicyResourceType +): ResourcePolicyResourceDefinition { + return RESOURCE_POLICY_DEFINITIONS[resourceType] +} + +export function getResourcePolicyConditionKeyDefinition( + resourceType: ResourcePolicyResourceType, + key: string +): ResourcePolicyConditionKeyDefinition | undefined { + if (Object.hasOwn(GLOBAL_RESOURCE_POLICY_CONDITION_KEYS, key)) { + return GLOBAL_RESOURCE_POLICY_CONDITION_KEYS[ + key as GlobalResourcePolicyConditionKey + ] as ResourcePolicyConditionKeyDefinition + } + const definition = RESOURCE_POLICY_DEFINITIONS[resourceType].conditionKeys + if (!Object.hasOwn(definition, key)) return undefined + return definition[key as keyof typeof definition] as ResourcePolicyConditionKeyDefinition +} diff --git a/apps/sim/lib/resource-policies/repository.test.ts b/apps/sim/lib/resource-policies/repository.test.ts new file mode 100644 index 00000000000..76a93ed992e --- /dev/null +++ b/apps/sim/lib/resource-policies/repository.test.ts @@ -0,0 +1,153 @@ +/** + * @vitest-environment node + */ +import { + dbChainMock, + dbChainMockFns, + hasMockCondition, + queueTableRows, + resetDbChainMock, + schemaMock, +} from '@sim/testing' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { + deleteResourcePolicyForResource, + ResourcePolicyNotFoundError, + ResourcePolicyRevisionConflictError, + requireDefaultResourcePolicyForNewResource, + requireResourcePolicy, + writeResourcePolicy, +} from '@/lib/resource-policies/repository' + +const TARGET = { + workspaceId: 'workspace-1', + resourceType: 'credential_group' as const, + resourceId: 'group-1', +} +const EMPTY_DOCUMENT = { + version: 1 as const, + resource: { type: 'credential_group' as const, id: 'group-1' }, + statements: [], +} + +function storedRow(revision = 1, document: unknown = EMPTY_DOCUMENT) { + return { + id: 'policy-1', + workspaceId: TARGET.workspaceId, + resourceType: TARGET.resourceType, + resourceId: TARGET.resourceId, + revision, + document, + createdBy: 'admin-1', + updatedBy: 'admin-1', + createdAt: new Date('2026-08-20T00:00:00.000Z'), + updatedAt: new Date('2026-08-20T00:00:00.000Z'), + } +} + +describe('resource policy repository', () => { + beforeEach(() => { + vi.clearAllMocks() + resetDbChainMock() + }) + + it('requires the canonical workspace, resource type, and resource ID', async () => { + queueTableRows(schemaMock.resourcePolicy, [storedRow()]) + + await expect(requireResourcePolicy(TARGET)).resolves.toMatchObject({ + id: 'policy-1', + workspaceId: TARGET.workspaceId, + revision: 1, + document: EMPTY_DOCUMENT, + }) + + const where = dbChainMockFns.where.mock.calls.at(-1)?.[0] + for (const expected of [TARGET.workspaceId, TARGET.resourceType, TARGET.resourceId]) { + expect( + hasMockCondition( + where, + (condition) => condition.type === 'eq' && condition.right === expected + ) + ).toBe(true) + } + }) + + it('fails fast for a missing or malformed stored policy', async () => { + await expect(requireResourcePolicy(TARGET)).rejects.toBeInstanceOf(ResourcePolicyNotFoundError) + + queueTableRows(schemaMock.resourcePolicy, [storedRow(1, { version: 1 })]) + await expect(requireResourcePolicy(TARGET)).rejects.toThrow() + }) + + it('verifies the trigger-created default policy for a new resource', async () => { + queueTableRows(schemaMock.resourcePolicy, [storedRow()]) + await expect( + requireDefaultResourcePolicyForNewResource(TARGET, dbChainMock.db) + ).resolves.toMatchObject({ revision: 1, document: EMPTY_DOCUMENT }) + + queueTableRows(schemaMock.resourcePolicy, [ + storedRow(2, { + ...EMPTY_DOCUMENT, + statements: [ + { + sid: 'WorkflowAccess', + effect: 'allow', + actions: ['credential_groups.credentials.use'], + principals: [{ type: 'workflow', workflowId: 'workflow-1' }], + }, + ], + }), + ]) + await expect( + requireDefaultResourcePolicyForNewResource(TARGET, dbChainMock.db) + ).rejects.toThrow('non-default') + }) + + it('locks and advances exactly the expected revision', async () => { + queueTableRows(schemaMock.resourcePolicy, [storedRow(3)]) + const updated = { ...storedRow(4), updatedAt: new Date('2026-08-20T01:00:00.000Z') } + dbChainMockFns.returning.mockResolvedValueOnce([updated]) + + await expect( + writeResourcePolicy({ + ...TARGET, + expectedRevision: 3, + actorUserId: 'admin-2', + document: EMPTY_DOCUMENT, + }) + ).resolves.toMatchObject({ revision: 4, document: EMPTY_DOCUMENT }) + + expect(dbChainMockFns.for).toHaveBeenCalledWith('update') + expect(dbChainMockFns.set).toHaveBeenCalledWith( + expect.objectContaining({ revision: 4, updatedBy: 'admin-2' }) + ) + const updateWhere = dbChainMockFns.where.mock.calls.at(-1)?.[0] + expect( + hasMockCondition(updateWhere, (condition) => condition.type === 'eq' && condition.right === 3) + ).toBe(true) + }) + + it('rejects a stale revision without issuing an update', async () => { + queueTableRows(schemaMock.resourcePolicy, [storedRow(4)]) + + await expect( + writeResourcePolicy({ + ...TARGET, + expectedRevision: 3, + actorUserId: 'admin-2', + document: EMPTY_DOCUMENT, + }) + ).rejects.toBeInstanceOf(ResourcePolicyRevisionConflictError) + expect(dbChainMockFns.update).not.toHaveBeenCalled() + }) + + it('requires exactly one policy row when deleting a resource', async () => { + dbChainMockFns.returning.mockResolvedValueOnce([{ id: 'policy-1' }]) + await expect(deleteResourcePolicyForResource(TARGET, dbChainMock.db)).resolves.toBeUndefined() + + dbChainMockFns.returning.mockResolvedValueOnce([]) + await expect(deleteResourcePolicyForResource(TARGET, dbChainMock.db)).rejects.toBeInstanceOf( + ResourcePolicyNotFoundError + ) + }) +}) diff --git a/apps/sim/lib/resource-policies/repository.ts b/apps/sim/lib/resource-policies/repository.ts index 9dc067d6e9a..065c39fcc91 100644 --- a/apps/sim/lib/resource-policies/repository.ts +++ b/apps/sim/lib/resource-policies/repository.ts @@ -1,7 +1,7 @@ import { db } from '@sim/db' import { resourcePolicy } from '@sim/db/schema' -import { generateId } from '@sim/utils/id' import { and, eq } from 'drizzle-orm' +import type { DbOrTx } from '@/lib/db/types' import { parseResourcePolicyDocument, type ResourcePolicyDocument, @@ -17,6 +17,13 @@ export interface StoredResourcePolicy { updatedAt: Date } +export class ResourcePolicyNotFoundError extends Error { + constructor(resourceType: ResourcePolicyResourceType, resourceId: string) { + super(`Required resource policy is missing for ${resourceType} ${resourceId}`) + this.name = 'ResourcePolicyNotFoundError' + } +} + export class ResourcePolicyRevisionConflictError extends Error { constructor() { super('Resource policy changed while it was being edited') @@ -24,20 +31,30 @@ export class ResourcePolicyRevisionConflictError extends Error { } } -export async function loadResourcePolicy(input: { +interface ResourcePolicyTarget { + workspaceId: string resourceType: ResourcePolicyResourceType resourceId: string -}): Promise { - const [row] = await db +} + +async function loadResourcePolicyWithExecutor( + input: ResourcePolicyTarget, + executor: DbOrTx, + options: { forUpdate?: boolean } = {} +): Promise { + const query = executor .select() .from(resourcePolicy) .where( and( + eq(resourcePolicy.workspaceId, input.workspaceId), eq(resourcePolicy.resourceType, input.resourceType), eq(resourcePolicy.resourceId, input.resourceId) ) ) .limit(1) + const rows = options.forUpdate ? await query.for('update') : await query + const row = rows[0] if (!row) return null return { id: row.id, @@ -52,6 +69,26 @@ export async function loadResourcePolicy(input: { } } +export async function requireResourcePolicy( + input: ResourcePolicyTarget, + executor: DbOrTx = db +): Promise { + const policy = await loadResourcePolicyWithExecutor(input, executor) + if (!policy) throw new ResourcePolicyNotFoundError(input.resourceType, input.resourceId) + return policy +} + +export async function requireDefaultResourcePolicyForNewResource( + input: ResourcePolicyTarget, + executor: DbOrTx +): Promise { + const policy = await requireResourcePolicy(input, executor) + if (policy.revision !== 1 || policy.document.statements.length !== 0) { + throw new Error('New resource was bound to a non-default resource policy') + } + return policy +} + export async function writeResourcePolicy(input: { workspaceId: string resourceType: ResourcePolicyResourceType @@ -60,63 +97,60 @@ export async function writeResourcePolicy(input: { document: ResourcePolicyDocument actorUserId: string }): Promise { + if (!Number.isInteger(input.expectedRevision) || input.expectedRevision < 1) { + throw new Error('Expected resource policy revision must be a positive integer') + } const document = parseResourcePolicyDocument(input.document, { type: input.resourceType, id: input.resourceId, }) - const now = new Date() - if (input.expectedRevision === 0) { - const [created] = await db - .insert(resourcePolicy) - .values({ - id: generateId(), - workspaceId: input.workspaceId, - resourceType: input.resourceType, - resourceId: input.resourceId, - revision: 1, + + return db.transaction(async (tx) => { + const existing = await loadResourcePolicyWithExecutor(input, tx, { forUpdate: true }) + if (!existing) throw new ResourcePolicyNotFoundError(input.resourceType, input.resourceId) + if (existing.revision !== input.expectedRevision) { + throw new ResourcePolicyRevisionConflictError() + } + + const [updated] = await tx + .update(resourcePolicy) + .set({ document, - createdBy: input.actorUserId, + revision: input.expectedRevision + 1, updatedBy: input.actorUserId, - createdAt: now, - updatedAt: now, + updatedAt: new Date(), }) - .onConflictDoNothing() + .where( + and(eq(resourcePolicy.id, existing.id), eq(resourcePolicy.revision, input.expectedRevision)) + ) .returning() - if (!created) throw new ResourcePolicyRevisionConflictError() + if (!updated) throw new Error('Locked resource policy update returned no row') return { - id: created.id, - workspaceId: created.workspaceId, - revision: created.revision, + id: updated.id, + workspaceId: updated.workspaceId, + revision: updated.revision, document, - createdAt: created.createdAt, - updatedAt: created.updatedAt, + createdAt: updated.createdAt, + updatedAt: updated.updatedAt, } - } + }) +} - const [updated] = await db - .update(resourcePolicy) - .set({ - document, - revision: input.expectedRevision + 1, - updatedBy: input.actorUserId, - updatedAt: now, - }) +export async function deleteResourcePolicyForResource( + input: ResourcePolicyTarget, + executor: DbOrTx +): Promise { + const deleted = await executor + .delete(resourcePolicy) .where( and( - eq(resourcePolicy.resourceType, input.resourceType), - eq(resourcePolicy.resourceId, input.resourceId), eq(resourcePolicy.workspaceId, input.workspaceId), - eq(resourcePolicy.revision, input.expectedRevision) + eq(resourcePolicy.resourceType, input.resourceType), + eq(resourcePolicy.resourceId, input.resourceId) ) ) - .returning() - if (!updated) throw new ResourcePolicyRevisionConflictError() - return { - id: updated.id, - workspaceId: updated.workspaceId, - revision: updated.revision, - document, - createdAt: updated.createdAt, - updatedAt: updated.updatedAt, + .returning({ id: resourcePolicy.id }) + if (deleted.length !== 1) { + throw new ResourcePolicyNotFoundError(input.resourceType, input.resourceId) } } diff --git a/apps/sim/lib/resource-policies/types.test.ts b/apps/sim/lib/resource-policies/types.test.ts index 18164b91e58..078669b8ef6 100644 --- a/apps/sim/lib/resource-policies/types.test.ts +++ b/apps/sim/lib/resource-policies/types.test.ts @@ -2,71 +2,185 @@ * @vitest-environment node */ import { describe, expect, it } from 'vitest' -import { parseResourcePolicyDocument } from '@/lib/resource-policies/types' +import { + parseResourcePolicyDocument, + parseResourcePolicySystemStatements, +} from '@/lib/resource-policies/types' -describe('resource policy documents', () => { - it('accepts exact allow-only grants', () => { - const document = { - version: 1 as const, - resource: { type: 'credential_group' as const, id: 'group-1' }, - grants: [ - { - id: 'grant-1', - subject: { type: 'workflow' as const, workflowId: 'workflow-1' }, - actions: ['credential_groups.credentials.use' as const], +function policy() { + return { + version: 1 as const, + resource: { type: 'credential_group' as const, id: 'group-1' }, + statements: [ + { + sid: 'DeployedWorkflowAccess', + effect: 'allow' as const, + actions: ['credential_groups.credentials.use' as const], + principals: [{ type: 'workflow' as const, workflowId: 'workflow-1' }], + condition: { StringEquals: { 'sim:WorkflowMode': 'deployment' } }, + }, + { + sid: 'BlockedExternalActor', + effect: 'deny' as const, + actions: ['credential_groups.credentials.use' as const], + principals: [{ type: 'any' as const }], + condition: { + StringEquals: { + 'credential_group:CredentialEnrollmentId': `\${sim:PrincipalCredentialGroupEnrollmentId}`, + }, + StringNotEquals: { 'sim:PrincipalExternalTenantId': ['T1', 'T2'] }, }, - ], - } + }, + ], + } +} + +describe('resource policy documents', () => { + it('accepts exact allow and deny statements with flat AWS-style conditions', () => { + const document = policy() expect( parseResourcePolicyDocument(document, { type: 'credential_group', id: 'group-1' }) ).toEqual(document) }) - it('rejects unknown fields, duplicate subjects, and wrong resource bindings', () => { + it('rejects recursive condition expressions and unknown fields', () => { expect(() => parseResourcePolicyDocument( { - version: 1, - resource: { type: 'credential_group', id: 'group-1' }, - grants: [], - effect: 'allow', + ...policy(), + statements: [ + { + ...policy().statements[0], + condition: { all: [{ StringEquals: { 'sim:WorkflowMode': 'deployment' } }] }, + }, + ], }, { type: 'credential_group', id: 'group-1' } ) - ).toThrow() + ).toThrow('Unrecognized key') + }) + + it('rejects every empty condition operator map', () => { + for (const operator of [ + 'StringEquals', + 'StringNotEquals', + 'StringLike', + 'StringNotLike', + 'Bool', + 'Null', + 'ForAnyValue:StringEquals', + 'ForAllValues:StringEquals', + ]) { + expect(() => + parseResourcePolicyDocument( + { + ...policy(), + statements: [ + { + ...policy().statements[0], + condition: { [operator]: {} }, + }, + ], + }, + { type: 'credential_group', id: 'group-1' } + ) + ).toThrow(`Resource policy condition operator ${operator} must contain at least one key`) + } + }) + + it('rejects duplicate statement IDs, actions, and principals', () => { + const duplicateStatement = policy().statements[0] + expect(() => + parseResourcePolicyDocument( + { ...policy(), statements: [duplicateStatement, duplicateStatement] }, + { type: 'credential_group', id: 'group-1' } + ) + ).toThrow('statement IDs must be unique') expect(() => parseResourcePolicyDocument( { - version: 1, - resource: { type: 'credential_group', id: 'group-1' }, - grants: [ + ...policy(), + statements: [ { - id: 'grant-1', - subject: { type: 'workflow', workflowId: 'workflow-1' }, - actions: ['credential_groups.credentials.use'], - }, - { - id: 'grant-2', - subject: { type: 'workflow', workflowId: 'workflow-1' }, - actions: ['credential_groups.credentials.use'], + ...duplicateStatement, + actions: ['credential_groups.credentials.use', 'credential_groups.credentials.use'], + principals: [ + { type: 'workflow', workflowId: 'workflow-1' }, + { type: 'workflow', workflowId: 'workflow-1' }, + ], }, ], }, { type: 'credential_group', id: 'group-1' } ) - ).toThrow('subject can only appear once') + ).toThrow() + }) + + it('bounds principals across the whole document', () => { + expect(() => + parseResourcePolicyDocument( + { + ...policy(), + statements: Array.from({ length: 3 }, (_, statementIndex) => ({ + sid: `Statement${statementIndex}`, + effect: 'allow', + actions: ['credential_groups.credentials.use'], + principals: Array.from({ length: 40 }, (_, principalIndex) => ({ + type: 'user', + userId: `user-${statementIndex}-${principalIndex}`, + })), + })), + }, + { type: 'credential_group', id: 'group-1' } + ) + ).toThrow('cannot contain more than 100 principals') + }) + + it('rejects reserved statement IDs and unsupported condition keys or variables', () => { + expect(() => + parseResourcePolicyDocument( + { + ...policy(), + statements: [{ ...policy().statements[0], sid: 'sim:ForgedSystemRule' }], + }, + { type: 'credential_group', id: 'group-1' } + ) + ).toThrow('are reserved') expect(() => parseResourcePolicyDocument( { - version: 1, - resource: { type: 'credential_group', id: 'group-2' }, - grants: [], + ...policy(), + statements: [ + { + ...policy().statements[0], + condition: { + StringEquals: { 'knowledge_base:Tag': `\${sim:Unknown}` }, + }, + }, + ], }, { type: 'credential_group', id: 'group-1' } ) + ).toThrow('does not apply') + }) + + it('requires reserved IDs on code-owned system statements', () => { + expect(() => + parseResourcePolicySystemStatements('credential_group', [ + { + ...policy().statements[0], + sid: 'ManagedLookingRule', + }, + ]) + ).toThrow('must begin with sim:') + }) + + it('rejects the wrong canonical resource binding', () => { + expect(() => + parseResourcePolicyDocument(policy(), { type: 'credential_group', id: 'group-2' }) ).toThrow('does not match') }) }) diff --git a/apps/sim/lib/resource-policies/types.ts b/apps/sim/lib/resource-policies/types.ts index c8bea9702f6..d16fda8480c 100644 --- a/apps/sim/lib/resource-policies/types.ts +++ b/apps/sim/lib/resource-policies/types.ts @@ -1,34 +1,49 @@ import { z } from 'zod' +import { + getResourcePolicyConditionKeyDefinition, + getResourcePolicyDefinition, + RESOURCE_POLICY_ACTIONS, + RESOURCE_POLICY_RESOURCE_TYPES, + type ResourcePolicyAction, + type ResourcePolicyContextValueType, + type ResourcePolicyResourceType, +} from '@/lib/resource-policies/registry' -export const RESOURCE_POLICY_VERSION = 1 as const -export const RESOURCE_POLICY_RESOURCE_TYPES = ['credential_group'] as const -export const RESOURCE_POLICY_ACTIONS = ['credential_groups.credentials.use'] as const -export type ResourcePolicyResourceType = (typeof RESOURCE_POLICY_RESOURCE_TYPES)[number] -export type ResourcePolicyAction = (typeof RESOURCE_POLICY_ACTIONS)[number] +export { + RESOURCE_POLICY_ACTIONS, + RESOURCE_POLICY_RESOURCE_TYPES, + type ResourcePolicyAction, + type ResourcePolicyConditionKey, + type ResourcePolicyResourceType, +} from '@/lib/resource-policies/registry' -export const RESOURCE_POLICY_ACTIONS_BY_RESOURCE = { - credential_group: RESOURCE_POLICY_ACTIONS, -} as const satisfies Record +export const RESOURCE_POLICY_VERSION = 1 as const +export const RESOURCE_POLICY_MAX_STATEMENTS = 100 +export const RESOURCE_POLICY_MAX_PRINCIPALS = 50 +export const RESOURCE_POLICY_MAX_CONDITION_VALUES = 50 +export const RESOURCE_POLICY_MAX_TOTAL_PRINCIPALS = 100 +export const RESOURCE_POLICY_MAX_TOTAL_CONDITION_KEYS = 100 +export const RESOURCE_POLICY_SYSTEM_SID_PREFIX = 'sim:' -const userSubjectSchema = z +const userPrincipalSchema = z .object({ type: z.literal('user'), userId: z.string().min(1).max(128) }) .strict() -const workspaceRoleSubjectSchema = z +const workspaceRolePrincipalSchema = z .object({ type: z.literal('workspace_role'), minimumRole: z.enum(['read', 'write', 'admin']), }) .strict() -const accessControlGroupSubjectSchema = z +const accessControlGroupPrincipalSchema = z .object({ type: z.literal('access_control_group'), accessControlGroupId: z.string().min(1).max(128), }) .strict() -const workflowSubjectSchema = z +const workflowPrincipalSchema = z .object({ type: z.literal('workflow'), workflowId: z.string().min(1).max(128) }) .strict() -const externalIdentitySubjectSchema = z +const externalIdentityPrincipalSchema = z .object({ type: z.literal('external_identity'), provider: z.string().min(1).max(128), @@ -36,81 +51,246 @@ const externalIdentitySubjectSchema = z subjectId: z.string().min(1).max(256), }) .strict() +const anyPrincipalSchema = z.object({ type: z.literal('any') }).strict() -export const resourcePolicySubjectSchema = z.discriminatedUnion('type', [ - userSubjectSchema, - workspaceRoleSubjectSchema, - accessControlGroupSubjectSchema, - workflowSubjectSchema, - externalIdentitySubjectSchema, +export const resourcePolicyPrincipalSchema = z.discriminatedUnion('type', [ + anyPrincipalSchema, + userPrincipalSchema, + workspaceRolePrincipalSchema, + accessControlGroupPrincipalSchema, + workflowPrincipalSchema, + externalIdentityPrincipalSchema, ]) -export type ResourcePolicySubject = z.output +export type ResourcePolicyPrincipal = z.output -export const resourcePolicyGrantSchema = z - .object({ - id: z.string().min(1).max(128), - subject: resourcePolicySubjectSchema, - actions: z.array(z.enum(RESOURCE_POLICY_ACTIONS)).min(1).max(RESOURCE_POLICY_ACTIONS.length), - }) - .strict() +const stringConditionValueSchema = z.union([ + z.string().max(1024), + z.array(z.string().max(1024)).min(1).max(RESOURCE_POLICY_MAX_CONDITION_VALUES), +]) +const booleanConditionValueSchema = z.union([ + z.boolean(), + z.array(z.boolean()).min(1).max(RESOURCE_POLICY_MAX_CONDITION_VALUES), +]) +const stringConditionMapSchema = z.record(z.string().min(1).max(256), stringConditionValueSchema) +const booleanConditionMapSchema = z.record(z.string().min(1).max(256), booleanConditionValueSchema) +const nullConditionMapSchema = z.record(z.string().min(1).max(256), z.boolean()) -export const resourcePolicyDocumentSchema = z +export const resourcePolicyConditionSchema = z .object({ - version: z.literal(RESOURCE_POLICY_VERSION), - resource: z - .object({ - type: z.enum(RESOURCE_POLICY_RESOURCE_TYPES), - id: z.string().min(1).max(128), - }) - .strict(), - grants: z.array(resourcePolicyGrantSchema).max(100), + StringEquals: stringConditionMapSchema.optional(), + StringNotEquals: stringConditionMapSchema.optional(), + StringLike: stringConditionMapSchema.optional(), + StringNotLike: stringConditionMapSchema.optional(), + Bool: booleanConditionMapSchema.optional(), + Null: nullConditionMapSchema.optional(), + 'ForAnyValue:StringEquals': stringConditionMapSchema.optional(), + 'ForAllValues:StringEquals': stringConditionMapSchema.optional(), }) .strict() - .superRefine((document, ctx) => { - const resourceActions: ReadonlySet = new Set( - RESOURCE_POLICY_ACTIONS_BY_RESOURCE[document.resource.type] - ) - const ids = new Set() - const subjects = new Set() - for (const [index, grant] of document.grants.entries()) { - if (ids.has(grant.id)) { + .superRefine((condition, ctx) => { + if (Object.values(condition).every((entries) => entries === undefined)) { + ctx.addIssue({ code: 'custom', message: 'Resource policy condition must not be empty' }) + } + let entryCount = 0 + for (const [operator, entries] of Object.entries(condition)) { + if (!entries) continue + const keys = Object.keys(entries) + if (keys.length === 0) { ctx.addIssue({ code: 'custom', - path: ['grants', index, 'id'], - message: 'Resource policy grant IDs must be unique', + path: [operator], + message: `Resource policy condition operator ${operator} must contain at least one key`, }) } - ids.add(grant.id) - const subjectKey = JSON.stringify(grant.subject) - if (subjects.has(subjectKey)) { + entryCount += keys.length + } + if (entryCount > RESOURCE_POLICY_MAX_CONDITION_VALUES) { + ctx.addIssue({ + code: 'custom', + message: `Resource policy condition cannot contain more than ${RESOURCE_POLICY_MAX_CONDITION_VALUES} keys`, + }) + } + }) + +export type ResourcePolicyCondition = z.output + +export const resourcePolicyStatementSchema = z + .object({ + sid: z.string().trim().min(1).max(128), + effect: z.enum(['allow', 'deny']), + actions: z.array(z.enum(RESOURCE_POLICY_ACTIONS)).min(1).max(RESOURCE_POLICY_ACTIONS.length), + principals: z.array(resourcePolicyPrincipalSchema).min(1).max(RESOURCE_POLICY_MAX_PRINCIPALS), + condition: resourcePolicyConditionSchema.optional(), + }) + .strict() + +export type ResourcePolicyStatement = z.output + +const POLICY_VARIABLE_PATTERN = /^\$\{([^}]+)\}$/ + +function valuesOf(value: string | string[]): string[] { + return Array.isArray(value) ? value : [value] +} + +function expectedConditionValueType(operator: string): ResourcePolicyContextValueType | 'any' { + if (operator === 'Bool') return 'boolean' + if (operator === 'Null') return 'any' + if (operator.startsWith('ForAnyValue:') || operator.startsWith('ForAllValues:')) { + return 'string_list' + } + return 'string' +} + +function validateCondition( + condition: ResourcePolicyCondition, + resourceType: ResourcePolicyResourceType, + statementIndex: number, + ctx: z.RefinementCtx +): void { + for (const [operator, entries] of Object.entries(condition)) { + if (!entries) continue + for (const [key, rawValue] of Object.entries(entries)) { + const definition = getResourcePolicyConditionKeyDefinition(resourceType, key) + if (!definition) { ctx.addIssue({ code: 'custom', - path: ['grants', index, 'subject'], - message: 'A resource policy subject can only appear once', + path: ['statements', statementIndex, 'condition', operator, key], + message: `Condition key ${key} does not apply to ${resourceType}`, }) + continue } - subjects.add(subjectKey) - if (new Set(grant.actions).size !== grant.actions.length) { + const expectedType = expectedConditionValueType(operator) + if (expectedType !== 'any' && definition.valueType !== expectedType) { ctx.addIssue({ code: 'custom', - path: ['grants', index, 'actions'], - message: 'Resource policy actions must be unique within a grant', + path: ['statements', statementIndex, 'condition', operator, key], + message: `${operator} cannot evaluate ${definition.valueType} condition key ${key}`, }) } - for (const [actionIndex, action] of grant.actions.entries()) { - if (!resourceActions.has(action)) { + if (operator === 'Bool' || operator === 'Null') continue + for (const value of valuesOf(rawValue as string | string[])) { + if (!value.includes('${')) continue + const variable = POLICY_VARIABLE_PATTERN.exec(value)?.[1] + const variableDefinition = variable + ? getResourcePolicyConditionKeyDefinition(resourceType, variable) + : undefined + if (!variable || !variableDefinition) { ctx.addIssue({ code: 'custom', - path: ['grants', index, 'actions', actionIndex], - message: 'Resource policy action does not apply to this resource type', + path: ['statements', statementIndex, 'condition', operator, key], + message: `Condition value ${value} is not a registered policy variable`, + }) + } else if (variableDefinition.valueType !== 'string') { + ctx.addIssue({ + code: 'custom', + path: ['statements', statementIndex, 'condition', operator, key], + message: `Policy variable ${variable} must resolve to one string`, }) } } } - }) + } +} + +function createResourcePolicyDocumentSchema(statementSource: 'managed' | 'system') { + return z + .object({ + version: z.literal(RESOURCE_POLICY_VERSION), + resource: z + .object({ + type: z.enum(RESOURCE_POLICY_RESOURCE_TYPES), + id: z.string().min(1).max(128), + }) + .strict(), + statements: z.array(resourcePolicyStatementSchema).max(RESOURCE_POLICY_MAX_STATEMENTS), + }) + .strict() + .superRefine((document, ctx) => { + const resourceActions: ReadonlySet = new Set( + getResourcePolicyDefinition(document.resource.type).actions + ) + const ids = new Set() + let totalPrincipals = 0 + let totalConditionKeys = 0 + for (const [statementIndex, statement] of document.statements.entries()) { + totalPrincipals += statement.principals.length + const isSystemStatement = statement.sid.startsWith(RESOURCE_POLICY_SYSTEM_SID_PREFIX) + if (statementSource === 'managed' && isSystemStatement) { + ctx.addIssue({ + code: 'custom', + path: ['statements', statementIndex, 'sid'], + message: `Statement IDs beginning with ${RESOURCE_POLICY_SYSTEM_SID_PREFIX} are reserved`, + }) + } + if (statementSource === 'system' && !isSystemStatement) { + ctx.addIssue({ + code: 'custom', + path: ['statements', statementIndex, 'sid'], + message: `System statement IDs must begin with ${RESOURCE_POLICY_SYSTEM_SID_PREFIX}`, + }) + } + if (ids.has(statement.sid)) { + ctx.addIssue({ + code: 'custom', + path: ['statements', statementIndex, 'sid'], + message: 'Resource policy statement IDs must be unique', + }) + } + ids.add(statement.sid) + if (new Set(statement.actions).size !== statement.actions.length) { + ctx.addIssue({ + code: 'custom', + path: ['statements', statementIndex, 'actions'], + message: 'Resource policy actions must be unique within a statement', + }) + } + const uniquePrincipals = new Set( + statement.principals.map((principal) => JSON.stringify(principal)) + ) + if (uniquePrincipals.size !== statement.principals.length) { + ctx.addIssue({ + code: 'custom', + path: ['statements', statementIndex, 'principals'], + message: 'Resource policy principals must be unique within a statement', + }) + } + for (const [actionIndex, action] of statement.actions.entries()) { + if (!resourceActions.has(action)) { + ctx.addIssue({ + code: 'custom', + path: ['statements', statementIndex, 'actions', actionIndex], + message: 'Resource policy action does not apply to this resource type', + }) + } + } + if (statement.condition) { + for (const entries of Object.values(statement.condition)) { + if (entries) totalConditionKeys += Object.keys(entries).length + } + validateCondition(statement.condition, document.resource.type, statementIndex, ctx) + } + } + if (totalPrincipals > RESOURCE_POLICY_MAX_TOTAL_PRINCIPALS) { + ctx.addIssue({ + code: 'custom', + path: ['statements'], + message: `Resource policy cannot contain more than ${RESOURCE_POLICY_MAX_TOTAL_PRINCIPALS} principals`, + }) + } + if (totalConditionKeys > RESOURCE_POLICY_MAX_TOTAL_CONDITION_KEYS) { + ctx.addIssue({ + code: 'custom', + path: ['statements'], + message: `Resource policy cannot contain more than ${RESOURCE_POLICY_MAX_TOTAL_CONDITION_KEYS} condition keys`, + }) + } + }) +} + +export const resourcePolicyDocumentSchema = createResourcePolicyDocumentSchema('managed') +const resourcePolicySystemDocumentSchema = createResourcePolicyDocumentSchema('system') -export type ResourcePolicyGrant = z.output export type ResourcePolicyDocument = z.output export function parseResourcePolicyDocument( @@ -123,3 +303,14 @@ export function parseResourcePolicyDocument( } return document } + +export function parseResourcePolicySystemStatements( + resourceType: ResourcePolicyResourceType, + statements: unknown +): readonly ResourcePolicyStatement[] { + return resourcePolicySystemDocumentSchema.parse({ + version: RESOURCE_POLICY_VERSION, + resource: { type: resourceType, id: 'system' }, + statements, + }).statements +} diff --git a/design/credential-group-authorization.md b/design/credential-group-authorization.md index d11443821bd..189ad9bed79 100644 --- a/design/credential-group-authorization.md +++ b/design/credential-group-authorization.md @@ -1,16 +1,16 @@ # Credential Group Authorization -Status: actor-only enforcement and group-wide resource grants are implemented +Status: list-all discovery and policy-gated credential use implemented ## Purpose -Credential Groups collect external users' managed credentials and let workflows select the credentials they are authorized to use. They are not general-purpose membership or authorization groups. +Credential Groups collect external users' managed credentials and let workflows discover credentials separately from the authority to use them. They are not general-purpose membership or authorization groups. -The default is simple: +The default is: -> An execution may list every non-secret credential reference, but it may use only the verified actor's credential. +> An authorized Credential Group execution may list every active non-secret credential reference, but it may use only the verified actor's own credential. -Explicit resource-policy grants can expand a user, Access Control Group, workspace role, or deployed workflow to every credential in the Credential Group. +Stored resource-policy statements can allow a user, Access Control Group, workspace role, external identity, or workflow to use every credential in the group. Explicit denies can remove access, including the otherwise built-in actor-own access. ## Credential identity @@ -48,11 +48,11 @@ verified webhook Principal subject (slack, teamId, userId) -> active enrollment ``` -Email is not accepted from workflow input for authorization. +Email supplied by workflow input can filter list results, but it is never accepted as proof of actor identity or credential authority. ## One block operation -The Credential Group block keeps one operation: +The Credential Group block keeps one credential-discovery operation: ```text List Credentials @@ -60,42 +60,47 @@ List Credentials Its contract is: -> Return every active credential reference in the group; selecting a reference does not authorize its use. +> Return a bounded page of active credential references in the group; selecting a reference does not authorize its use. -Inputs may select the Credential Group, provider option, and pagination. Inputs never select identity or authorization scope: +Inputs can select the Credential Group, optional email and provider filters, and pagination: ```ts interface ListCredentialGroupCredentialsInput { - credentialGroupId: string - credentialProviderIds?: string[] - limit: number - cursor?: string + credentialGroupId: string; + email?: string; + credentialProviderIds?: string[]; + limit: number; + cursor?: string; } ``` -The input does not contain: +The input does not contain an enrollment ID, credential ID, authorization scope, or `all` flag. The email and provider fields are query filters only. -- email; -- user ID; -- enrollment ID; -- credential ID; -- `all` or `scope` flags. +## List-all semantics -## List authorization +The list application operation verifies executor delegation, canonical workspace and Credential Group binding, the group's active status, entitlement, and the caller's workspace read boundary. It then returns all matching active credential references, not only the actor's enrollment. -The application operation loads the canonical Credential Group, verifies executor delegation, workspace binding, group scope, status, and entitlement, then returns a bounded page of all active credential references in the group. The result contains opaque IDs and account metadata but no token material, and a returned credential ID is never an authorization capability. +The result contains opaque credential IDs and bounded account metadata but no access or refresh token material. A returned credential ID is not a bearer capability. Every later credential use performs a new canonical authorization check. -## Credential use authorization +List authorization is intentionally distinct from the resource-policy action. The current policy registry has `credential_groups.credentials.use`; it does not have or imply a credential-list action. -OAuth token material is resolved only inside a protected application operation. Every use performs a fresh authorization check: +## Credential-use chokepoint + +OAuth token material is resolved only inside the protected managed-credential application operation: ```text -credential belongs to the actor's active enrollment -OR -execution matches a current credentials.use grant on the Credential Group +selected credential ID + -> load canonical credential, group, option, enrollment, and workspace + -> authorize executor delegation and workspace scope + -> require the Credential Group policy + -> evaluate credential_groups.credentials.use + -> resolve provider token material + -> emit semantic audit ``` -The credential must also be: +The protected operation supplies the selected credential's canonical enrollment ID as trusted resource context. If the execution has a verified actor enrollment, it also supplies that enrollment ID as trusted principal context. + +The credential must additionally be: - in the canonical requested Credential Group; - linked to an active enrollment and active provider option; @@ -103,64 +108,130 @@ The credential must also be: - for the expected provider; - granted all scopes required by the tool. -Tokens and refresh tokens never appear in block output or execution logs. List results return opaque credential references and bounded account metadata only. +Tokens and refresh tokens never appear in block output or execution logs. -## Resource-policy grants +## Hidden actor-own statement -The Credential Group resource policy can grant: +Credential Group evaluation always adds a code-owned system allow: -```text -credential_groups.credentials.use +```json +{ + "sid": "sim:CredentialGroupActorCredential", + "effect": "allow", + "actions": ["credential_groups.credentials.use"], + "principals": [{ "type": "any" }], + "condition": { + "StringEquals": { + "credential_group:CredentialEnrollmentId": "${sim:PrincipalCredentialGroupEnrollmentId}" + } + } +} ``` -Example workflow grant: +The statement matches only when the selected credential and verified actor resolve to the same enrollment. If there is no verified actor enrollment, its policy variable is absent and the statement does not match. + +This statement is evaluated but not stored or shown in the raw JSON editor. Its reserved `sim:` statement ID cannot be used by an administrator. A matching stored explicit deny has precedence over this system allow, so administrators can intentionally block even actor-own credential use. + +## Stored group-wide policy + +Each Credential Group has a required stored policy. A newly created or backfilled group begins with an empty stored `statements` array; the hidden actor-own statement provides the default data-plane access. + +Example deployed-workflow allow: ```json { - "id": "support-workflow-credentials", - "subject": { - "type": "workflow", - "workflowId": "wf_support" + "version": 1, + "resource": { + "type": "credential_group", + "id": "cg_support" }, - "actions": ["credential_groups.credentials.use"] + "statements": [ + { + "sid": "SupportWorkflowDeployed", + "effect": "allow", + "actions": ["credential_groups.credentials.use"], + "principals": [ + { + "type": "workflow", + "workflowId": "wf_support" + } + ], + "condition": { + "StringEquals": { + "sim:WorkflowMode": "deployment" + } + } + } + ] } ``` -The stable `workflowId` is the policy key. It matches only a canonically deployed execution. The deployment ID is carried for proof and audit but is not stored in the policy, so deploying a new version does not break the grant. +The stable `workflowId` is the policy principal. `sim:WorkflowMode` is an explicit condition. Omitting that condition allows the workflow principal to match both draft and deployed executions when it is the canonically bound current workflow. -Example Access Control Group grant: +Example Access Control Group allow: ```json { - "id": "support-admin-credentials", - "subject": { - "type": "access_control_group", - "accessControlGroupId": "pg_support_admins" - }, - "actions": ["credential_groups.credentials.use"] + "sid": "SupportOperators", + "effect": "allow", + "actions": ["credential_groups.credentials.use"], + "principals": [ + { + "type": "access_control_group", + "accessControlGroupId": "pg_support_operators" + } + ] } ``` -Membership is evaluated at operation time. Removing a user from the Access Control Group revokes access immediately on the next protected operation. +Membership is evaluated at operation time. Removing a Sim user from the Access Control Group revokes that allow on the next protected use. Actorless workflows cannot match a workspace-role or Access Control Group principal. -## Execution behavior +## Decision semantics -All executions that pass the list operation's workspace and group checks can see the same credential references. Using a selected credential resolves as follows: +For `credential_groups.credentials.use`, evaluation combines the hidden system statement with all stored statements: -| Execution | Credential use result | -| --- | --- | -| Manual actor without an explicit grant | Actor's enrollment only | -| Manual actor in a granted Access Control Group | All credentials | -| Manual draft relying only on a workflow grant | Actor's enrollment only | -| Deployed workflow with a workflow grant | All credentials | -| Deployed workflow with an actor but no workflow grant | Actor's enrollment only | -| Actorless schedule with a workflow grant | All credentials | -| Actorless schedule without a workflow grant | Fail | -| Actor with no enrollment and no explicit grant | Fail | +```text +matching stored deny + -> deny + +otherwise matching stored or system allow + -> allow + +otherwise + -> implicit deny +``` -There is no silent fallback from a requested all-credentials mode because the block has no caller-controlled access mode. Authorization happens when the selected credential is assumed. +Principals within a statement are ORed. Condition operators and keys are ANDed. Statement ordering never overrides explicit-deny precedence. -Manual testing exercises the actor path with the tester's credential. Full group-wide behavior is tested through a deployed execution, preferably against a staging Credential Group in a forked workspace. +| Execution | Credential-use result with no other matching statement | +| ----------------------------------------------------- | ------------------------------------------------------------ | +| Manual actor selecting their own enrollment | Allow through the system statement | +| Manual actor selecting another enrollment | Implicit deny | +| Verified Slack actor selecting their own enrollment | Allow through the system statement | +| Actor with no enrollment | Implicit deny | +| Actorless schedule | Implicit deny | +| Deployed workflow with a matching deployed-only allow | Allow every credential | +| Draft workflow with only a deployed-only allow | Actor-own only, or implicit deny without an actor enrollment | +| Workflow with an unconditional matching allow | Allow every credential in draft or deployment | +| Any execution matching a stored deny | Deny, even if an allow or actor-own statement also matches | + +Manual testing naturally exercises the actor path with the tester's own credential. Group-wide behavior can be tested in a draft only if the stored workflow statement intentionally allows draft mode; deployment-only behavior must be tested through a deployed execution, preferably against a staging Credential Group in a forked workspace. + +## Principal and workflow execution + +Credential use requires a bound executor delegation. The execution Principal carries both: + +```text +original actor Principal + -> Sim user or verified external identity + +current workflow + -> stable workflow ID, draft/deployment mode, and deployment proof when deployed +``` + +The policy evaluator unwraps the executor delegation to match actor principals while separately matching current-workflow principals and workflow conditions. It does not substitute a billing owner, workflow creator, credential creator, or API-key owner for the actor. + +For a regular subworkflow, current workflow identity switches to the child. A parent workflow's credential allow is not transitive. The original actor Principal and Access Control boundary remain attached to the execution. ## Slack-triggered execution @@ -168,36 +239,51 @@ Slack actor access requires a verified nested subject in the workflow execution ```text Slack signature and installation verified - -> webhook Principal with (teamId, userId) subject - -> workflow execution + -> webhook Principal with (teamId, userId) external subject + -> workflow execution Principal -> Credential Group enrollment resolution - -> actor-scoped credential use + -> actor-own system statement + -> credential use ``` The Slack trigger subscription credential only receives events. It is not the external user's downstream credential. -Bot events and events without a verified human subject have no actor. They require an explicit deployed-workflow grant for group-wide access or fail. +Bot events and events without a verified human subject have no actor enrollment. They require a matching stored allow, such as a deployed-workflow statement, or credential use is implicitly denied. + +## Required lifecycle + +The resource-policy row is part of the Credential Group lifecycle, not optional configuration: + +- the database lifecycle trigger provisions an empty revision-1 policy after group insertion; +- group creation verifies the default policy inside its transaction; +- group deletion removes the policy in the same transaction, with database lifecycle cleanup as a backstop; +- a script migration transforms legacy grants into allow statements, adds deployment-mode conditions to migrated workflow grants, backfills missing policies, and validates resource/workspace relationships. + +Runtime authorization calls `requireResourcePolicy`. A missing row fails fast as a storage invariant violation rather than falling back to actor-only or open behavior. + +## Management boundary and raw JSON UI -## Management boundary +Credential Group creation, options, invitations, enrollment lifecycle, and resource-policy changes are control-plane operations requiring a current workspace-admin session and audit. -Credential Group creation, options, invitations, enrollment lifecycle, and resource-policy changes remain control-plane operations requiring current workspace-admin authorization and audit. +The Access tab edits the complete stored policy as raw JSON. This preserves allow and deny effects, multiple principals, exact actions, and flat conditions without a lossy form projection. The editor validates strict JSON, canonical resource binding, referenced users/workflows/Access Control Groups, and the expected revision before saving. -Managing the policy does not automatically grant the administrator permission to use credentials. Data-plane credential use still requires actor ownership or an explicit grant. +The hidden actor-own system statement is explained but not displayed. Policy management does not automatically grant the administrator credential use. -## Current implementation delta +## Current scope -The branch: +The implementation: -- threads the original Principal into Credential Group executor delegation; -- removes caller-supplied email filtering from credential listing; -- resolves a verified Sim user to an exact active enrollment; -- lists bounded non-secret references for every active credential in the group; -- rechecks the same enrollment when resolving managed OAuth tokens; -- fails actorless execution instead of substituting a billing or workflow owner. -- stores and evaluates generic allow-only resource policies; -- resolves user, workspace-role, Access Control Group, external-identity, and deployed-workflow subjects; -- grants whole-group credential use when an explicit policy matches; -- binds Slack's verified provider subject to an active enrollment; -- exposes an audited, optimistic-concurrency admin management API and structured Access tab. +- threads the original Principal and current workflow through executor delegation; +- resolves verified Sim and Slack actors to active enrollments; +- lists bounded, non-secret references across the group with optional email/provider filters; +- treats list filtering as discovery rather than authorization; +- requires the stored Credential Group policy at credential-use time; +- evaluates allow and deny statements with explicit-deny precedence; +- adds the hidden actor-own system statement; +- supports exact user, workspace-role, Access Control Group, external-identity, workflow, and any principals; +- supports flat registered conditions, including explicit workflow-mode checks; +- rechecks canonical enrollment and provider requirements before exposing a managed OAuth token; +- exposes an audited optimistic-concurrency admin API and raw JSON Access editor; +- synchronizes policy creation, deletion, and backfill with Credential Group lifecycle. Knowledge Base/table resource enforcement and provenance-aware log redaction remain separate follow-up work. diff --git a/design/resource-policies.md b/design/resource-policies.md index 938700eaa86..4cc1a862b6f 100644 --- a/design/resource-policies.md +++ b/design/resource-policies.md @@ -1,12 +1,12 @@ # Resource Policies -Status: reusable store and Credential Group integration implemented +Status: statement policies and Credential Group enforcement implemented ## Goal Resource policies add data-plane authorization to individual Sim resources without replacing workspace roles or organizational Access Controls. -The same model starts with Credential Groups and later extends to Knowledge Bases, tables, files, and other protected resources. +The same model starts with Credential Groups and can later extend to Knowledge Bases, tables, files, and other protected resources. ## Responsibilities @@ -20,7 +20,7 @@ Access Control boundary -> may this actor or workflow use this integration, model, or tool category? resource policy - -> may this subject perform this action on this specific resource? + -> may this principal perform this action on this specific resource? ``` The effective decision requires every applicable layer: @@ -32,189 +32,243 @@ AND current workflow execution boundary AND resource authorization ``` -A resource grant never overrides an Access Control restriction. +A resource-policy allow never overrides an Access Control restriction. ## Policy format -The first version is allow-only and exact: +Policies use exact, AWS-style statements: ```ts interface ResourcePolicyV1 { - version: 1 + version: 1; resource: { - type: ResourceType - id: string - } - grants: ResourcePolicyGrant[] + type: ResourceType; + id: string; + }; + statements: ResourcePolicyStatement[]; } -interface ResourcePolicyGrant { - id: string - subject: ResourcePolicySubject - actions: string[] +interface ResourcePolicyStatement { + sid: string; + effect: "allow" | "deny"; + actions: ResourcePolicyAction[]; + principals: ResourcePolicyPrincipal[]; + condition?: ResourcePolicyCondition; } ``` -The first version intentionally has no: +The current version has exact resource types and actions. It has no resource or action wildcards and no persisted access-scope flag. + +Within one statement: -- explicit deny; -- wildcard resources or actions; -- arbitrary conditions; -- precedence rules; -- persisted access scope; -- workflow deployment-version key. +- actions match the exact requested action; +- principals are ORed; +- condition operators and keys are ANDed; +- multiple expected values for one key are ORed. -Each protected application operation names one exact resource and action. +Across statements, any matching explicit deny wins over every allow. If no deny matches, any matching allow authorizes the action. If nothing matches, the result is an implicit deny. Evaluation scans all matching denies before considering allows, so statement order does not change precedence. -## Subjects +## Principals ```ts -type ResourcePolicySubject = - | { type: 'user'; userId: string } - | { type: 'workspace_role'; minimumRole: 'read' | 'write' | 'admin' } - | { type: 'access_control_group'; accessControlGroupId: string } - | { type: 'workflow'; workflowId: string } +type ResourcePolicyPrincipal = + | { type: "any" } + | { type: "user"; userId: string } + | { type: "workspace_role"; minimumRole: "read" | "write" | "admin" } + | { type: "access_control_group"; accessControlGroupId: string } + | { type: "workflow"; workflowId: string } | { - type: 'external_identity' - provider: string - tenantId: string - subjectId: string - } + type: "external_identity"; + provider: string; + tenantId: string; + subjectId: string; + }; ``` -Policies store stable subject references, never session IDs, API keys, delegation IDs, emails, or serialized bearer credentials. +Policies store stable principal references, never session IDs, emails, API keys, delegation IDs, or serialized bearer credentials. -## Example Credential Group policy +Principal matching is exact: + +- `any` matches every authenticated execution context reaching the evaluator. +- `user` matches the canonical Sim user in the original actor Principal. +- `workspace_role` resolves the Sim user's current effective workspace role and compares it to `minimumRole`. +- `access_control_group` resolves the Sim user's current effective Access Control Group for the workspace. +- `external_identity` matches a verified nested provider subject by provider, tenant ID, and subject ID. +- `workflow` matches the canonically bound current workflow ID. + +Workspace-role and Access Control Group principals apply to a resolved Sim user, not to an actorless execution. Workflows do not implicitly inherit a human Access Control Group. + +## Flat conditions + +Conditions use flat AWS-style operator maps rather than a recursive expression tree: ```json { - "version": 1, - "resource": { - "type": "credential_group", - "id": "cg_support" + "StringEquals": { + "sim:WorkflowMode": "deployment" }, - "grants": [ - { - "id": "support-workflow", - "subject": { - "type": "workflow", - "workflowId": "wf_support" - }, - "actions": ["credential_groups.credentials.use"] - }, + "StringLike": { + "sim:PrincipalExternalSubjectId": "U-*" + } +} +``` + +Supported operators are: + +- `StringEquals` and `StringNotEquals`; +- `StringLike` and `StringNotLike`, with `*` and `?` wildcards; +- `Bool`; +- `Null`; +- `ForAnyValue:StringEquals` and `ForAllValues:StringEquals` for registered string-list keys. + +Condition keys and their value types come from code-owned registries. Unknown keys, operators, type combinations, and policy variables fail validation. A policy variable must occupy the complete expected value, for example: + +```json +{ + "StringEquals": { + "credential_group:CredentialEnrollmentId": "${sim:PrincipalCredentialGroupEnrollmentId}" + } +} +``` + +As in IAM, a missing context key satisfies `StringNotEquals` and `StringNotLike`. Use `Null: { "key": false }` in the same condition when the key must be present. This avoids hiding absence semantics behind a recursive `not` expression. + +Global context currently includes principal kind and stable IDs, verified external identity, current workflow ID and mode, and workspace ID. Each resource type registers its own additional context. Resource adapters must provide every context key required by an action; missing or incorrectly typed trusted context fails before policy evaluation. + +## Workflow mode + +A workflow principal stores only the stable `workflowId`. Deployment versions are runtime proof and audit context, not policy keys, so deploying a new version does not invalidate the policy. + +Workflow mode is an explicit condition rather than hidden principal behavior. A grant intended only for deployed execution must say so: + +```json +{ + "sid": "SupportWorkflowDeployed", + "effect": "allow", + "actions": ["credential_groups.credentials.use"], + "principals": [ { - "id": "support-admins", - "subject": { - "type": "access_control_group", - "accessControlGroupId": "pg_support_admins" - }, - "actions": ["credential_groups.credentials.use"] + "type": "workflow", + "workflowId": "wf_support" } - ] + ], + "condition": { + "StringEquals": { + "sim:WorkflowMode": "deployment" + } + } } ``` -An explicit grant on a Credential Group covers the whole group. Credential Group actor-only access is a built-in domain invariant, not a policy statement or scope value. +Without the mode condition, the same workflow principal can match either draft or deployed execution when that workflow is the canonically bound current workflow. + +## Execution identity and subworkflows + +The evaluator does not replace the execution Principal. For executor delegation it derives two trusted views from that Principal: + +```text +original actor Principal + -> user, external identity, role, and Access Control matching + +current workflow + -> workflow ID and workflow-mode matching +``` + +The execution also retains root workflow identity for audit, causality, and recursion. When a regular subworkflow runs, current workflow identity switches to the child, so parent workflow grants do not flow transitively. The original actor and their Access Control boundary remain in force. + +A bare workflow ID supplied by a block, tool, or request is never authority. The internal delegation boundary canonically rebinds workflow execution context before a protected application operation evaluates a policy. -## Example Knowledge Base policy +## Example Credential Group policy ```json { "version": 1, "resource": { - "type": "knowledge_base", - "id": "kb_finance" + "type": "credential_group", + "id": "cg_support" }, - "grants": [ + "statements": [ { - "id": "finance-team-read", - "subject": { - "type": "access_control_group", - "accessControlGroupId": "pg_finance" - }, - "actions": [ - "knowledge_bases.read", - "knowledge_bases.search" - ] + "sid": "SupportWorkflowDeployed", + "effect": "allow", + "actions": ["credential_groups.credentials.use"], + "principals": [ + { + "type": "workflow", + "workflowId": "wf_support" + } + ], + "condition": { + "StringEquals": { + "sim:WorkflowMode": "deployment" + } + } }, { - "id": "finance-agent-read", - "subject": { - "type": "workflow", - "workflowId": "wf_finance_agent" - }, - "actions": [ - "knowledge_bases.read", - "knowledge_bases.search" + "sid": "BlockDepartedSlackUser", + "effect": "deny", + "actions": ["credential_groups.credentials.use"], + "principals": [ + { + "type": "external_identity", + "provider": "slack", + "tenantId": "T123", + "subjectId": "U456" + } ] } ] } ``` -## Default behavior - -Resources remain open under their existing workspace-role behavior until a restrictive policy is attached. Once attached, unmatched subjects cannot perform the protected data action, including workspace administrators. +An explicit allow on a Credential Group authorizes use of any credential in that group unless a deny also matches. Actor access to the actor's own credential is supplied by a hidden system statement described below. -Resource types may define a narrowly scoped built-in rule where required. Credential Groups always retain actor access to that actor's own enrollment; their policy only adds broader grants. +## System statements -Workspace administrators can manage policies through audited control-plane operations. Policy-management authority does not imply data-read authority. +A resource type may define code-owned system statements. The evaluator combines them with the stored statements for every decision, but they are not persisted or shown in the editor. -## Runtime subject resolution +Credential Groups currently add this effective statement: -The evaluator receives trusted execution context and resolves current subjects: - -```ts -interface ResourceAuthorizationContext { - workspaceId: string - invoker: Principal - principalSubject: PrincipalSubject | null - workspaceRole?: 'read' | 'write' | 'admin' - accessControlGroupId?: string - currentWorkflow?: { - workflowId: string - mode: 'draft' | 'deployment' - deploymentVersionId?: string +```json +{ + "sid": "sim:CredentialGroupActorCredential", + "effect": "allow", + "actions": ["credential_groups.credentials.use"], + "principals": [{ "type": "any" }], + "condition": { + "StringEquals": { + "credential_group:CredentialEnrollmentId": "${sim:PrincipalCredentialGroupEnrollmentId}" + } } } ``` -Subject matching is exact: - -- `user` matches a canonical Sim user subject. -- `workspace_role` matches a current role at or above `minimumRole`. -- `access_control_group` matches the user's current effective Access Control Group for this workspace. -- `external_identity` matches a verified nested provider subject. -- `workflow` matches only the canonically deployed `currentWorkflow.workflowId`. - -Draft workflow context never matches a workflow subject. A bare workflow ID supplied by a block, tool, or request is not authority. - -## Access Control Groups - -Existing Access Control Groups remain organization governance groups. Their current restrictions define a maximum capability boundary for users. They can also serve as stable subjects in resource policies. - -The policy remains stored on the resource. The Access Control Group UI may show a Resources view by querying policies that reference the group, but it does not store a second copy. +This allows only the credential whose canonical enrollment matches the verified actor's enrollment. Because explicit deny has global precedence, a stored deny can block this system allow. Statement IDs beginning with `sim:` are reserved so administrators cannot forge or replace system statements. -Membership and workspace targeting are evaluated live. A group must belong to the resource's organization and apply to the resource workspace when a grant is created and when it is evaluated. +## Resource registry -Workflows do not implicitly inherit a human Access Control Group. Deployed workflows receive their own execution boundary and explicit resource grants. +The resource registry is the extension point for future protected resources. Each resource type registers: -## Workflow authority and subworkflows +- its exact actions; +- its allowed condition keys and value types; +- resource-specific trusted context; +- context keys required for each action; +- any code-owned system statements. -Workflow policies target a stable `workflowId`, so upgrades retain grants. Runtime authorization additionally requires canonical deployed-workflow proof. - -The execution carries both root and current workflow identity: +The current registry contains only: ```text -root workflow -> audit, causality, recursion -current workflow -> resource-policy matching +credential_group + action: credential_groups.credentials.use + required context: credential_group:CredentialEnrollmentId + actor context: sim:PrincipalCredentialGroupEnrollmentId ``` -When a subworkflow runs, resource grants switch to the child workflow. Parent grants do not flow transitively. The original actor Principal and Access Control boundary remain in force. +Adding Knowledge Base, table, or file authorization means registering its actions and context, provisioning a policy with every resource instance, and calling the same evaluator from the protected application operation. It does not require routes, tools, or every repository to understand provider-specific identities. -## Storage +## Required storage and lifecycle -Use a generic resource-policy table rather than adding unrelated policy JSON columns to every resource: +Every registered protected resource must have exactly one stored policy. A missing policy is an invariant violation and fails fast; it is not interpreted as open access or as an empty policy. ```text resource_policy @@ -230,11 +284,33 @@ resource_policy updated_at ``` -The table has one active policy per `(resource_type, resource_id)` and an index on `workspace_id`. Application use cases validate canonical resource ownership before reads or writes. +The Credential Group lifecycle is synchronized with this table: + +- creation provisions revision 1 with an empty stored `statements` array; +- the creation transaction verifies that the required default policy exists; +- deletion removes the policy in the same resource transaction; +- a database trigger also synchronizes Credential Group insert/delete lifecycle; +- the script migration converts legacy grants into allow statements and backfills missing policies for existing groups. + +Repository reads bind workspace ID, resource type, and resource ID. Writes require the current positive revision, lock the row, validate the complete document against its canonical resource, and increment the revision. Referential principal validation verifies users, workflows, and Access Control Groups against the canonical workspace before a write succeeds. + +If reverse queries such as "all resources referencing this Access Control Group" become frequent, a normalized statement-principal index can be derived transactionally. The policy document remains the single source of truth. + +## Management UI -If reverse queries such as "all resources granted to this Access Control Group" become frequent, add a normalized grant-subject index derived transactionally from the policy document. The policy document remains the single source of truth. +Credential Group policy management is an audited, workspace-admin control-plane operation. The Access tab intentionally exposes the stored document as raw JSON so the full statement language remains available without a partial form silently deleting effects, actions, principals, or conditions. -Policy writes require current workspace-admin authorization, exact schema validation, semantic audit, and optimistic concurrency through the current policy version or update timestamp. +The editor: + +- loads and saves the complete policy document; +- validates strict JSON and canonical resource binding before save; +- uses optimistic concurrency through `expectedRevision`; +- hides system statements and explains that actor-own access is built in; +- rejects reserved `sim:` statement IDs. + +The update route caps policy JSON at 256 KiB. The codec also bounds statements, principals, condition keys, and per-key values, while referential validation runs with fixed concurrency. These limits are part of the policy contract rather than assumptions about current workspace size. + +Managing a policy does not grant the administrator data-plane access. The administrator still needs a matching allow, and any matching explicit deny still wins. ## Enforcement boundary @@ -243,47 +319,19 @@ Every protected resource operation enters through its authorized application use ```text authenticate Principal -> load canonical resource and workspace - -> authorize current workspace access + -> authorize current workspace access and delegation scope -> apply actor and workflow Access Control boundaries - -> load current resource policy - -> match exact subject and action + -> require the stored resource policy + -> add code-owned system statements + -> evaluate exact action, principals, and flat conditions -> execute repository or manager operation -> emit semantic audit ``` -Routes, blocks, tools, Copilot adapters, and executor handlers do not query policy tables or make independent authorization decisions. - -## Current implementation - -The generic policy document, strict parser, optimistic repository, subject evaluator, and subject-management validation are implemented. Credential Groups are the first resource type and currently expose one managed permission in the UI: use every credential in the group. - -Future resource types extend the exact resource/action registry and call the same evaluator from their application operations. Knowledge Base/table enforcement and log-provenance redaction remain separate follow-up work. - -## Provenance and logs - -Future protected resources attach provenance to values returned into workflow execution: - -```ts -{ - resourceType: 'knowledge_base', - resourceId: 'kb_finance', - action: 'knowledge_bases.read' -} -``` - -Tool inputs, outputs, and log fields retain the provenance transitively. When a viewer reads execution logs, Sim reevaluates current resource access and redacts values derived from resources the viewer cannot read. - -Credential secrets remain stronger: token material is never inserted into logs or ordinary block output regardless of viewer permission. +Routes, blocks, tools, Copilot adapters, and executor handlers do not query policy tables or make independent resource-authorization decisions. -Provenance and viewer-specific log shielding are a later phase. They use the same policy evaluator but are not required for the first Credential Group resource-policy implementation. +## Current scope -## Initial implementation order +The generic statement codec, resource and condition registry, deny-first evaluator, required-policy repository, referential principal validation, lifecycle backfill, raw JSON editor, and Credential Group credential-use enforcement are implemented. -1. Add strict policy types, validation, storage, and evaluator. -2. Add admin policy read/write application operations and audit. -3. Add deployed-workflow authority to execution identity. -4. Integrate Credential Group credential-use authorization. -5. Add user, workspace-role, and Access Control Group subject resolution. -6. Add policy management UI on Credential Groups. -7. Extend the same evaluator to Knowledge Bases and tables. -8. Add resource provenance and viewer-specific log shielding. +Credential listing remains list-all and non-secret; possession of a returned credential ID is not authority. Knowledge Base/table enforcement and provenance-aware log redaction remain separate follow-up work. diff --git a/packages/db/migrations/0298_sparkling_hemingway.sql b/packages/db/migrations/0298_sparkling_hemingway.sql index 6502a9b9991..6cb13a08186 100644 --- a/packages/db/migrations/0298_sparkling_hemingway.sql +++ b/packages/db/migrations/0298_sparkling_hemingway.sql @@ -15,4 +15,49 @@ ALTER TABLE "resource_policy" ADD CONSTRAINT "resource_policy_workspace_id_works ALTER TABLE "resource_policy" ADD CONSTRAINT "resource_policy_created_by_user_id_fk" FOREIGN KEY ("created_by") REFERENCES "public"."user"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint ALTER TABLE "resource_policy" ADD CONSTRAINT "resource_policy_updated_by_user_id_fk" FOREIGN KEY ("updated_by") REFERENCES "public"."user"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint CREATE UNIQUE INDEX "resource_policy_resource_unique" ON "resource_policy" USING btree ("resource_type","resource_id");--> statement-breakpoint -CREATE INDEX "resource_policy_workspace_id_idx" ON "resource_policy" USING btree ("workspace_id"); \ No newline at end of file +CREATE INDEX "resource_policy_workspace_id_idx" ON "resource_policy" USING btree ("workspace_id");--> 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 + 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 + ), + NEW."created_by", + NEW."created_by" + ); + RETURN NEW; + END IF; + + DELETE FROM "public"."resource_policy" + WHERE "workspace_id" = OLD."workspace_id" + AND "resource_type" = 'credential_group' + AND "resource_id" = OLD."id"; + RETURN OLD; +END; +$$;--> statement-breakpoint +CREATE TRIGGER "credential_group_resource_policy_lifecycle" +AFTER INSERT OR DELETE ON "public"."credential_group" +FOR EACH ROW +EXECUTE FUNCTION "public"."sync_credential_group_resource_policy"(); diff --git a/packages/db/schema.ts b/packages/db/schema.ts index 4ef59ddf770..e65f8713437 100644 --- a/packages/db/schema.ts +++ b/packages/db/schema.ts @@ -4264,7 +4264,7 @@ export const permissionGroupMember = pgTable( }) ) -/** Versioned allow-only policy attached to one canonical workspace resource. */ +/** Versioned statement policy attached to one canonical workspace resource. */ export const resourcePolicy = pgTable( 'resource_policy', { diff --git a/packages/db/script-migrations-paused-billing-attribution.test.ts b/packages/db/script-migrations-paused-billing-attribution.test.ts index edc4ec2a0a9..cee28ba99fe 100644 --- a/packages/db/script-migrations-paused-billing-attribution.test.ts +++ b/packages/db/script-migrations-paused-billing-attribution.test.ts @@ -442,6 +442,7 @@ describe('script migration registry', () => { '0003_backfill_workspace_storage_usage', '0004_backfill_fork_kb_file_ownership', '0005_repair_unknown_table_row_provenance', + '0006_backfill_credential_group_resource_policies', ]) }) }) diff --git a/packages/db/script-migrations/0006_backfill_credential_group_resource_policies.test.ts b/packages/db/script-migrations/0006_backfill_credential_group_resource_policies.test.ts new file mode 100644 index 00000000000..8c1eccaf614 --- /dev/null +++ b/packages/db/script-migrations/0006_backfill_credential_group_resource_policies.test.ts @@ -0,0 +1,287 @@ +/** + * @vitest-environment node + */ +import { readFile } from 'node:fs/promises' +import type { Sql } from 'postgres' +import { describe, expect, it, vi } from 'vitest' +import { + type CredentialGroupPolicyBackfillStore, + createPostgresCredentialGroupPolicyBackfillStore, + type LegacyCredentialGroupPolicyRow, + type MissingCredentialGroupPolicyRow, + reconcileCredentialGroupResourcePolicies, + type StoredCredentialGroupPolicyRow, + transformLegacyCredentialGroupPolicyDocument, + validateCredentialGroupPolicyDocument, +} from './0006_backfill_credential_group_resource_policies' + +const EMPTY_DOCUMENT = (id: string) => ({ + version: 1 as const, + resource: { type: 'credential_group' as const, id }, + statements: [], +}) + +function normalizeSql(value: string): string { + return value.replace(/\s+/g, ' ').trim() +} + +describe('Credential Group resource policy backfill', () => { + it('strictly transforms legacy grants and deployment-gates workflow principals', () => { + const transformed = transformLegacyCredentialGroupPolicyDocument( + { + version: 1, + resource: { type: 'credential_group', id: 'group-1' }, + grants: [ + { + id: 'workflow-grant', + subject: { type: 'workflow', workflowId: 'workflow-1' }, + actions: ['credential_groups.credentials.use'], + }, + { + id: 'user-grant', + subject: { type: 'user', userId: 'user-1' }, + actions: ['credential_groups.credentials.use'], + }, + ], + }, + 'group-1' + ) + + expect(transformed).toEqual({ + version: 1, + resource: { type: 'credential_group', id: 'group-1' }, + statements: [ + { + sid: 'workflow-grant', + effect: 'allow', + actions: ['credential_groups.credentials.use'], + principals: [{ type: 'workflow', workflowId: 'workflow-1' }], + condition: { StringEquals: { 'sim:WorkflowMode': 'deployment' } }, + }, + { + sid: 'user-grant', + effect: 'allow', + actions: ['credential_groups.credentials.use'], + principals: [{ type: 'user', userId: 'user-1' }], + }, + ], + }) + }) + + it('rejects ambiguous legacy documents and malformed final documents', () => { + expect(() => + transformLegacyCredentialGroupPolicyDocument( + { + version: 1, + resource: { type: 'credential_group', id: 'group-1' }, + grants: [], + statements: [], + }, + 'group-1' + ) + ).toThrow('invalid shape') + + expect(() => + validateCredentialGroupPolicyDocument( + { + version: 1, + resource: { type: 'credential_group', id: 'group-1' }, + statements: [ + { + sid: 'sim:reserved', + effect: 'allow', + actions: ['credential_groups.credentials.use'], + principals: [{ type: 'any' }], + }, + ], + }, + 'group-1' + ) + ).toThrow('sid is invalid') + + expect(() => + validateCredentialGroupPolicyDocument( + { + version: 1, + resource: { type: 'credential_group', id: 'group-1' }, + statements: [ + { + sid: 'statement-1', + effect: 'allow', + actions: ['credential_groups.credentials.use'], + principals: [{ type: 'any' }], + condition: { + StringEquals: { 'sim:WorkflowId': String.raw`\${sim:UnknownVariable}` }, + }, + }, + ], + }, + 'group-1' + ) + ).toThrow('invalid policy variable') + }) + + it('walks bounded pages, is resumable, and validates the completed one-to-one state', async () => { + const legacyRows: LegacyCredentialGroupPolicyRow[] = [ + { + id: 'policy-1', + resourceId: 'group-1', + revision: 1, + document: { + version: 1, + resource: { type: 'credential_group', id: 'group-1' }, + grants: [], + }, + }, + ] + const missingRows: MissingCredentialGroupPolicyRow[] = [ + { id: 'group-2', workspaceId: 'workspace-1', createdBy: 'user-1' }, + { id: 'group-3', workspaceId: 'workspace-1', createdBy: null }, + ] + const policies: StoredCredentialGroupPolicyRow[] = [] + const pageCalls: Array<{ kind: string; afterId: string; limit: number }> = [] + const store: CredentialGroupPolicyBackfillStore = { + async listLegacyPolicies(afterId, limit) { + pageCalls.push({ kind: 'legacy', afterId, limit }) + return legacyRows.filter((row) => row.id > afterId).slice(0, limit) + }, + async transformLegacyPolicies(rows) { + for (const row of rows) { + legacyRows.splice( + legacyRows.findIndex((candidate) => candidate.id === row.id), + 1 + ) + policies.push({ + id: row.id, + workspaceId: 'workspace-1', + resourceId: row.resourceId, + revision: row.revision + 1, + document: row.nextDocument, + }) + } + return rows.length + }, + async listMissingPolicies(afterId, limit) { + pageCalls.push({ kind: 'missing', afterId, limit }) + return missingRows.filter((row) => row.id > afterId).slice(0, limit) + }, + async insertDefaultPolicies(rows) { + for (const row of rows) { + missingRows.splice( + missingRows.findIndex((candidate) => candidate.id === row.id), + 1 + ) + policies.push({ + id: `policy-${row.id}`, + workspaceId: row.workspaceId, + resourceId: row.id, + revision: 1, + document: EMPTY_DOCUMENT(row.id), + }) + } + return rows.length + }, + async findRelationalInvariantViolation() { + return null + }, + async listPolicies(afterId, limit) { + pageCalls.push({ kind: 'validate', afterId, limit }) + return [...policies] + .filter((row) => row.id > afterId) + .sort((left, right) => left.id.localeCompare(right.id)) + .slice(0, limit) + }, + } + + await expect( + reconcileCredentialGroupResourcePolicies(store, { batchSize: 2 }) + ).resolves.toEqual({ transformed: 1, scannedMissing: 2, inserted: 2, validated: 3 }) + await expect( + reconcileCredentialGroupResourcePolicies(store, { batchSize: 2 }) + ).resolves.toEqual({ transformed: 0, scannedMissing: 0, inserted: 0, validated: 3 }) + expect(pageCalls).toContainEqual({ kind: 'missing', afterId: '', limit: 2 }) + expect(pageCalls).toContainEqual({ kind: 'missing', afterId: 'group-3', limit: 2 }) + }) + + it('fails closed on relational violations and non-advancing stores', async () => { + const base: CredentialGroupPolicyBackfillStore = { + listLegacyPolicies: vi.fn().mockResolvedValue([]), + transformLegacyPolicies: vi.fn(), + listMissingPolicies: vi.fn().mockResolvedValue([]), + insertDefaultPolicies: vi.fn(), + findRelationalInvariantViolation: vi + .fn() + .mockResolvedValue({ kind: 'orphan', resourceId: 'group-1' }), + listPolicies: vi.fn(), + } + await expect(reconcileCredentialGroupResourcePolicies(base)).rejects.toThrow( + 'orphan policy for group-1' + ) + + const nonAdvancing: CredentialGroupPolicyBackfillStore = { + ...base, + listLegacyPolicies: vi + .fn() + .mockResolvedValue([{ id: '', resourceId: 'group-1', revision: 1, document: {} }]), + findRelationalInvariantViolation: vi.fn().mockResolvedValue(null), + } + await expect(reconcileCredentialGroupResourcePolicies(nonAdvancing)).rejects.toThrow( + 'non-advancing page' + ) + }) + + it('uses optimistic legacy rewrites and idempotent canonical inserts', async () => { + const queries: Array<{ text: string; values: unknown[] }> = [] + const query = vi.fn((strings: TemplateStringsArray, ...values: unknown[]) => { + const text = strings.join('?') + queries.push({ text, values }) + if (text.includes('UPDATE resource_policy')) return Promise.resolve([{ id: 'policy-1' }]) + if (text.includes('INSERT INTO resource_policy')) { + return Promise.resolve([{ resourceId: 'group-2' }]) + } + throw new Error(`Unexpected SQL in test: ${text}`) + }) + const sql = query as unknown as Sql + sql.begin = vi.fn(async (callback) => callback(sql)) as Sql['begin'] + const store = createPostgresCredentialGroupPolicyBackfillStore(sql) + const legacy: LegacyCredentialGroupPolicyRow & { + nextDocument: ReturnType + } = { + id: 'policy-1', + resourceId: 'group-1', + revision: 4, + document: { + version: 1, + resource: { type: 'credential_group', id: 'group-1' }, + grants: [], + }, + nextDocument: EMPTY_DOCUMENT('group-1'), + } + + await expect(store.transformLegacyPolicies([legacy])).resolves.toBe(1) + await expect( + store.insertDefaultPolicies([ + { id: 'group-2', workspaceId: 'workspace-1', createdBy: 'user-1' }, + ]) + ).resolves.toBe(1) + + const update = normalizeSql(queries[0].text) + expect(update).toContain('revision = revision + 1') + expect(update).toContain('AND revision = ?') + expect(update).toContain('AND document = ?::jsonb') + const insert = normalizeSql(queries[1].text) + expect(insert).toContain("'statements', '[]'::jsonb") + expect(insert).toContain('ON CONFLICT (resource_type, resource_id) DO NOTHING') + }) + + it('installs fail-fast rolling lifecycle triggers with the final empty document', async () => { + const migration = normalizeSql( + await readFile(new URL('../migrations/0298_sparkling_hemingway.sql', import.meta.url), 'utf8') + ) + + expect(migration).toContain('AFTER INSERT OR DELETE ON "public"."credential_group"') + expect(migration).toContain("'statements', '[]'::jsonb") + expect(migration).toContain('DELETE FROM "public"."resource_policy"') + expect(migration).not.toContain('ON CONFLICT DO NOTHING') + }) +}) diff --git a/packages/db/script-migrations/0006_backfill_credential_group_resource_policies.ts b/packages/db/script-migrations/0006_backfill_credential_group_resource_policies.ts new file mode 100644 index 00000000000..ae692e91ed8 --- /dev/null +++ b/packages/db/script-migrations/0006_backfill_credential_group_resource_policies.ts @@ -0,0 +1,624 @@ +import type { Sql } from 'postgres' +import type { ScriptMigration } from './types' + +export const CREDENTIAL_GROUP_POLICY_BACKFILL_BATCH_SIZE = 500 + +type ResourcePolicyPrincipal = + | { type: 'any' } + | { type: 'user'; userId: string } + | { type: 'workspace_role'; minimumRole: 'read' | 'write' | 'admin' } + | { type: 'access_control_group'; accessControlGroupId: string } + | { type: 'workflow'; workflowId: string } + | { type: 'external_identity'; provider: string; tenantId: string; subjectId: string } + +interface LegacyResourcePolicyGrant { + id: string + subject: ResourcePolicyPrincipal + actions: string[] +} + +interface ResourcePolicyStatement { + sid: string + effect: 'allow' | 'deny' + actions: string[] + principals: ResourcePolicyPrincipal[] + condition?: { + StringEquals: Record + } +} + +interface ResourcePolicyDocument { + version: 1 + resource: { type: 'credential_group'; id: string } + statements: ResourcePolicyStatement[] +} + +export interface LegacyCredentialGroupPolicyRow { + id: string + resourceId: string + revision: number + document: unknown +} + +export interface MissingCredentialGroupPolicyRow { + id: string + workspaceId: string + createdBy: string | null +} + +export interface StoredCredentialGroupPolicyRow { + id: string + workspaceId: string + resourceId: string + revision: number + document: unknown +} + +export interface CredentialGroupPolicyInvariantViolation { + kind: 'missing' | 'workspace_mismatch' | 'orphan' + resourceId: string +} + +export interface CredentialGroupPolicyBackfillStore { + listLegacyPolicies(afterId: string, limit: number): Promise + transformLegacyPolicies( + rows: Array + ): Promise + listMissingPolicies(afterId: string, limit: number): Promise + insertDefaultPolicies(rows: MissingCredentialGroupPolicyRow[]): Promise + findRelationalInvariantViolation(): Promise + listPolicies(afterId: string, limit: number): Promise +} + +interface CredentialGroupPolicyBackfillOptions { + batchSize?: number +} + +export interface CredentialGroupPolicyBackfillResult { + transformed: number + scannedMissing: number + inserted: number + validated: number +} + +function requireRecord(value: unknown, label: string): Record { + if (typeof value !== 'object' || value === null || Array.isArray(value)) { + throw new Error(`${label} must be an object`) + } + return value as Record +} + +function requireExactKeys( + value: Record, + required: readonly string[], + optional: readonly string[], + label: string +): void { + const allowed = new Set([...required, ...optional]) + const unexpected = Object.keys(value).filter((key) => !allowed.has(key)) + const missing = required.filter((key) => !(key in value)) + if (unexpected.length > 0 || missing.length > 0) { + throw new Error(`${label} has an invalid shape`) + } +} + +function requireNonEmptyString( + value: unknown, + label: string, + maxLength = Number.MAX_SAFE_INTEGER +): string { + if (typeof value !== 'string' || value.length === 0 || value.length > maxLength) { + throw new Error(`${label} must be a non-empty string`) + } + return value +} + +function requireStringArray(value: unknown, label: string): string[] { + if (!Array.isArray(value) || value.length === 0) { + throw new Error(`${label} must be a non-empty string array`) + } + return value.map((entry, index) => requireNonEmptyString(entry, `${label}[${index}]`)) +} + +function parsePrincipal( + value: unknown, + label: string, + options: { allowAny: boolean } +): ResourcePolicyPrincipal { + const principal = requireRecord(value, label) + const type = requireNonEmptyString(principal.type, `${label}.type`) + switch (type) { + case 'any': + requireExactKeys(principal, ['type'], [], label) + if (!options.allowAny) throw new Error(`${label}.type is unsupported`) + return { type } + case 'user': + requireExactKeys(principal, ['type', 'userId'], [], label) + return { type, userId: requireNonEmptyString(principal.userId, `${label}.userId`, 128) } + case 'workspace_role': { + requireExactKeys(principal, ['type', 'minimumRole'], [], label) + const minimumRole = requireNonEmptyString(principal.minimumRole, `${label}.minimumRole`) + if (minimumRole !== 'read' && minimumRole !== 'write' && minimumRole !== 'admin') { + throw new Error(`${label}.minimumRole is invalid`) + } + return { type, minimumRole } + } + case 'access_control_group': + requireExactKeys(principal, ['type', 'accessControlGroupId'], [], label) + return { + type, + accessControlGroupId: requireNonEmptyString( + principal.accessControlGroupId, + `${label}.accessControlGroupId`, + 128 + ), + } + case 'workflow': + requireExactKeys(principal, ['type', 'workflowId'], [], label) + return { + type, + workflowId: requireNonEmptyString(principal.workflowId, `${label}.workflowId`, 128), + } + case 'external_identity': + requireExactKeys(principal, ['type', 'provider', 'tenantId', 'subjectId'], [], label) + return { + type, + provider: requireNonEmptyString(principal.provider, `${label}.provider`, 128), + tenantId: requireNonEmptyString(principal.tenantId, `${label}.tenantId`, 256), + subjectId: requireNonEmptyString(principal.subjectId, `${label}.subjectId`, 256), + } + default: + throw new Error(`${label}.type is unsupported`) + } +} + +function parseLegacyGrant(value: unknown, index: number): LegacyResourcePolicyGrant { + const label = `legacy policy grant ${index}` + const grant = requireRecord(value, label) + requireExactKeys(grant, ['id', 'subject', 'actions'], [], label) + const actions = requireStringArray(grant.actions, `${label}.actions`) + if ( + actions.length !== 1 || + actions[0] !== 'credential_groups.credentials.use' || + new Set(actions).size !== actions.length + ) { + throw new Error(`${label}.actions are invalid for a Credential Group`) + } + return { + id: requireNonEmptyString(grant.id, `${label}.id`, 128), + subject: parsePrincipal(grant.subject, `${label}.subject`, { allowAny: false }), + actions, + } +} + +export function transformLegacyCredentialGroupPolicyDocument( + value: unknown, + expectedResourceId: string +): ResourcePolicyDocument { + const document = requireRecord(value, 'legacy policy document') + requireExactKeys(document, ['version', 'resource', 'grants'], [], 'legacy policy document') + if (document.version !== 1) throw new Error('Legacy policy version is unsupported') + const resource = requireRecord(document.resource, 'legacy policy resource') + requireExactKeys(resource, ['type', 'id'], [], 'legacy policy resource') + if (resource.type !== 'credential_group' || resource.id !== expectedResourceId) { + throw new Error('Legacy policy resource does not match its canonical Credential Group') + } + requireNonEmptyString(resource.id, 'legacy policy resource.id', 128) + if (!Array.isArray(document.grants) || document.grants.length > 100) { + throw new Error('Legacy policy grants must be a bounded array') + } + const grants = document.grants.map(parseLegacyGrant) + const statementIds = new Set() + for (const grant of grants) { + if (statementIds.has(grant.id)) throw new Error('Legacy policy grant IDs must be unique') + statementIds.add(grant.id) + } + const transformed: ResourcePolicyDocument = { + version: 1, + resource: { type: 'credential_group', id: expectedResourceId }, + statements: grants.map((grant) => ({ + sid: grant.id, + effect: 'allow', + actions: grant.actions, + principals: [grant.subject], + ...(grant.subject.type === 'workflow' + ? { + condition: { + StringEquals: { 'sim:WorkflowMode': 'deployment' }, + }, + } + : {}), + })), + } + validateCredentialGroupPolicyDocument(transformed, expectedResourceId) + return transformed +} + +function validateConditionValue(value: unknown, label: string, kind: 'string' | 'boolean'): void { + const values = Array.isArray(value) ? value : [value] + if ( + values.length === 0 || + values.length > 50 || + values.some( + (entry) => typeof entry !== kind || (typeof entry === 'string' && entry.length > 1024) + ) + ) { + throw new Error(`${label} has an invalid value`) + } +} + +function validateCondition(value: unknown, label: string): void { + const condition = requireRecord(value, label) + const stringOperators = new Set([ + 'StringEquals', + 'StringNotEquals', + 'StringLike', + 'StringNotLike', + 'ForAnyValue:StringEquals', + 'ForAllValues:StringEquals', + ]) + const booleanOperators = new Set(['Bool']) + const nullOperators = new Set(['Null']) + const allowedOperators = new Set([...stringOperators, ...booleanOperators, ...nullOperators]) + requireExactKeys(condition, [], [...allowedOperators], label) + if (Object.keys(condition).length === 0) throw new Error(`${label} must not be empty`) + let entryCount = 0 + const conditionKeyTypes: Readonly> = { + 'sim:PrincipalKind': 'string', + 'sim:PrincipalUserId': 'string', + 'sim:PrincipalKeyId': 'string', + 'sim:PrincipalServiceId': 'string', + 'sim:PrincipalExternalProvider': 'string', + 'sim:PrincipalExternalTenantId': 'string', + 'sim:PrincipalExternalSubjectId': 'string', + 'sim:WorkflowId': 'string', + 'sim:WorkflowMode': 'string', + 'sim:WorkspaceId': 'string', + 'credential_group:CredentialEnrollmentId': 'string', + 'sim:PrincipalCredentialGroupEnrollmentId': 'string', + } + for (const [operator, rawEntries] of Object.entries(condition)) { + const entries = requireRecord(rawEntries, `${label}.${operator}`) + for (const [key, entry] of Object.entries(entries)) { + requireNonEmptyString(key, `${label}.${operator} key`, 256) + entryCount += 1 + const keyType = conditionKeyTypes[key] + if (!keyType) throw new Error(`${label}.${operator}.${key} is not registered`) + if (booleanOperators.has(operator)) { + throw new Error(`${label}.${operator} cannot evaluate ${keyType}`) + } + if ( + (operator.startsWith('ForAnyValue:') || operator.startsWith('ForAllValues:')) && + keyType !== 'string_list' + ) { + throw new Error(`${label}.${operator} cannot evaluate ${keyType}`) + } + validateConditionValue( + entry, + `${label}.${operator}.${key}`, + stringOperators.has(operator) ? 'string' : 'boolean' + ) + if (stringOperators.has(operator)) { + const values = Array.isArray(entry) ? entry : [entry] + for (const value of values) { + if (typeof value !== 'string' || !value.includes('${')) continue + const variable = /^\$\{([^}]+)\}$/.exec(value)?.[1] + if (!variable || conditionKeyTypes[variable] !== 'string') { + throw new Error(`${label}.${operator}.${key} contains an invalid policy variable`) + } + } + } + if (nullOperators.has(operator) && Array.isArray(entry)) { + throw new Error(`${label}.${operator}.${key} must be a boolean`) + } + } + } + if (entryCount > 50) throw new Error(`${label} has too many keys`) +} + +export function validateCredentialGroupPolicyDocument( + value: unknown, + expectedResourceId: string +): void { + const document = requireRecord(value, 'resource policy document') + requireExactKeys(document, ['version', 'resource', 'statements'], [], 'resource policy document') + if (document.version !== 1) throw new Error('Resource policy version is unsupported') + const resource = requireRecord(document.resource, 'resource policy resource') + requireExactKeys(resource, ['type', 'id'], [], 'resource policy resource') + if (resource.type !== 'credential_group' || resource.id !== expectedResourceId) { + throw new Error('Resource policy document does not match its canonical Credential Group') + } + requireNonEmptyString(resource.id, 'resource policy resource.id', 128) + if (!Array.isArray(document.statements) || document.statements.length > 100) { + throw new Error('Resource policy statements must be a bounded array') + } + const statementIds = new Set() + let totalPrincipals = 0 + let totalConditionKeys = 0 + for (const [index, rawStatement] of document.statements.entries()) { + const label = `resource policy statement ${index}` + const statement = requireRecord(rawStatement, label) + requireExactKeys(statement, ['sid', 'effect', 'actions', 'principals'], ['condition'], label) + const sid = requireNonEmptyString(statement.sid, `${label}.sid`, 128) + if (sid.trim() !== sid || sid.startsWith('sim:')) { + throw new Error(`${label}.sid is invalid`) + } + if (statementIds.has(sid)) throw new Error('Resource policy statement IDs must be unique') + statementIds.add(sid) + if (statement.effect !== 'allow' && statement.effect !== 'deny') { + throw new Error(`${label}.effect is invalid`) + } + const actions = requireStringArray(statement.actions, `${label}.actions`) + if ( + actions.length !== 1 || + actions[0] !== 'credential_groups.credentials.use' || + new Set(actions).size !== actions.length + ) { + throw new Error(`${label}.actions are invalid for a Credential Group`) + } + if ( + !Array.isArray(statement.principals) || + statement.principals.length === 0 || + statement.principals.length > 50 + ) { + throw new Error(`${label}.principals must be a bounded non-empty array`) + } + const principals = statement.principals.map((principal, principalIndex) => + parsePrincipal(principal, `${label}.principals[${principalIndex}]`, { allowAny: true }) + ) + totalPrincipals += principals.length + if ( + new Set(principals.map((principal) => JSON.stringify(principal))).size !== principals.length + ) { + throw new Error(`${label}.principals must be unique`) + } + if (statement.condition !== undefined) { + const condition = requireRecord(statement.condition, `${label}.condition`) + for (const entries of Object.values(condition)) { + totalConditionKeys += Object.keys( + requireRecord(entries, `${label}.condition entries`) + ).length + } + validateCondition(statement.condition, `${label}.condition`) + } + } + if (totalPrincipals > 100) throw new Error('Resource policy has too many principals') + if (totalConditionKeys > 100) throw new Error('Resource policy has too many condition keys') +} + +function assertPage( + rows: T[], + afterId: string, + batchSize: number, + label: string +): string | null { + if (rows.length === 0) return null + if (rows.length > batchSize) throw new Error(`${label} returned an oversized page`) + const lastId = rows.at(-1)?.id + if (!lastId || lastId <= afterId) throw new Error(`${label} returned a non-advancing page`) + return lastId +} + +export async function reconcileCredentialGroupResourcePolicies( + store: CredentialGroupPolicyBackfillStore, + options: CredentialGroupPolicyBackfillOptions = {} +): Promise { + const batchSize = options.batchSize ?? CREDENTIAL_GROUP_POLICY_BACKFILL_BATCH_SIZE + if (!Number.isInteger(batchSize) || batchSize <= 0) { + throw new Error('Credential Group policy backfill batch size must be a positive integer') + } + const result: CredentialGroupPolicyBackfillResult = { + transformed: 0, + scannedMissing: 0, + inserted: 0, + validated: 0, + } + + let afterId = '' + for (;;) { + const rows = await store.listLegacyPolicies(afterId, batchSize) + const lastId = assertPage(rows, afterId, batchSize, 'Legacy Credential Group policy store') + if (!lastId) break + const transformed = rows.map((row) => ({ + ...row, + nextDocument: transformLegacyCredentialGroupPolicyDocument(row.document, row.resourceId), + })) + result.transformed += await store.transformLegacyPolicies(transformed) + afterId = lastId + } + + afterId = '' + for (;;) { + const rows = await store.listMissingPolicies(afterId, batchSize) + const lastId = assertPage(rows, afterId, batchSize, 'Missing Credential Group policy store') + if (!lastId) break + result.scannedMissing += rows.length + result.inserted += await store.insertDefaultPolicies(rows) + afterId = lastId + } + + const violation = await store.findRelationalInvariantViolation() + if (violation) { + throw new Error( + `Credential Group policy invariant failed: ${violation.kind} policy for ${violation.resourceId}` + ) + } + + afterId = '' + for (;;) { + const rows = await store.listPolicies(afterId, batchSize) + const lastId = assertPage(rows, afterId, batchSize, 'Credential Group policy validation store') + if (!lastId) break + for (const row of rows) { + if (!Number.isInteger(row.revision) || row.revision < 1) { + throw new Error(`Credential Group policy ${row.id} has an invalid revision`) + } + validateCredentialGroupPolicyDocument(row.document, row.resourceId) + } + result.validated += rows.length + afterId = lastId + } + return result +} + +export function createPostgresCredentialGroupPolicyBackfillStore( + sql: Sql +): CredentialGroupPolicyBackfillStore { + return { + async listLegacyPolicies(afterId, limit) { + return sql` + SELECT + id, + resource_id AS "resourceId", + revision, + document + FROM resource_policy + WHERE resource_type = 'credential_group' + AND id > ${afterId} + AND document ? 'grants' + ORDER BY id + LIMIT ${limit} + ` + }, + + async transformLegacyPolicies(rows) { + if (rows.length === 0) return 0 + return sql.begin(async (tx) => { + let transformed = 0 + for (const row of rows) { + const updated = await tx>` + UPDATE resource_policy + SET + document = ${JSON.stringify(row.nextDocument)}::jsonb, + revision = revision + 1, + updated_at = now() + WHERE id = ${row.id} + AND resource_type = 'credential_group' + AND resource_id = ${row.resourceId} + AND revision = ${row.revision} + AND document = ${JSON.stringify(row.document)}::jsonb + RETURNING id + ` + if (updated.length !== 1) { + throw new Error(`Legacy Credential Group policy ${row.id} changed during migration`) + } + transformed += 1 + } + return transformed + }) as Promise + }, + + async listMissingPolicies(afterId, limit) { + return sql` + SELECT + cg.id, + cg.workspace_id AS "workspaceId", + cg.created_by AS "createdBy" + FROM credential_group cg + WHERE cg.id > ${afterId} + AND NOT EXISTS ( + SELECT 1 + FROM resource_policy rp + WHERE rp.resource_type = 'credential_group' + AND rp.resource_id = cg.id + ) + ORDER BY cg.id + LIMIT ${limit} + ` + }, + + async insertDefaultPolicies(rows) { + if (rows.length === 0) return 0 + const ids = rows.map((row) => row.id) + const inserted = await sql>` + INSERT INTO resource_policy ( + id, + workspace_id, + resource_type, + resource_id, + revision, + document, + created_by, + updated_by + ) + SELECT + gen_random_uuid()::text, + cg.workspace_id, + 'credential_group', + cg.id, + 1, + jsonb_build_object( + 'version', 1, + 'resource', jsonb_build_object('type', 'credential_group', 'id', cg.id), + 'statements', '[]'::jsonb + ), + cg.created_by, + cg.created_by + FROM credential_group cg + WHERE cg.id = ANY(${ids}::text[]) + ON CONFLICT (resource_type, resource_id) DO NOTHING + RETURNING resource_id AS "resourceId" + ` + return inserted.length + }, + + async findRelationalInvariantViolation() { + const [violation] = await sql` + SELECT kind, resource_id AS "resourceId" + FROM ( + SELECT + CASE + WHEN rp.resource_id IS NULL THEN 'missing' + ELSE 'workspace_mismatch' + END AS kind, + cg.id AS resource_id + FROM credential_group cg + LEFT JOIN resource_policy rp + ON rp.resource_type = 'credential_group' + AND rp.resource_id = cg.id + WHERE rp.resource_id IS NULL + OR rp.workspace_id IS DISTINCT FROM cg.workspace_id + + UNION ALL + + SELECT 'orphan' AS kind, rp.resource_id + 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 + ) violations + ORDER BY resource_id + LIMIT 1 + ` + return violation ?? null + }, + + async listPolicies(afterId, limit) { + return sql` + SELECT + id, + workspace_id AS "workspaceId", + resource_id AS "resourceId", + revision, + document + FROM resource_policy + WHERE resource_type = 'credential_group' + AND id > ${afterId} + ORDER BY id + LIMIT ${limit} + ` + }, + } +} + +export const backfillCredentialGroupResourcePolicies: ScriptMigration = { + name: '0006_backfill_credential_group_resource_policies', + async up(sql) { + const result = await reconcileCredentialGroupResourcePolicies( + createPostgresCredentialGroupPolicyBackfillStore(sql) + ) + console.log( + `Credential Group policy backfill complete: ${result.transformed} transformed, ${result.inserted}/${result.scannedMissing} inserted, ${result.validated} validated.` + ) + }, +} diff --git a/packages/db/script-migrations/index.ts b/packages/db/script-migrations/index.ts index 8f022c456fe..3a6cc360c03 100644 --- a/packages/db/script-migrations/index.ts +++ b/packages/db/script-migrations/index.ts @@ -4,6 +4,7 @@ import { backfillPausedBillingAttribution } from './0002_backfill_paused_billing import { backfillWorkspaceStorageUsage } from './0003_backfill_workspace_storage_usage' import { backfillForkKnowledgeBaseFileOwnership } from './0004_backfill_fork_kb_file_ownership' import { repairUnknownTableRowProvenance } from './0005_repair_unknown_table_row_provenance' +import { backfillCredentialGroupResourcePolicies } from './0006_backfill_credential_group_resource_policies' import type { ScriptMigration } from './types' export type { ScriptMigration } from './types' @@ -19,6 +20,7 @@ export const scriptMigrations: readonly ScriptMigration[] = [ backfillWorkspaceStorageUsage, backfillForkKnowledgeBaseFileOwnership, repairUnknownTableRowProvenance, + backfillCredentialGroupResourcePolicies, ] /** diff --git a/packages/testing/src/mocks/schema.mock.ts b/packages/testing/src/mocks/schema.mock.ts index 1cdbc71c2e5..2e198c63dd1 100644 --- a/packages/testing/src/mocks/schema.mock.ts +++ b/packages/testing/src/mocks/schema.mock.ts @@ -1170,6 +1170,18 @@ export const schemaMock = { createdAt: 'createdAt', updatedAt: 'updatedAt', }, + resourcePolicy: { + id: 'id', + workspaceId: 'workspaceId', + resourceType: 'resourceType', + resourceId: 'resourceId', + revision: 'revision', + document: 'document', + createdBy: 'createdBy', + updatedBy: 'updatedBy', + createdAt: 'createdAt', + updatedAt: 'updatedAt', + }, credentialGroupEnrollmentStatusEnum: { enumValues: ['invited', 'delivery_failed', 'in_progress', 'completed', 'revoked'] as const, },