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..25d03774976 --- /dev/null +++ b/apps/sim/app/api/workspaces/[id]/credential-groups/[groupId]/access/route.test.ts @@ -0,0 +1,156 @@ +/** + * @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 }) } +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' } }, + }, + ], +} +const permissionGroups = [{ id: 'permission-group-1', name: 'Engineering' }] +const users = [{ userId: 'user-1', name: 'Test User', email: 'user@example.com' }] +const workflows = [{ id: 'workflow-1', name: 'Support workflow' }] + +describe('Credential Group access route', () => { + beforeEach(() => { + vi.clearAllMocks() + mocks.getSession.mockResolvedValue({ + user: { id: 'admin-1' }, + session: { id: 'session-1' }, + }) + mocks.read.mockResolvedValue({ revision: 1, document, users, workflows, permissionGroups }) + mocks.update.mockResolvedValue({ revision: 2, document }) + }) + + 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: 1, + document, + users, + workflows, + permissionGroups, + }) + expect(mocks.read).toHaveBeenCalledWith({ + principal: { kind: 'session', userId: 'admin-1', sessionId: 'session-1' }, + input: { assertedWorkspaceId: WORKSPACE_ID, credentialGroupId: GROUP_ID }, + request, + }) + }) + + 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), + headers: { 'content-type': 'application/json' }, + }) + const response = await PUT(request, context) + + expect(response.status).toBe(200) + expect(await response.json()).toEqual({ revision: 2, document }) + 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 recursive conditions at the HTTP boundary', async () => { + const request = new NextRequest(url, { + method: 'PUT', + body: JSON.stringify({ + expectedRevision: 1, + document: { + ...document, + statements: [ + { + ...document.statements[0], + condition: { all: [{ StringEquals: { 'sim:WorkflowMode': 'deployment' } }] }, + }, + ], + }, + }), + headers: { 'content-type': 'application/json' }, + }) + + const response = await PUT(request, context) + + 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 new file mode 100644 index 00000000000..d7bc12ee07a --- /dev/null +++ b/apps/sim/app/api/workspaces/[id]/credential-groups/[groupId]/access/route.ts @@ -0,0 +1,49 @@ +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', +}) +const MAX_RESOURCE_POLICY_BODY_BYTES = 256 * 1024 + +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'), + parseOptions: { maxBodyBytes: MAX_RESOURCE_POLICY_BODY_BYTES }, + mapInput: ({ params, body }) => ({ + assertedWorkspaceId: params.id, + credentialGroupId: params.groupId, + expectedRevision: body.expectedRevision, + document: body.document, + }), + 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/components/permissions/resource-policy-editor/index.ts b/apps/sim/components/permissions/resource-policy-editor/index.ts new file mode 100644 index 00000000000..64d2e4a1cc5 --- /dev/null +++ b/apps/sim/components/permissions/resource-policy-editor/index.ts @@ -0,0 +1,6 @@ +export { ResourcePolicyEditor } from '@/components/permissions/resource-policy-editor/resource-policy-editor' +export type { + ResourcePolicyEditorProps, + ResourcePolicyIdentityOption, + ResourcePolicyPrincipalOptions, +} from '@/components/permissions/resource-policy-editor/resource-policy-editor-types' diff --git a/apps/sim/components/permissions/resource-policy-editor/resource-policy-editor-model.test.ts b/apps/sim/components/permissions/resource-policy-editor/resource-policy-editor-model.test.ts new file mode 100644 index 00000000000..e4d6c450063 --- /dev/null +++ b/apps/sim/components/permissions/resource-policy-editor/resource-policy-editor-model.test.ts @@ -0,0 +1,149 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it } from 'vitest' +import { + addResourcePolicyStatement, + createUniqueResourcePolicyStatementSid, + duplicateResourcePolicyStatement, + flattenResourcePolicyCondition, + rebuildResourcePolicyCondition, + removeResourcePolicyStatement, + replaceResourcePolicyStatement, + serializeResourcePolicyDocument, +} from '@/components/permissions/resource-policy-editor/resource-policy-editor-model' +import type { + ResourcePolicyCondition, + ResourcePolicyDocument, + ResourcePolicyStatement, +} from '@/lib/resource-policies/types' + +const ALL_PRINCIPALS: ResourcePolicyStatement['principals'] = [ + { type: 'any' }, + { type: 'user', userId: 'user-1' }, + { type: 'workspace_role', minimumRole: 'write' }, + { type: 'access_control_group', accessControlGroupId: 'access-group-1' }, + { type: 'workflow', workflowId: 'workflow-1' }, + { + type: 'external_identity', + provider: 'slack', + tenantId: 'team-1', + subjectId: 'slack-user-1', + }, +] + +const FIRST_STATEMENT: ResourcePolicyStatement = { + sid: 'FinanceReadOnly', + effect: 'allow', + actions: ['credential_groups.credentials.use'], + principals: ALL_PRINCIPALS, + condition: { + StringEquals: { 'sim:WorkflowMode': 'deployment' }, + StringNotLike: { 'sim:WorkspaceId': ['sandbox-*', 'test-*'] }, + }, +} + +const SECOND_STATEMENT: ResourcePolicyStatement = { + sid: 'BlockOtherWorkflows', + effect: 'deny', + actions: ['credential_groups.credentials.use'], + principals: [{ type: 'any' }], + condition: { + StringNotEquals: { 'sim:WorkflowId': 'workflow-1' }, + }, +} + +const DOCUMENT: ResourcePolicyDocument = { + version: 1, + resource: { type: 'credential_group', id: 'group-1' }, + statements: [FIRST_STATEMENT, SECOND_STATEMENT], +} + +const EVERY_CONDITION_OPERATOR: ResourcePolicyCondition = { + StringEquals: { 'test:StringEquals': 'one' }, + StringNotEquals: { 'test:StringNotEquals': ['one', 'two'] }, + StringLike: { 'test:StringLike': 'finance-*' }, + StringNotLike: { 'test:StringNotLike': ['sandbox-*', 'test-*'] }, + Bool: { 'test:Bool': [true, false] }, + Null: { 'test:Null': false }, + 'ForAnyValue:StringEquals': { 'test:ForAnyValue': ['one', 'two'] }, + 'ForAllValues:StringEquals': { 'test:ForAllValues': 'required' }, +} + +describe('resource policy editor model', () => { + it('serializes the canonical document for dirty-state comparison', () => { + expect(serializeResourcePolicyDocument(DOCUMENT)).toBe(JSON.stringify(DOCUMENT, null, 2)) + }) + + it('creates unique human-readable statement IDs', () => { + const documentWithDefaultSid: ResourcePolicyDocument = { + ...DOCUMENT, + statements: [{ ...FIRST_STATEMENT, sid: 'NewAccessRule' }, SECOND_STATEMENT], + } + + expect(createUniqueResourcePolicyStatementSid(DOCUMENT, 'finance read-only')).toBe( + 'FinanceReadOnly2' + ) + expect(createUniqueResourcePolicyStatementSid(documentWithDefaultSid)).toBe('NewAccessRule2') + }) + + it('adds, replaces, duplicates, and removes statements without changing untouched fields', () => { + const inserted: ResourcePolicyStatement = { + sid: 'InsertedRule', + effect: 'allow', + actions: ['credential_groups.credentials.use'], + principals: [{ type: 'user', userId: 'user-2' }], + } + const added = addResourcePolicyStatement(DOCUMENT, inserted, 1) + expect(added.resource).toEqual(DOCUMENT.resource) + expect(added.statements).toEqual([FIRST_STATEMENT, inserted, SECOND_STATEMENT]) + + const replacement: ResourcePolicyStatement = { ...FIRST_STATEMENT, effect: 'deny' } + const replaced = replaceResourcePolicyStatement(DOCUMENT, 0, replacement) + expect(replaced.statements[0]).toEqual(replacement) + expect(replaced.statements[1]).toEqual(SECOND_STATEMENT) + + const duplicated = duplicateResourcePolicyStatement(DOCUMENT, 0) + expect(duplicated.statements[0]).toEqual(FIRST_STATEMENT) + expect(duplicated.statements[1]).toEqual({ ...FIRST_STATEMENT, sid: 'FinanceReadOnlyCopy' }) + expect(duplicated.statements[2]).toEqual(SECOND_STATEMENT) + expect(removeResourcePolicyStatement(duplicated, 1)).toEqual(DOCUMENT) + }) + + it('fails fast on invalid indexes, duplicate statement IDs, and duplicate principals', () => { + expect(() => removeResourcePolicyStatement(DOCUMENT, -1)).toThrow(RangeError) + expect(() => replaceResourcePolicyStatement(DOCUMENT, 2, FIRST_STATEMENT)).toThrow(RangeError) + expect(() => addResourcePolicyStatement(DOCUMENT, FIRST_STATEMENT)).toThrow( + 'statement IDs must be unique' + ) + expect(() => + replaceResourcePolicyStatement(DOCUMENT, 0, { + ...FIRST_STATEMENT, + principals: [ + { type: 'workflow', workflowId: 'workflow-1' }, + { type: 'workflow', workflowId: 'workflow-1' }, + ], + }) + ).toThrow('principals must be unique') + }) + + it('flattens and rebuilds every condition operator without changing value types', () => { + const rows = flattenResourcePolicyCondition(EVERY_CONDITION_OPERATOR) + + expect(rows.map(({ operator }) => operator)).toEqual([ + 'StringEquals', + 'StringNotEquals', + 'StringLike', + 'StringNotLike', + 'Bool', + 'Null', + 'ForAnyValue:StringEquals', + 'ForAllValues:StringEquals', + ]) + expect(rebuildResourcePolicyCondition(rows)).toEqual(EVERY_CONDITION_OPERATOR) + expect(rebuildResourcePolicyCondition([])).toBeUndefined() + expect(() => rebuildResourcePolicyCondition([rows[0], rows[0]])).toThrow( + 'Duplicate StringEquals condition key test:StringEquals' + ) + }) +}) diff --git a/apps/sim/components/permissions/resource-policy-editor/resource-policy-editor-model.ts b/apps/sim/components/permissions/resource-policy-editor/resource-policy-editor-model.ts new file mode 100644 index 00000000000..62a9000bab6 --- /dev/null +++ b/apps/sim/components/permissions/resource-policy-editor/resource-policy-editor-model.ts @@ -0,0 +1,158 @@ +import { + parseResourcePolicyDocument, + RESOURCE_POLICY_CONDITION_OPERATORS, + type ResourcePolicyCondition, + type ResourcePolicyConditionOperator, + type ResourcePolicyDocument, + type ResourcePolicyStatement, + resourcePolicyConditionSchema, +} from '@/lib/resource-policies/types' + +export type ResourcePolicyConditionValue = string | string[] | boolean | boolean[] + +export interface ResourcePolicyConditionRow { + operator: ResourcePolicyConditionOperator + key: string + value: ResourcePolicyConditionValue +} + +const DEFAULT_STATEMENT_SID = 'NewAccessRule' +const MAX_STATEMENT_SID_LENGTH = 128 + +function validateDocument(document: ResourcePolicyDocument): ResourcePolicyDocument { + return parseResourcePolicyDocument(document, document.resource) +} + +function assertStatementIndex(index: number, statementCount: number, allowEnd: boolean): void { + const maximum = allowEnd ? statementCount : statementCount - 1 + if (!Number.isInteger(index) || index < 0 || index > maximum) { + throw new RangeError(`Resource policy statement index ${index} is out of bounds`) + } +} + +function normalizeStatementSid(preferredSid: string): string { + const words = preferredSid + .trim() + .split(/[^A-Za-z0-9]+/) + .filter(Boolean) + const normalized = words.map((word) => `${word.charAt(0).toUpperCase()}${word.slice(1)}`).join('') + return normalized || DEFAULT_STATEMENT_SID +} + +function uniqueStatementSid( + statements: readonly ResourcePolicyStatement[], + preferredSid: string +): string { + const existingSids = new Set(statements.map(({ sid }) => sid)) + const base = normalizeStatementSid(preferredSid) + for (let candidateNumber = 1; candidateNumber <= statements.length + 1; candidateNumber += 1) { + const suffix = candidateNumber === 1 ? '' : `${candidateNumber}` + const candidate = `${base.slice(0, MAX_STATEMENT_SID_LENGTH - suffix.length)}${suffix}` + if (!existingSids.has(candidate)) return candidate + } + throw new Error('Could not create a unique resource policy statement ID') +} + +export function serializeResourcePolicyDocument(document: ResourcePolicyDocument): string { + return JSON.stringify(validateDocument(document), null, 2) +} + +export function createUniqueResourcePolicyStatementSid( + document: ResourcePolicyDocument, + preferredSid = DEFAULT_STATEMENT_SID +): string { + return uniqueStatementSid(validateDocument(document).statements, preferredSid) +} + +export function addResourcePolicyStatement( + document: ResourcePolicyDocument, + statement: ResourcePolicyStatement, + index = document.statements.length +): ResourcePolicyDocument { + const canonicalDocument = validateDocument(document) + assertStatementIndex(index, canonicalDocument.statements.length, true) + const statements = [...canonicalDocument.statements] + statements.splice(index, 0, statement) + return parseResourcePolicyDocument( + { ...canonicalDocument, statements }, + canonicalDocument.resource + ) +} + +export function replaceResourcePolicyStatement( + document: ResourcePolicyDocument, + index: number, + statement: ResourcePolicyStatement +): ResourcePolicyDocument { + const canonicalDocument = validateDocument(document) + assertStatementIndex(index, canonicalDocument.statements.length, false) + const statements = [...canonicalDocument.statements] + statements[index] = statement + return parseResourcePolicyDocument( + { ...canonicalDocument, statements }, + canonicalDocument.resource + ) +} + +export function removeResourcePolicyStatement( + document: ResourcePolicyDocument, + index: number +): ResourcePolicyDocument { + const canonicalDocument = validateDocument(document) + assertStatementIndex(index, canonicalDocument.statements.length, false) + const statements = [...canonicalDocument.statements] + statements.splice(index, 1) + return parseResourcePolicyDocument( + { ...canonicalDocument, statements }, + canonicalDocument.resource + ) +} + +export function duplicateResourcePolicyStatement( + document: ResourcePolicyDocument, + index: number +): ResourcePolicyDocument { + const canonicalDocument = validateDocument(document) + assertStatementIndex(index, canonicalDocument.statements.length, false) + const source = canonicalDocument.statements[index] + if (!source) throw new Error(`Resource policy statement ${index} does not exist`) + const duplicate: ResourcePolicyStatement = { + ...structuredClone(source), + sid: uniqueStatementSid(canonicalDocument.statements, `${source.sid}Copy`), + } + return addResourcePolicyStatement(canonicalDocument, duplicate, index + 1) +} + +export function flattenResourcePolicyCondition( + condition: ResourcePolicyCondition | undefined +): ResourcePolicyConditionRow[] { + if (!condition) return [] + const canonicalCondition = resourcePolicyConditionSchema.parse(condition) + const rows: ResourcePolicyConditionRow[] = [] + for (const operator of RESOURCE_POLICY_CONDITION_OPERATORS) { + const entries = canonicalCondition[operator] + if (!entries) continue + for (const [key, value] of Object.entries(entries)) { + rows.push({ operator, key, value: structuredClone(value) }) + } + } + return rows +} + +export function rebuildResourcePolicyCondition( + rows: readonly ResourcePolicyConditionRow[] +): ResourcePolicyCondition | undefined { + if (rows.length === 0) return undefined + const candidate: Partial< + Record> + > = {} + for (const row of rows) { + const entries = candidate[row.operator] ?? {} + if (Object.hasOwn(entries, row.key)) { + throw new Error(`Duplicate ${row.operator} condition key ${row.key}`) + } + entries[row.key] = structuredClone(row.value) + candidate[row.operator] = entries + } + return resourcePolicyConditionSchema.parse(candidate) +} diff --git a/apps/sim/components/permissions/resource-policy-editor/resource-policy-editor-types.ts b/apps/sim/components/permissions/resource-policy-editor/resource-policy-editor-types.ts new file mode 100644 index 00000000000..1a8a64ed1b7 --- /dev/null +++ b/apps/sim/components/permissions/resource-policy-editor/resource-policy-editor-types.ts @@ -0,0 +1,21 @@ +import type { ResourcePolicyDocument } from '@/lib/resource-policies/types' + +export interface ResourcePolicyIdentityOption { + value: string + label: string +} + +export interface ResourcePolicyPrincipalOptions { + user?: readonly ResourcePolicyIdentityOption[] + workflow?: readonly ResourcePolicyIdentityOption[] + access_control_group?: readonly ResourcePolicyIdentityOption[] +} + +export interface ResourcePolicyEditorProps { + document: ResourcePolicyDocument + revision: number + onDocumentChange: (document: ResourcePolicyDocument, expectedRevision: number) => void + error: string | null + disabled?: boolean + principalOptions?: ResourcePolicyPrincipalOptions +} diff --git a/apps/sim/components/permissions/resource-policy-editor/resource-policy-editor.test.tsx b/apps/sim/components/permissions/resource-policy-editor/resource-policy-editor.test.tsx new file mode 100644 index 00000000000..a409e6213e7 --- /dev/null +++ b/apps/sim/components/permissions/resource-policy-editor/resource-policy-editor.test.tsx @@ -0,0 +1,219 @@ +/** + * @vitest-environment jsdom + */ +import { act } from 'react' +import { createRoot, type Root } from 'react-dom/client' +import { afterEach, describe, expect, it, vi } from 'vitest' +import type { ResourcePolicyEditorProps } from '@/components/permissions/resource-policy-editor/resource-policy-editor-types' +import type { ResourcePolicyDocument, ResourcePolicyStatement } from '@/lib/resource-policies/types' + +const mocks = vi.hoisted(() => ({ + ruleModalProps: null as { + onSave: (statement: ResourcePolicyStatement) => string | null + } | null, + toastError: vi.fn(), +})) + +vi.mock('@sim/emcn', () => ({ + Badge: ({ children }: { children: React.ReactNode }) => {children}, + Chip: ({ + children, + onClick, + disabled, + }: { + children: React.ReactNode + onClick?: () => void + disabled?: boolean + }) => ( + + ), + toast: { error: mocks.toastError }, +})) + +vi.mock('@/app/workspace/[workspaceId]/settings/components/row-actions-menu', () => ({ + RowActionsMenu: ({ actions }: { actions: { label: string; onSelect: () => void }[] }) => ( +
+ {actions.map((action) => ( + + ))} +
+ ), +})) + +vi.mock('@/app/workspace/[workspaceId]/settings/components/settings-empty-state', () => ({ + SettingsEmptyState: ({ children }: { children: React.ReactNode }) =>
{children}
, +})) + +vi.mock('@/app/workspace/[workspaceId]/settings/components/settings-resource-row', () => ({ + RESOURCE_LIST_STACK: '', + SettingsResourceRow: ({ + title, + description, + badge, + trailing, + }: { + title: React.ReactNode + description?: React.ReactNode + badge?: React.ReactNode + trailing?: React.ReactNode + }) => ( +
+ {title} + {description} + {badge} + {trailing} +
+ ), +})) + +vi.mock( + '@/app/workspace/[workspaceId]/settings/components/settings-section/settings-section', + () => ({ + SettingsSection: ({ + label, + action, + children, + }: { + label: React.ReactNode + action?: React.ReactNode + children: React.ReactNode + }) => ( +
+

{label}

+ {action} + {children} +
+ ), + }) +) + +vi.mock('@/components/permissions/resource-policy-editor/resource-policy-rule-modal', () => ({ + ResourcePolicyRuleModal: (props: { + onSave: (statement: ResourcePolicyStatement) => string | null + }) => { + mocks.ruleModalProps = props + return
Rule modal
+ }, +})) + +import { ResourcePolicyEditor } from '@/components/permissions/resource-policy-editor/resource-policy-editor' + +const EMPTY_DOCUMENT: ResourcePolicyDocument = { + version: 1, + resource: { type: 'credential_group', id: 'group-1' }, + statements: [], +} + +const RULE_DOCUMENT: ResourcePolicyDocument = { + ...EMPTY_DOCUMENT, + statements: [ + { + sid: 'FinanceWorkflow', + effect: 'allow', + actions: ['credential_groups.credentials.use'], + principals: [{ type: 'workflow', workflowId: 'workflow-1' }], + condition: { StringEquals: { 'sim:WorkflowMode': 'deployment' } }, + }, + ], +} + +const roots: Root[] = [] + +function renderEditor(overrides: Partial = {}) { + ;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true + const container = document.createElement('div') + const root = createRoot(container) + roots.push(root) + const baseProps: ResourcePolicyEditorProps = { + document: EMPTY_DOCUMENT, + revision: 7, + onDocumentChange: vi.fn(), + error: null, + } + let props = { ...baseProps, ...overrides } + + const rerender = (next: Partial = {}) => { + props = { ...props, ...next } + act(() => root.render()) + } + rerender() + + const button = (label: string) => { + const match = [...container.querySelectorAll('button')].find( + (candidate) => candidate.textContent === label + ) + if (!(match instanceof HTMLButtonElement)) throw new Error(`Button ${label} not found`) + return match + } + + return { container, getProps: () => props, rerender, button } +} + +afterEach(() => { + vi.clearAllMocks() + mocks.ruleModalProps = null + act(() => { + for (const root of roots.splice(0)) root.unmount() + }) +}) + +describe('ResourcePolicyEditor', () => { + it('shows only the guided rule editor', () => { + const editor = renderEditor() + + expect(editor.container.textContent).toContain('No custom policy rules') + expect(editor.container.querySelector('textarea')).toBeNull() + expect( + [...editor.container.querySelectorAll('button')].map((button) => button.textContent) + ).toEqual(['Add rule']) + }) + + it('pins the document revision when an added rule is saved', () => { + const onDocumentChange = vi.fn() + const editor = renderEditor({ onDocumentChange }) + act(() => editor.button('Add rule').click()) + if (!mocks.ruleModalProps) throw new Error('Rule modal did not open') + + const statement: ResourcePolicyStatement = { + sid: 'FinanceWorkflow', + effect: 'allow', + actions: ['credential_groups.credentials.use'], + principals: [{ type: 'workflow', workflowId: 'workflow-1' }], + } + expect(mocks.ruleModalProps.onSave(statement)).toBeNull() + expect(onDocumentChange).toHaveBeenCalledWith({ ...EMPTY_DOCUMENT, statements: [statement] }, 7) + }) + + it('duplicates and removes rules through canonical document mutations', () => { + const onDocumentChange = vi.fn() + const editor = renderEditor({ + document: RULE_DOCUMENT, + onDocumentChange, + principalOptions: { + workflow: [{ value: 'workflow-1', label: 'Finance agent' }], + }, + }) + + expect(editor.container.textContent).toContain('Workflow: Finance agent') + expect(editor.container.textContent).toContain('1 condition') + + act(() => editor.button('Duplicate').click()) + expect(onDocumentChange).toHaveBeenLastCalledWith( + { + ...RULE_DOCUMENT, + statements: [ + RULE_DOCUMENT.statements[0], + { ...RULE_DOCUMENT.statements[0], sid: 'FinanceWorkflowCopy' }, + ], + }, + 7 + ) + + act(() => editor.button('Delete').click()) + expect(onDocumentChange).toHaveBeenLastCalledWith(EMPTY_DOCUMENT, 7) + }) +}) diff --git a/apps/sim/components/permissions/resource-policy-editor/resource-policy-editor.tsx b/apps/sim/components/permissions/resource-policy-editor/resource-policy-editor.tsx new file mode 100644 index 00000000000..62da353da47 --- /dev/null +++ b/apps/sim/components/permissions/resource-policy-editor/resource-policy-editor.tsx @@ -0,0 +1,263 @@ +'use client' + +import { useState } from 'react' +import { Badge, Chip, toast } from '@sim/emcn' +import { Ban, Plus, ShieldCheck } from '@sim/emcn/icons' +import { getErrorMessage } from '@sim/utils/errors' +import { + addResourcePolicyStatement, + createUniqueResourcePolicyStatementSid, + duplicateResourcePolicyStatement, + flattenResourcePolicyCondition, + removeResourcePolicyStatement, + replaceResourcePolicyStatement, +} from '@/components/permissions/resource-policy-editor/resource-policy-editor-model' +import type { + ResourcePolicyEditorProps, + ResourcePolicyPrincipalOptions, +} from '@/components/permissions/resource-policy-editor/resource-policy-editor-types' +import { ResourcePolicyRuleModal } from '@/components/permissions/resource-policy-editor/resource-policy-rule-modal' +import { + getResourcePolicyActionPresentation, + getResourcePolicyPrincipalPresentation, +} from '@/lib/resource-policies/presentation' +import { getResourcePolicyDefinition } from '@/lib/resource-policies/registry' +import type { + ResourcePolicyDocument, + ResourcePolicyPrincipal, + ResourcePolicyStatement, +} 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' + +interface RuleModalState { + index: number | null + statement: ResourcePolicyStatement + document: ResourcePolicyDocument + revision: number +} + +function resolveOptionLabel( + type: 'user' | 'workflow' | 'access_control_group', + value: string, + options?: ResourcePolicyPrincipalOptions +): string { + return options?.[type]?.find((option) => option.value === value)?.label ?? value +} + +function describePrincipal( + principal: ResourcePolicyPrincipal, + options?: ResourcePolicyPrincipalOptions +): string { + const presentation = getResourcePolicyPrincipalPresentation(principal.type) + switch (principal.type) { + case 'any': + return presentation.label + case 'user': + return `${presentation.label}: ${resolveOptionLabel('user', principal.userId, options)}` + case 'workflow': + return `${presentation.label}: ${resolveOptionLabel('workflow', principal.workflowId, options)}` + case 'access_control_group': + return `${presentation.label}: ${resolveOptionLabel( + 'access_control_group', + principal.accessControlGroupId, + options + )}` + case 'workspace_role': { + const role = presentation.valueOptions?.find( + (option) => option.value === principal.minimumRole + ) + if (!role) throw new Error(`Workspace role ${principal.minimumRole} has no presentation`) + return role.label + } + case 'external_identity': + return `${presentation.label}: ${principal.provider} / ${principal.tenantId} / ${principal.subjectId}` + } +} + +function summarizePrincipals( + principals: readonly ResourcePolicyPrincipal[], + options?: ResourcePolicyPrincipalOptions +): string { + const labels = principals.map((principal) => describePrincipal(principal, options)) + if (labels.length <= 2) return labels.join(' or ') + return `${labels.slice(0, 2).join(' or ')} +${labels.length - 2}` +} + +function createNewStatement(document: ResourcePolicyDocument): ResourcePolicyStatement { + const action = getResourcePolicyDefinition(document.resource.type).actions[0] + if (!action) { + throw new Error(`Resource policy type ${document.resource.type} has no registered actions`) + } + return { + sid: createUniqueResourcePolicyStatementSid(document), + effect: 'allow', + actions: [action], + principals: [{ type: 'workflow', workflowId: '' }], + } +} + +function ResourcePolicyRuleRow({ + statement, + document, + disabled, + principalOptions, + onEdit, + onDuplicate, + onDelete, +}: { + statement: ResourcePolicyStatement + document: ResourcePolicyDocument + disabled: boolean + principalOptions?: ResourcePolicyPrincipalOptions + onEdit: () => void + onDuplicate: () => void + onDelete: () => void +}) { + const actionLabels = statement.actions.map( + (action) => getResourcePolicyActionPresentation(document.resource.type, action).label + ) + const conditionCount = flattenResourcePolicyCondition(statement.condition).length + const description = [ + actionLabels.join(', '), + summarizePrincipals(statement.principals, principalOptions), + conditionCount > 0 + ? `${conditionCount} ${conditionCount === 1 ? 'condition' : 'conditions'}` + : null, + ] + .filter((part): part is string => Boolean(part)) + .join(' · ') + const RuleIcon = statement.effect === 'allow' ? ShieldCheck : Ban + + return ( + } + iconFilled + title={statement.sid} + description={description} + badge={ + + {statement.effect === 'allow' ? 'Allow' : 'Deny'} + + } + onClick={disabled ? undefined : onEdit} + clickLabel={`Edit ${statement.sid}`} + disabled={disabled} + trailing={ + disabled ? undefined : ( + + ) + } + /> + ) +} + +export function ResourcePolicyEditor({ + document, + revision, + onDocumentChange, + error, + disabled = false, + principalOptions, +}: ResourcePolicyEditorProps) { + const [ruleModal, setRuleModal] = useState(null) + + const updateDocument = (operation: () => ResourcePolicyDocument) => { + try { + onDocumentChange(operation(), revision) + } catch (caught) { + toast.error(getErrorMessage(caught, 'Could not update policy rule')) + } + } + + const saveRule = (statement: ResourcePolicyStatement): string | null => { + if (!ruleModal) return 'Access policy is unavailable' + try { + const nextDocument = + ruleModal.index === null + ? addResourcePolicyStatement(ruleModal.document, statement) + : replaceResourcePolicyStatement(ruleModal.document, ruleModal.index, statement) + onDocumentChange(nextDocument, ruleModal.revision) + return null + } catch (caught) { + return getErrorMessage(caught, 'Rule is invalid') + } + } + + const sectionAction = ( + + setRuleModal({ + index: null, + statement: createNewStatement(document), + document, + revision, + }) + } + disabled={disabled} + > + Add rule + + ) + + return ( + <> + + {error && ( +

+ {error} +

+ )} + + {document.statements.length === 0 ? ( + No custom policy rules + ) : ( +
+ {document.statements.map((statement, index) => ( + setRuleModal({ index, statement, document, revision })} + onDuplicate={() => + updateDocument(() => duplicateResourcePolicyStatement(document, index)) + } + onDelete={() => + updateDocument(() => removeResourcePolicyStatement(document, index)) + } + /> + ))} +
+ )} +
+ + {ruleModal && ( + setRuleModal(null)} + onSave={saveRule} + /> + )} + + ) +} diff --git a/apps/sim/components/permissions/resource-policy-editor/resource-policy-rule-modal.test.tsx b/apps/sim/components/permissions/resource-policy-editor/resource-policy-rule-modal.test.tsx new file mode 100644 index 00000000000..c0e0b13ef84 --- /dev/null +++ b/apps/sim/components/permissions/resource-policy-editor/resource-policy-rule-modal.test.tsx @@ -0,0 +1,179 @@ +/** + * @vitest-environment jsdom + */ + +import type { ReactNode } from 'react' +import { act } from 'react' +import { createRoot, type Root } from 'react-dom/client' +import { afterEach, describe, expect, it, vi } from 'vitest' +import type { ResourcePolicyDocument, ResourcePolicyStatement } from '@/lib/resource-policies/types' + +interface FieldAria { + 'aria-required'?: boolean + 'aria-invalid'?: boolean + 'aria-describedby'?: string +} + +vi.mock('@sim/emcn', () => ({ + Button: ({ children, ...props }: React.ButtonHTMLAttributes) => ( + + ), + ButtonGroup: ({ children }: { children: ReactNode }) =>
{children}
, + ButtonGroupItem: ({ children }: { children: ReactNode }) => ( + + ), + Chip: ({ children, onClick }: { children: ReactNode; onClick?: () => void }) => ( + + ), + ChipCombobox: ({ value, ...props }: { value?: string; 'aria-label'?: string }) => ( + + ), + ChipDropdown: ({ value, ...props }: { value?: string; 'aria-label'?: string }) => ( + + ), + ChipInput: ({ value, ...props }: { value?: string; 'aria-label'?: string }) => ( + + ), + ChipModal: ({ children }: { children: ReactNode }) =>
{children}
, + ChipModalBody: ({ children }: { children: ReactNode }) =>
{children}
, + ChipModalError: ({ children }: { children: ReactNode }) =>
{children}
, + ChipModalField: ({ + type, + title, + value, + children, + }: { + type: string + title: ReactNode + value?: string + children?: ReactNode | ((aria: FieldAria) => ReactNode) + }) => ( +
+ {title} + {type === 'input' ? ( + + ) : typeof children === 'function' ? ( + children({}) + ) : ( + children + )} +
+ ), + ChipModalFooter: ({ + onCancel, + primaryAction, + }: { + onCancel: () => void + primaryAction: { label: string; onClick: () => void; disabled?: boolean } + }) => ( +
+ + +
+ ), + ChipModalHeader: ({ children }: { children: ReactNode }) =>

{children}

, + ChipSelect: ({ value, ...props }: { value?: string; 'aria-label'?: string }) => ( + + ), + TagInput: ({ items }: { items: { value: string }[] }) => ( +
{items.map((item) => item.value).join(', ')}
+ ), +})) + +vi.mock('@sim/emcn/icons', () => ({ Plus: () => null, X: () => null })) + +import { ResourcePolicyRuleModal } from '@/components/permissions/resource-policy-editor/resource-policy-rule-modal' + +const DOCUMENT: ResourcePolicyDocument = { + version: 1, + resource: { type: 'credential_group', id: 'group-1' }, + statements: [], +} + +const EXISTING_STATEMENT: ResourcePolicyStatement = { + sid: 'ExcludeSandboxes', + effect: 'deny', + actions: ['credential_groups.credentials.use'], + principals: [{ type: 'workflow', workflowId: 'workflow-1' }], + condition: { + StringNotLike: { 'sim:WorkspaceId': ['sandbox-*', 'test-*'] }, + }, +} + +const roots: Root[] = [] + +function renderModal(statement: ResourcePolicyStatement, onSave = vi.fn(() => null)) { + ;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true + const container = document.createElement('div') + const root = createRoot(container) + roots.push(root) + const onClose = vi.fn() + act(() => + root.render( + + ) + ) + const button = (label: string) => { + const match = [...container.querySelectorAll('button')].find( + (candidate) => candidate.textContent === label + ) + if (!(match instanceof HTMLButtonElement)) throw new Error(`Button ${label} not found`) + return match + } + return { button, container, onClose, onSave } +} + +afterEach(() => { + act(() => { + for (const root of roots.splice(0)) root.unmount() + }) +}) + +describe('ResourcePolicyRuleModal', () => { + it('preserves an existing array condition and uses field-operator-value order', () => { + const modal = renderModal(EXISTING_STATEMENT) + const field = modal.container.querySelector('[aria-label="Condition 1 field"]') + const operator = modal.container.querySelector('[aria-label="Condition 1 operator"]') + if (!(field instanceof HTMLElement) || !(operator instanceof HTMLElement)) { + throw new Error('Condition controls did not render') + } + + expect(field.compareDocumentPosition(operator) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy() + expect(modal.container.textContent).toContain('sandbox-*, test-*') + expect(modal.container.textContent).not.toContain('Multiple values') + expect(modal.container.textContent).not.toContain('Remove') + + act(() => modal.button('Save rule').click()) + + expect(modal.onSave).toHaveBeenCalledWith(EXISTING_STATEMENT) + expect(modal.onClose).toHaveBeenCalledOnce() + }) + + it('does not allow an incomplete principal to be saved', () => { + const modal = renderModal({ + ...EXISTING_STATEMENT, + principals: [{ type: 'workflow', workflowId: '' }], + condition: undefined, + }) + + expect(modal.button('Save rule').disabled).toBe(true) + }) +}) diff --git a/apps/sim/components/permissions/resource-policy-editor/resource-policy-rule-modal.tsx b/apps/sim/components/permissions/resource-policy-editor/resource-policy-rule-modal.tsx new file mode 100644 index 00000000000..6e8b28281a8 --- /dev/null +++ b/apps/sim/components/permissions/resource-policy-editor/resource-policy-rule-modal.tsx @@ -0,0 +1,734 @@ +'use client' + +import { useState } from 'react' +import { + Button, + ButtonGroup, + ButtonGroupItem, + Chip, + ChipCombobox, + ChipDropdown, + ChipInput, + ChipModal, + ChipModalBody, + ChipModalError, + ChipModalField, + ChipModalFooter, + ChipModalHeader, + ChipSelect, + TagInput, +} from '@sim/emcn' +import { Plus, X } from '@sim/emcn/icons' +import { getErrorMessage } from '@sim/utils/errors' +import { omit } from '@sim/utils/object' +import { + flattenResourcePolicyCondition, + type ResourcePolicyConditionRow, + type ResourcePolicyConditionValue, + rebuildResourcePolicyCondition, +} from '@/components/permissions/resource-policy-editor/resource-policy-editor-model' +import type { ResourcePolicyPrincipalOptions } from '@/components/permissions/resource-policy-editor/resource-policy-editor-types' +import { + getResourcePolicyActionPresentation, + getResourcePolicyConditionOperatorPresentation, + getResourcePolicyConditionPresentation, + getResourcePolicyPrincipalPresentation, +} from '@/lib/resource-policies/presentation' +import { + GLOBAL_RESOURCE_POLICY_CONDITION_KEYS, + getResourcePolicyDefinition, + type ResourcePolicyResourceType, +} from '@/lib/resource-policies/registry' +import type { + ResourcePolicyConditionOperator, + ResourcePolicyDocument, + ResourcePolicyPrincipal, + ResourcePolicyStatement, +} from '@/lib/resource-policies/types' + +type PrincipalType = ResourcePolicyPrincipal['type'] +type ConditionOperator = ResourcePolicyConditionOperator +type ConditionValue = ResourcePolicyConditionValue +type ConditionEntry = ResourcePolicyConditionRow +type ResourcePolicyStatementDraft = Omit + +interface ResourcePolicyRuleModalProps { + document: ResourcePolicyDocument + statement: ResourcePolicyStatement + title: string + principalOptions?: ResourcePolicyPrincipalOptions + onClose: () => void + onSave: (statement: ResourcePolicyStatement) => string | null +} + +const PRINCIPAL_TYPE_ORDER = [ + 'workflow', + 'user', + 'workspace_role', + 'access_control_group', + 'external_identity', + 'any', +] as const satisfies readonly PrincipalType[] + +const PRINCIPAL_TYPE_OPTIONS = PRINCIPAL_TYPE_ORDER.map((value) => ({ + value, + label: getResourcePolicyPrincipalPresentation(value).label, +})) + +function requireWorkspaceRoleOptions() { + const options = getResourcePolicyPrincipalPresentation('workspace_role').valueOptions + if (!options) { + throw new Error('Workspace role principal presentation must define role options') + } + return options +} + +const WORKSPACE_ROLE_OPTIONS = requireWorkspaceRoleOptions() + +const BOOLEAN_OPTIONS = [ + { value: 'true', label: 'True' }, + { value: 'false', label: 'False' }, +] as const + +function createPrincipal(type: PrincipalType): ResourcePolicyPrincipal { + switch (type) { + case 'any': + return { type: 'any' } + case 'user': + return { type: 'user', userId: '' } + case 'workspace_role': + return { type: 'workspace_role', minimumRole: 'read' } + case 'access_control_group': + return { type: 'access_control_group', accessControlGroupId: '' } + case 'workflow': + return { type: 'workflow', workflowId: '' } + case 'external_identity': + return { type: 'external_identity', provider: '', tenantId: '', subjectId: '' } + } +} + +function requirePolicyEffect(value: string): ResourcePolicyStatement['effect'] { + if (value !== 'allow' && value !== 'deny') { + throw new Error(`Unknown resource policy effect ${value}`) + } + return value +} + +function conditionKeys(resourceType: ResourcePolicyResourceType): string[] { + return [ + ...Object.keys(GLOBAL_RESOURCE_POLICY_CONDITION_KEYS), + ...Object.keys(getResourcePolicyDefinition(resourceType).conditionKeys), + ] +} + +function defaultConditionValue(operator: ConditionOperator): ConditionValue { + if (operator === 'Bool' || operator === 'Null') return false + return '' +} + +function principalValue(principal: ResourcePolicyPrincipal): string { + switch (principal.type) { + case 'user': + return principal.userId + case 'workflow': + return principal.workflowId + case 'access_control_group': + return principal.accessControlGroupId + case 'workspace_role': + return principal.minimumRole + case 'external_identity': + case 'any': + return '' + } +} + +function isPrincipalComplete(principal: ResourcePolicyPrincipal): boolean { + switch (principal.type) { + case 'any': + case 'workspace_role': + return true + case 'user': + return Boolean(principal.userId.trim()) + case 'workflow': + return Boolean(principal.workflowId.trim()) + case 'access_control_group': + return Boolean(principal.accessControlGroupId.trim()) + case 'external_identity': + return Boolean( + principal.provider.trim() && principal.tenantId.trim() && principal.subjectId.trim() + ) + } +} + +function isConditionValueComplete(value: ConditionValue): boolean { + if (typeof value === 'boolean') return true + if (typeof value === 'string') return Boolean(value.trim()) + return ( + value.length > 0 && value.every((item) => typeof item === 'boolean' || Boolean(item.trim())) + ) +} + +function setPrincipalValue( + principal: ResourcePolicyPrincipal, + value: string +): ResourcePolicyPrincipal { + switch (principal.type) { + case 'user': + return { ...principal, userId: value } + case 'workflow': + return { ...principal, workflowId: value } + case 'access_control_group': + return { ...principal, accessControlGroupId: value } + case 'workspace_role': + if (value !== 'read' && value !== 'write' && value !== 'admin') { + throw new Error(`Unknown workspace role ${value}`) + } + return { ...principal, minimumRole: value } + case 'external_identity': + case 'any': + return principal + } +} + +function principalIdentityOptions( + principal: ResourcePolicyPrincipal, + options?: ResourcePolicyPrincipalOptions +) { + const available = + principal.type === 'user' || principal.type === 'workflow' + ? options?.[principal.type] + : principal.type === 'access_control_group' + ? options?.access_control_group + : undefined + const currentValue = principalValue(principal) + const hasCurrent = available?.some((option) => option.value === currentValue) ?? false + return [ + ...(available ?? []), + ...(!currentValue || hasCurrent + ? [] + : [{ value: currentValue, label: `Unavailable · ${currentValue}`, disabled: true }]), + ] +} + +function hasPrincipalIdentityCatalog( + principal: ResourcePolicyPrincipal, + options?: ResourcePolicyPrincipalOptions +): boolean { + if (principal.type === 'user' || principal.type === 'workflow') { + return options?.[principal.type] !== undefined + } + return principal.type === 'access_control_group' && options?.access_control_group !== undefined +} + +interface PrincipalEditorProps { + principal: ResourcePolicyPrincipal + index: number + options?: ResourcePolicyPrincipalOptions + onChange: (principal: ResourcePolicyPrincipal) => void + onRemove: () => void +} + +function PrincipalEditor({ principal, index, options, onChange, onRemove }: PrincipalEditorProps) { + const identityOptions = principalIdentityOptions(principal, options) + return ( +
+
+ onChange(createPrincipal(value as PrincipalType))} + options={PRINCIPAL_TYPE_OPTIONS} + aria-label={`Principal ${index + 1} type`} + align='start' + fullWidth + /> +
+
+ {principal.type === 'any' ? ( +
+ Matches every principal +
+ ) : principal.type === 'workspace_role' ? ( + onChange(setPrincipalValue(principal, value))} + options={WORKSPACE_ROLE_OPTIONS} + aria-label={`Principal ${index + 1} minimum workspace role`} + align='start' + fullWidth + /> + ) : principal.type === 'external_identity' ? ( +
+ onChange({ ...principal, provider: event.target.value })} + placeholder='Provider, e.g. slack' + aria-label={`Principal ${index + 1} provider`} + /> + onChange({ ...principal, tenantId: event.target.value })} + placeholder='Tenant ID' + aria-label={`Principal ${index + 1} tenant ID`} + inputClassName='font-mono' + /> + onChange({ ...principal, subjectId: event.target.value })} + placeholder='Subject ID' + aria-label={`Principal ${index + 1} subject ID`} + inputClassName='font-mono' + /> +
+ ) : hasPrincipalIdentityCatalog(principal, options) ? ( + onChange(setPrincipalValue(principal, value))} + placeholder={`Select ${getResourcePolicyPrincipalPresentation(principal.type).label.toLowerCase()}`} + searchable + aria-label={`Principal ${index + 1} ${principal.type.replaceAll('_', ' ')} ID`} + fullWidth + dropdownWidth='trigger' + /> + ) : ( + onChange(setPrincipalValue(principal, event.target.value))} + placeholder={`${getResourcePolicyPrincipalPresentation(principal.type).label} ID`} + aria-label={`Principal ${index + 1} ${principal.type.replaceAll('_', ' ')} ID`} + inputClassName='font-mono' + /> + )} +
+ +
+ ) +} + +interface ConditionValueEditorProps { + entry: ConditionEntry + index: number + resourceType: ResourcePolicyResourceType + onChange: (value: ConditionValue) => void +} + +function ConditionValueEditor({ entry, index, resourceType, onChange }: ConditionValueEditorProps) { + const presentation = getResourcePolicyConditionPresentation(resourceType, entry.key) + const valueOptions = presentation.valueOptions + if (entry.operator === 'Null') { + return ( + onChange(value === 'true')} + options={[ + { value: 'true', label: 'Is absent' }, + { value: 'false', label: 'Is present' }, + ]} + aria-label={`Condition ${index + 1} value`} + align='start' + fullWidth + /> + ) + } + if (entry.operator === 'Bool') { + if (Array.isArray(entry.value)) { + const values = entry.value.map(String) + return ( + onChange(next.map((value) => value === 'true'))} + placeholder='Select values' + aria-label={`Condition ${index + 1} values`} + fullWidth + dropdownWidth='trigger' + /> + ) + } + return ( + onChange(value === 'true')} + options={BOOLEAN_OPTIONS} + aria-label={`Condition ${index + 1} value`} + align='start' + fullWidth + /> + ) + } + if (Array.isArray(entry.value)) { + const values = entry.value.filter((value): value is string => typeof value === 'string') + return ( + ({ value, isValid: true }))} + onAdd={(value) => { + if (!value.trim() || values.includes(value)) return false + onChange([...values, value]) + return true + }} + onRemove={(_value, removeIndex) => + onChange(values.filter((_item, valueIndex) => valueIndex !== removeIndex)) + } + placeholder='Enter expected values' + placeholderWithTags='Add another value' + triggerKeys={['Enter']} + /> + ) + } + if (valueOptions) { + const currentValue = typeof entry.value === 'string' ? entry.value : '' + const isKnownValue = valueOptions.some((option) => option.value === currentValue) + if (isKnownValue) { + return ( + + ) + } + return ( + ({ value: option.value, label: option.label }))} + placeholder='Select or enter a value' + inputProps={{ 'aria-label': `Condition ${index + 1} value` }} + dropdownWidth='trigger' + /> + ) + } + return ( + onChange(event.target.value)} + placeholder='Expected value' + aria-label={`Condition ${index + 1} value`} + inputClassName='font-mono' + /> + ) +} + +interface ConditionEditorProps { + entry: ConditionEntry + index: number + resourceType: ResourcePolicyResourceType + onChange: (entry: ConditionEntry) => void + onRemove: () => void +} + +function ConditionEditor({ entry, index, resourceType, onChange, onRemove }: ConditionEditorProps) { + const presentation = getResourcePolicyConditionPresentation(resourceType, entry.key) + const operatorOptions = presentation.operators.map((operator) => ({ + value: operator, + label: getResourcePolicyConditionOperatorPresentation(operator).label, + })) + const keyOptions = conditionKeys(resourceType).map((key) => ({ + value: key, + label: getResourcePolicyConditionPresentation(resourceType, key).label, + })) + return ( +
+
+ { + const nextOperators = getResourcePolicyConditionPresentation( + resourceType, + key + ).operators + const operator = nextOperators.includes(entry.operator) + ? entry.operator + : nextOperators[0] + if (!operator) throw new Error(`Condition key ${key} has no supported operators`) + onChange({ operator, key, value: defaultConditionValue(operator) }) + }} + placeholder='Field' + searchable + aria-label={`Condition ${index + 1} field`} + fullWidth + dropdownWidth='trigger' + /> +
+
+ { + const operator = value as ConditionOperator + onChange({ ...entry, operator, value: defaultConditionValue(operator) }) + }} + options={operatorOptions} + aria-label={`Condition ${index + 1} operator`} + align='start' + fullWidth + /> +
+
+ onChange({ ...entry, value })} + /> +
+ +
+ ) +} + +export function ResourcePolicyRuleModal({ + document, + statement, + title, + principalOptions, + onClose, + onSave, +}: ResourcePolicyRuleModalProps) { + const [draft, setDraft] = useState(() => + omit(structuredClone(statement), ['condition']) + ) + const [conditions, setConditions] = useState(() => + flattenResourcePolicyCondition(statement.condition) + ) + const [error, setError] = useState(null) + const resourceDefinition = getResourcePolicyDefinition(document.resource.type) + + const updateDraft = (next: ResourcePolicyStatementDraft) => { + setError(null) + setDraft(next) + } + + const handleSave = () => { + try { + const nextStatement: ResourcePolicyStatement = { + ...draft, + sid: draft.sid.trim(), + condition: rebuildResourcePolicyCondition(conditions), + } + const saveError = onSave(nextStatement) + if (saveError) { + setError(saveError) + return + } + onClose() + } catch (caught) { + setError(getErrorMessage(caught, 'Rule is invalid')) + } + } + + const saveDisabled = + !draft.sid.trim() || + draft.actions.length === 0 || + draft.principals.length === 0 || + draft.principals.some((principal) => !isPrincipalComplete(principal)) || + conditions.some((condition) => !isConditionValueComplete(condition.value)) + + return ( + !open && onClose()} srTitle={title} size='lg'> + {title} + + updateDraft({ ...draft, sid })} + placeholder='FinanceWorkflowAccess' + required + mono + /> + + {(aria) => ( + + updateDraft({ ...draft, effect: requirePolicyEffect(effect) }) + } + aria-label='Rule effect' + {...aria} + > + Allow + Deny + + )} + + + {(aria) => ( +
+ ({ + value: action, + label: getResourcePolicyActionPresentation(document.resource.type, action).label, + }))} + multiSelect + multiSelectValues={draft.actions} + onMultiSelectChange={(actions) => + updateDraft({ + ...draft, + actions: actions.filter( + (action): action is ResourcePolicyStatement['actions'][number] => + resourceDefinition.actions.includes( + action as ResourcePolicyStatement['actions'][number] + ) + ), + }) + } + placeholder='Select actions' + aria-label='Rule actions' + fullWidth + dropdownWidth='trigger' + /> +
+ )} +
+ + {(aria) => ( +
+ {draft.principals.map((principal, index) => ( + + updateDraft({ + ...draft, + principals: draft.principals.map((item, principalIndex) => + principalIndex === index ? nextPrincipal : item + ), + }) + } + onRemove={() => + updateDraft({ + ...draft, + principals: draft.principals.filter( + (_item, principalIndex) => principalIndex !== index + ), + }) + } + /> + ))} + + updateDraft({ + ...draft, + principals: [...draft.principals, createPrincipal('workflow')], + }) + } + > + Add principal + +
+ )} +
+ + {(aria) => ( +
+ {conditions.map((condition, index) => ( + { + setError(null) + setConditions((current) => + current.map((item, conditionIndex) => + conditionIndex === index ? nextCondition : item + ) + ) + }} + onRemove={() => { + setError(null) + setConditions((current) => + current.filter((_item, conditionIndex) => conditionIndex !== index) + ) + }} + /> + ))} + { + const key = conditionKeys(document.resource.type)[0] + if (!key) throw new Error('Resource policy has no registered condition keys') + const operators = getResourcePolicyConditionPresentation( + document.resource.type, + key + ).operators + const operator = operators[0] + if (!operator) throw new Error(`Condition key ${key} has no supported operators`) + setConditions((current) => [ + ...current, + { operator, key, value: defaultConditionValue(operator) }, + ]) + }} + > + Add condition + +
+ )} +
+ {error && {error}} +
+ +
+ ) +} 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..73b176088ee --- /dev/null +++ b/apps/sim/ee/credential-groups/components/credential-group-access.test.tsx @@ -0,0 +1,261 @@ +/** + * @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 { ResourcePolicyEditorProps } from '@/components/permissions/resource-policy-editor' +import type { ResourcePolicyDocument } from '@/lib/resource-policies/types' + +const mocks = vi.hoisted(() => ({ + mutationError: null as Error | null, + mutateAsync: vi.fn(), + policyEditorProps: null as ResourcePolicyEditorProps | null, + reset: vi.fn(), + toastError: vi.fn(), + toastSuccess: vi.fn(), + useAccess: vi.fn(), +})) + +vi.mock('@sim/emcn', () => ({ + 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, + }), +})) + +vi.mock('@/components/permissions/resource-policy-editor', () => ({ + ResourcePolicyEditor: (props: ResourcePolicyEditorProps) => { + mocks.policyEditorProps = props + return
Policy editor
+ }, +})) + +vi.mock('@/app/workspace/[workspaceId]/settings/components/settings-empty-state', () => ({ + SettingsEmptyState: ({ children }: { children: React.ReactNode }) =>
{children}
, +})) + +import { + CredentialGroupAccess, + 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.policyEditorProps = null + mocks.reset.mockImplementation(() => { + mocks.mutationError = null + }) + mocks.useAccess.mockReturnValue({ + data: { + revision: 3, + document: EMPTY_DOCUMENT, + users: [{ userId: 'user-1', name: 'Test User', email: 'user@example.com' }], + workflows: [{ id: 'workflow-1', name: 'Finance workflow' }], + permissionGroups: [{ id: 'permission-group-1', name: 'Finance' }], + }, + 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('starts from the canonical stored policy and fails fast on a mismatched resource', () => { + const editor = renderEditor() + + expect(editor.getResult().document).toEqual(EMPTY_DOCUMENT) + expect(editor.getResult().revision).toBe(3) + expect(editor.getResult().users).toEqual([ + { userId: 'user-1', name: 'Test User', email: 'user@example.com' }, + ]) + expect(editor.getResult().workflows).toEqual([{ id: 'workflow-1', name: 'Finance workflow' }]) + expect(editor.getResult().permissionGroups).toEqual([ + { id: 'permission-group-1', name: 'Finance' }, + ]) + expect(editor.getResult().dirty).toBe(false) + + expect(() => + act(() => + editor + .getResult() + .setDocument( + { ...EMPTY_DOCUMENT, resource: { type: 'credential_group', id: 'group-2' } }, + 3 + ) + ) + ).toThrow('does not match its canonical resource') + }) + + it('pins the revision and preserves the draft when a concurrent update conflicts', async () => { + const editor = renderEditor() + act(() => editor.getResult().setDocument(EDITED_DOCUMENT, 3)) + + mocks.useAccess.mockReturnValue({ + data: { + revision: 4, + document: EMPTY_DOCUMENT, + users: [], + workflows: [], + permissionGroups: [], + }, + 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().document).toEqual(EDITED_DOCUMENT) + expect(editor.getResult().revision).toBe(3) + 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().setDocument(EDITED_DOCUMENT, 3)) + + act(() => editor.getResult().discard()) + + expect(editor.getResult().dirty).toBe(false) + expect(editor.getResult().document).toEqual(EMPTY_DOCUMENT) + }) + + it('rejects a rule saved against a stale revision before creating a draft', () => { + const editor = renderEditor() + mocks.useAccess.mockReturnValue({ + data: { + revision: 4, + document: EMPTY_DOCUMENT, + users: [], + workflows: [], + permissionGroups: [], + }, + error: null, + isPending: false, + }) + editor.rerender() + + expect(() => act(() => editor.getResult().setDocument(EDITED_DOCUMENT, 3))).toThrow( + 'changed while the rule was being edited' + ) + expect(editor.getResult().dirty).toBe(false) + }) + + it('passes labeled user, workflow, and permission-group catalogs to the shared editor', () => { + const container = document.createElement('div') + const root = createRoot(container) + mountedRoots.push(root) + act(() => + root.render( + + ) + ) + + expect(mocks.policyEditorProps?.principalOptions).toEqual({ + workflow: [{ value: 'workflow-1', label: 'Finance workflow' }], + user: [{ value: 'user-1', label: 'Test User · user@example.com' }], + access_control_group: [{ value: 'permission-group-1', label: 'Finance' }], + }) + }) + + it('fails fast when a principal catalog is unavailable', () => { + expect(() => + CredentialGroupAccess({ + document: EMPTY_DOCUMENT, + revision: 3, + users: null, + workflows: [], + permissionGroups: [], + onDocumentChange: vi.fn(), + error: null, + isPending: false, + loadError: null, + saving: false, + }) + ).toThrow('Credential Group user catalog is unavailable') + }) +}) 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..edb41647c16 --- /dev/null +++ b/apps/sim/ee/credential-groups/components/credential-group-access.tsx @@ -0,0 +1,182 @@ +'use client' + +import { useState } from 'react' +import { toast } from '@sim/emcn' +import { getErrorMessage } from '@sim/utils/errors' +import { ResourcePolicyEditor } from '@/components/permissions/resource-policy-editor' +import { serializeResourcePolicyDocument } from '@/components/permissions/resource-policy-editor/resource-policy-editor-model' +import type { CredentialGroupAccessResponse } from '@/lib/api/contracts/credential-groups' +import { + parseResourcePolicyDocument, + type ResourcePolicyDocument, +} from '@/lib/resource-policies/types' +import { SettingsEmptyState } from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state' +import { + useCredentialGroupAccess, + useUpdateCredentialGroupAccess, +} from '@/hooks/queries/credential-groups' + +interface PolicyDraft { + document: ResourcePolicyDocument + baseline: string + expectedRevision: number + groupId: string +} + +interface UseCredentialGroupAccessEditorProps { + workspaceId: string + groupId: string +} + +export function useCredentialGroupAccessEditor({ + workspaceId, + groupId, +}: UseCredentialGroupAccessEditorProps) { + const access = useCredentialGroupAccess(workspaceId, groupId) + const updateAccess = useUpdateCredentialGroupAccess() + const [draft, setDraft] = useState(null) + + if (draft && draft.groupId !== groupId) { + throw new Error('Credential Group access draft cannot move between resources') + } + const persistedDocument = access.data + ? parseResourcePolicyDocument(access.data.document, { type: 'credential_group', id: groupId }) + : null + const persistedValue = persistedDocument ? serializeResourcePolicyDocument(persistedDocument) : '' + const document = draft?.document ?? persistedDocument + const revision = draft?.expectedRevision ?? access.data?.revision ?? null + const dirty = draft !== null + + const setDocument = (nextDocument: ResourcePolicyDocument, expectedRevision: number) => { + if (!access.data) throw new Error('Credential Group access policy is unavailable') + const currentRevision = draft?.expectedRevision ?? access.data.revision + if (expectedRevision !== currentRevision) { + throw new Error('Resource policy changed while the rule was being edited') + } + const canonicalDocument = parseResourcePolicyDocument(nextDocument, { + type: 'credential_group', + id: groupId, + }) + const nextValue = serializeResourcePolicyDocument(canonicalDocument) + updateAccess.reset() + setDraft((current) => { + const baseline = current?.baseline ?? persistedValue + if (nextValue === baseline) return null + return { + document: canonicalDocument, + baseline, + expectedRevision: current?.expectedRevision ?? access.data.revision, + groupId, + } + }) + } + + const discard = () => { + setDraft(null) + updateAccess.reset() + } + + const save = async () => { + if (!draft) return + try { + await updateAccess.mutateAsync({ + workspaceId, + groupId, + body: { + expectedRevision: draft.expectedRevision, + document: draft.document, + }, + }) + setDraft(null) + toast.success('Access policy saved') + } catch (error) { + const message = getErrorMessage(error, 'Could not update access policy') + toast.error(message) + } + } + + return { + document, + revision, + users: access.data?.users ?? null, + workflows: access.data?.workflows ?? null, + permissionGroups: access.data?.permissionGroups ?? null, + setDocument, + discard, + save, + dirty, + 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, + } +} + +interface CredentialGroupAccessProps { + document: ResourcePolicyDocument | null + revision: number | null + users: CredentialGroupAccessResponse['users'] | null + workflows: CredentialGroupAccessResponse['workflows'] | null + permissionGroups: CredentialGroupAccessResponse['permissionGroups'] | null + onDocumentChange: (document: ResourcePolicyDocument, expectedRevision: number) => void + error: string | null + isPending: boolean + loadError: unknown + saving: boolean +} + +export function CredentialGroupAccess({ + document, + revision, + users, + workflows, + permissionGroups, + onDocumentChange, + error, + isPending, + loadError, + saving, +}: CredentialGroupAccessProps) { + if (loadError) { + return ( + + {getErrorMessage(loadError, "Couldn't load access policy")} + + ) + } + if (isPending) return null + if (!users) throw new Error('Credential Group user catalog is unavailable') + if (!workflows) throw new Error('Credential Group workflow catalog is unavailable') + if (!document) throw new Error('Credential Group access policy is unavailable') + if (revision === null) throw new Error('Credential Group access policy revision is unavailable') + if (!permissionGroups) { + throw new Error('Credential Group permission group catalog is unavailable') + } + + return ( + ({ + value: workflow.id, + label: workflow.name, + })), + user: users.map((user) => ({ + value: user.userId, + label: user.name ? `${user.name} · ${user.email}` : user.email, + })), + access_control_group: permissionGroups.map((group) => ({ + value: group.id, + label: group.name, + })), + }} + /> + ) +} 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..943e692fa19 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,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, + 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 { @@ -45,11 +49,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 { @@ -97,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, @@ -128,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) @@ -150,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' @@ -165,15 +167,24 @@ 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, + }, + ] + : saveDiscardActions({ + dirty: accessEditor.dirty, + saving: accessEditor.saving, + onSave: () => void accessEditor.save(), + onDiscard: accessEditor.discard, + saveDisabled: !accessEditor.isReady, + saveTooltip: !accessEditor.isReady ? 'Access policy is unavailable' : undefined, + })), { id: 'delete', text: deleteGroup.isPending ? 'Deleting...' : 'Delete', @@ -306,6 +317,22 @@ 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 6f348655939..8795e404058 100644 --- a/apps/sim/executor/handlers/workflow/workflow-handler.test.ts +++ b/apps/sim/executor/handlers/workflow/workflow-handler.test.ts @@ -408,7 +408,13 @@ describe('WorkflowBlockHandler', () => { json: () => Promise.resolve({ data: { - deployedState: { blocks: {}, edges: [], loops: {}, parallels: {} }, + deployedState: { + blocks: {}, + edges: [], + loops: {}, + parallels: {}, + deploymentVersionId: 'deployment-version-1', + }, }, }), } @@ -600,7 +606,13 @@ describe('WorkflowBlockHandler', () => { json: () => Promise.resolve({ data: { - deployedState: { blocks: {}, edges: [], loops: {}, parallels: {} }, + deployedState: { + blocks: {}, + edges: [], + loops: {}, + parallels: {}, + deploymentVersionId: 'deployment-version-1', + }, }, }), } @@ -637,6 +649,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', @@ -700,6 +717,7 @@ describe('WorkflowBlockHandler', () => { edges: [], loops: {}, parallels: {}, + deploymentVersionId: 'deployment-version-1', }, }, }), @@ -798,6 +816,7 @@ describe('WorkflowBlockHandler', () => { edges: [], loops: {}, parallels: {}, + deploymentVersionId: 'deployment-version-1', }, }, }), @@ -1203,7 +1222,15 @@ describe('WorkflowBlockHandler', () => { ok: true, json: () => Promise.resolve({ - data: { deployedState: { blocks: {}, edges: [], loops: {}, parallels: {} } }, + data: { + deployedState: { + blocks: {}, + edges: [], + loops: {}, + parallels: {}, + deploymentVersionId: 'deployment-version-1', + }, + }, }), } } @@ -1617,6 +1644,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', @@ -1943,6 +1975,7 @@ describe('WorkflowBlockHandler', () => { edges: [], loops: {}, parallels: {}, + deploymentVersionId: 'deployment-version-1', }, }, }), @@ -1996,14 +2029,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() }) @@ -2035,9 +2072,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 991f8f1cb28..7cc2dd6715e 100644 --- a/apps/sim/executor/handlers/workflow/workflow-handler.ts +++ b/apps/sim/executor/handlers/workflow/workflow-handler.ts @@ -376,6 +376,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 @@ -585,6 +606,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. @@ -1192,6 +1214,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, @@ -1280,6 +1303,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 6bd28fbcc86..42b7fa02948 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' @@ -368,6 +368,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..31bf6124845 100644 --- a/apps/sim/hooks/queries/utils/credential-group-queries.ts +++ b/apps/sim/hooks/queries/utils/credential-group-queries.ts @@ -4,6 +4,7 @@ import { listCredentialGroupsContract } from '@/lib/api/contracts/credential-gro export const CREDENTIAL_GROUP_DETAIL_STALE_TIME = Number.POSITIVE_INFINITY export const CREDENTIAL_GROUP_LIST_STALE_TIME = 30 * 1000 +const CREDENTIAL_GROUP_ACCESS_QUERY_VERSION = 2 export const credentialGroupKeys = { all: ['credential-groups'] as const, @@ -12,6 +13,12 @@ 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', + CREDENTIAL_GROUP_ACCESS_QUERY_VERSION, + ] 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..5e5b7c73700 100644 --- a/apps/sim/lib/api/contracts/credential-groups.test.ts +++ b/apps/sim/lib/api/contracts/credential-groups.test.ts @@ -1,12 +1,16 @@ import { describe, expect, it } from 'vitest' import { createCredentialGroupBodySchema, + credentialGroupAccessPolicySchema, + credentialGroupAccessResponseSchema, credentialGroupEnrollmentDetailSchema, credentialGroupEnrollmentListQuerySchema, credentialGroupSchema, inviteCredentialGroupEnrollmentsBodySchema, + updateCredentialGroupAccessBodySchema, updateCredentialGroupBodySchema, } from '@/lib/api/contracts/credential-groups' +import { RESOURCE_POLICY_PRINCIPAL_CATALOG_LIMIT } from '@/lib/resource-policies/limits' describe('credential group contracts', () => { it('accepts a group before account types are added', () => { @@ -196,4 +200,117 @@ describe('credential group contracts', () => { expect(result.connections).toEqual([{ provider: 'gmail', status: 'active', count: 2 }]) }) + + it('accepts the complete resource policy document', () => { + const result = updateCredentialGroupAccessBodySchema.parse({ + expectedRevision: 3, + 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.document.statements).toHaveLength(2) + expect(result.document.statements[1].effect).toBe('deny') + }) + + it('requires bounded principal catalogs only on access reads', () => { + const policy = { + revision: 1, + document: { + version: 1, + resource: { type: 'credential_group', id: 'group-1' }, + statements: [], + }, + } + + expect( + credentialGroupAccessResponseSchema.parse({ + ...policy, + users: [{ userId: 'user-1', name: 'Test User', email: 'user@example.com' }], + workflows: [{ id: 'workflow-1', name: 'Support workflow' }], + permissionGroups: [{ id: 'permission-group-1', name: 'Engineering' }], + }).permissionGroups + ).toEqual([{ id: 'permission-group-1', name: 'Engineering' }]) + expect(credentialGroupAccessResponseSchema.safeParse(policy).success).toBe(false) + expect( + credentialGroupAccessResponseSchema.safeParse({ + ...policy, + users: [], + workflows: [], + permissionGroups: Array.from( + { length: RESOURCE_POLICY_PRINCIPAL_CATALOG_LIMIT + 1 }, + (_, index) => ({ id: `permission-group-${index}`, name: `Group ${index}` }) + ), + }).success + ).toBe(false) + expect( + credentialGroupAccessResponseSchema.safeParse({ + ...policy, + users: Array.from({ length: RESOURCE_POLICY_PRINCIPAL_CATALOG_LIMIT + 1 }, (_, index) => ({ + userId: `user-${index}`, + name: `User ${index}`, + email: `user-${index}@example.com`, + })), + workflows: [], + permissionGroups: [], + }).success + ).toBe(false) + expect( + credentialGroupAccessResponseSchema.safeParse({ + ...policy, + users: [], + workflows: Array.from( + { length: RESOURCE_POLICY_PRINCIPAL_CATALOG_LIMIT + 1 }, + (_, index) => ({ id: `workflow-${index}`, name: `Workflow ${index}` }) + ), + permissionGroups: [], + }).success + ).toBe(false) + expect(credentialGroupAccessPolicySchema.safeParse(policy).success).toBe(true) + expect( + credentialGroupAccessPolicySchema.safeParse({ + ...policy, + users: [], + workflows: [], + permissionGroups: [], + }).success + ).toBe(false) + }) + + it('rejects revision zero and recursive conditions', () => { + expect( + updateCredentialGroupAccessBodySchema.safeParse({ + expectedRevision: 0, + 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 fc9bf7dc522..1a682859144 100644 --- a/apps/sim/lib/api/contracts/credential-groups.ts +++ b/apps/sim/lib/api/contracts/credential-groups.ts @@ -5,6 +5,8 @@ import { CREDENTIAL_GROUP_PROVIDER_IDS, CREDENTIAL_GROUP_STANDARD_OAUTH_PROVIDER_IDS, } from '@/lib/credential-groups/providers' +import { RESOURCE_POLICY_PRINCIPAL_CATALOG_LIMIT } from '@/lib/resource-policies/limits' +import { resourcePolicyDocumentSchema } from '@/lib/resource-policies/types' export const credentialGroupProviderSchema = z.enum(CREDENTIAL_GROUP_PROVIDER_IDS) export const credentialGroupStatusSchema = z.enum(['active', 'disabled']) @@ -115,6 +117,56 @@ export type CredentialGroupEnrollmentConnection = z.output< > export type CredentialGroupEnrollmentDetail = z.output +export const credentialGroupAccessPolicySchema = z + .object({ + revision: z.number().int().positive(), + document: resourcePolicyDocumentSchema, + }) + .strict() + +export type CredentialGroupAccessPolicy = z.output + +export const resourcePolicyPermissionGroupSchema = z + .object({ + id: z.string().min(1).max(128), + name: z.string().min(1).max(100), + }) + .strict() + +export const resourcePolicyUserSchema = z + .object({ + userId: z.string().min(1).max(128), + name: z.string().max(255).nullable(), + email: z.string().email().max(320), + }) + .strict() + +export const resourcePolicyWorkflowSchema = z + .object({ + id: z.string().min(1).max(128), + name: z.string().max(255), + }) + .strict() + +export const credentialGroupAccessResponseSchema = credentialGroupAccessPolicySchema.extend({ + users: z.array(resourcePolicyUserSchema).max(RESOURCE_POLICY_PRINCIPAL_CATALOG_LIMIT), + workflows: z.array(resourcePolicyWorkflowSchema).max(RESOURCE_POLICY_PRINCIPAL_CATALOG_LIMIT), + permissionGroups: z + .array(resourcePolicyPermissionGroupSchema) + .max(RESOURCE_POLICY_PRINCIPAL_CATALOG_LIMIT), +}) + +export type CredentialGroupAccessResponse = z.output + +export const updateCredentialGroupAccessBodySchema = z + .object({ + expectedRevision: z.number().int().positive(), + document: resourcePolicyDocumentSchema, + }) + .strict() + +export type UpdateCredentialGroupAccessBody = z.input + export const credentialGroupWorkspaceParamsSchema = z.object({ id: workspaceIdSchema, }) @@ -382,6 +434,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: credentialGroupAccessResponseSchema }, +}) + +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.test.ts b/apps/sim/lib/credential-groups/application/authorization.test.ts index c07b7c6b032..5855eee23d5 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(() => ({ + evaluatePolicy: vi.fn(), loadEnrollmentAccess: vi.fn(), })) @@ -12,7 +13,19 @@ vi.mock('@/lib/credential-groups/credentials', () => ({ loadCredentialGroupEnrollmentAccessForSubject: mocks.loadEnrollmentAccess, })) -import { requireCredentialGroupEnrollmentAccess } from '@/lib/credential-groups/application/authorization' +vi.mock('@/lib/resource-policies/authorization', () => ({ + evaluateResourcePolicy: mocks.evaluatePolicy, +})) + +import { requireCredentialGroupCredentialAccess } from '@/lib/credential-groups/application/authorization' + +const context = { + workspaceId: 'workspace-1', + workspaceOrganizationId: null, + allowPersonalApiKeys: true, + credentialGroupId: 'group-1', + credentialGroupEnrollmentId: 'enrollment-1', +} function executorPrincipal(): WorkflowExecutionDelegatedPrincipal { return { @@ -44,31 +57,48 @@ function executorPrincipal(): WorkflowExecutionDelegatedPrincipal { } } -describe('requireCredentialGroupEnrollmentAccess', () => { +describe('requireCredentialGroupCredentialAccess', () => { beforeEach(() => { vi.clearAllMocks() + 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(requireCredentialGroupEnrollmentAccess(principal, 'group-1')).resolves.toEqual({ - 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', @@ -76,17 +106,52 @@ describe('requireCredentialGroupEnrollmentAccess', () => { workspaceId: 'workspace-1', workflowId: 'workflow-1', } + mocks.evaluatePolicy.mockResolvedValue({ decision: 'allow', statementSid: 'ScheduledWorkflow' }) await expect( - requireCredentialGroupEnrollmentAccess(principal, 'group-1') + requireCredentialGroupCredentialAccess( + principal, + context, + 'credential_groups.credentials.use' + ) + ).resolves.toBeUndefined() + expect(mocks.loadEnrollmentAccess).not.toHaveBeenCalled() + expect(mocks.evaluatePolicy).toHaveBeenCalledWith( + expect.objectContaining({ + resourceContext: { + 'credential_group:CredentialEnrollmentId': 'enrollment-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 enrollment access required', + message: 'Credential Group credential access denied', }) - expect(mocks.loadEnrollmentAccess).not.toHaveBeenCalled() + + mocks.evaluatePolicy.mockResolvedValueOnce({ + decision: 'deny', + statementSid: 'BlockedActor', + }) + await expect( + requireCredentialGroupCredentialAccess( + executorPrincipal(), + context, + 'credential_groups.credentials.use' + ) + ).rejects.toMatchObject({ code: 'forbidden' }) }) - it('rejects an executor delegation whose Sim subject does not match the workflow actor', 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 = { @@ -96,8 +161,12 @@ describe('requireCredentialGroupEnrollmentAccess', () => { } await expect( - requireCredentialGroupEnrollmentAccess(principal, 'group-1') + requireCredentialGroupCredentialAccess( + principal, + context, + 'credential_groups.credentials.use' + ) ).rejects.toMatchObject({ code: 'forbidden' }) - expect(mocks.loadEnrollmentAccess).not.toHaveBeenCalled() + 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 553f522aeed..b31024d330d 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' @@ -10,19 +9,20 @@ 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 { 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' -export interface CredentialGroupApplicationContext - extends WorkspaceAuthorizationContext, - CredentialGroupCredentialListContext { - enrollmentAccess?: CredentialGroupEnrollmentAccess +export interface CredentialGroupAuthorizationContext extends WorkspaceAuthorizationContext { + credentialGroupId: string } +export interface CredentialGroupApplicationContext + extends CredentialGroupAuthorizationContext, + CredentialGroupCredentialListContext {} + function requireWorkflowExecutionPrincipal(principal: Principal) { if (principal.kind !== 'delegated' || principal.serviceId !== 'executor') { throw new Error('Credential Group use requires an executor delegation') @@ -36,39 +36,49 @@ 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 & { credentialGroupEnrollmentId: string }, + action: ResourcePolicyAction +): Promise { const executionPrincipal = requireWorkflowExecutionPrincipal(principal) const subject = resolvePrincipalSubject(executionPrincipal) - if (!subject) { - throw new OrchestrationError('forbidden', 'Credential Group enrollment access required') - } if ( - subject.kind === 'sim_user' && + 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) - 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 access } export const credentialGroupDelegationPolicy = { 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..91776d17719 --- /dev/null +++ b/apps/sim/lib/credential-groups/application/manage-access.test.ts @@ -0,0 +1,211 @@ +/** + * @vitest-environment node + */ +import type { SessionPrincipal } from '@sim/auth/principal' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const mocks = vi.hoisted(() => ({ + requirePolicy: vi.fn(), + loadPermissionGroups: vi.fn(), + loadUsers: vi.fn(), + loadWorkflows: vi.fn(), + resolveGroup: vi.fn(), + resolvePermission: vi.fn(), + validatePrincipals: 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', () => ({ + loadApplicableResourcePolicyPermissionGroups: mocks.loadPermissionGroups, + loadResourcePolicyUsers: mocks.loadUsers, + loadResourcePolicyWorkflows: mocks.loadWorkflows, + validateResourcePolicyPrincipals: mocks.validatePrincipals, +})) + +vi.mock('@/lib/resource-policies/repository', () => { + class ResourcePolicyRevisionConflictError extends Error {} + class ResourcePolicyNotFoundError extends Error {} + return { + requireResourcePolicy: mocks.requirePolicy, + ResourcePolicyNotFoundError, + ResourcePolicyRevisionConflictError, + writeResourcePolicy: mocks.writePolicy, + } +}) + +import { + readCredentialGroupAccess, + updateCredentialGroupAccess, +} from '@/lib/credential-groups/application/manage-access' +import { + ResourcePolicyNotFoundError, + 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', +} +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' }], + }, + ], +} + +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.requirePolicy.mockResolvedValue(storedPolicy()) + mocks.loadPermissionGroups.mockResolvedValue([ + { id: 'permission-group-1', name: 'Engineering' }, + ]) + mocks.loadUsers.mockResolvedValue([ + { userId: 'user-1', name: 'Test User', email: 'user@example.com' }, + ]) + mocks.loadWorkflows.mockResolvedValue([{ id: 'workflow-1', name: 'Support workflow' }]) + mocks.validatePrincipals.mockResolvedValue(undefined) + mocks.writePolicy.mockResolvedValue(storedPolicy(2)) + }) + + it('returns the exact managed policy document without the hidden system rule', async () => { + await expect(readCredentialGroupAccess.execute({ principal, input: target })).resolves.toEqual({ + revision: 1, + document, + users: [{ userId: 'user-1', name: 'Test User', email: 'user@example.com' }], + workflows: [{ id: 'workflow-1', name: 'Support workflow' }], + permissionGroups: [{ id: 'permission-group-1', name: 'Engineering' }], + }) + expect(mocks.requirePolicy).toHaveBeenCalledWith({ + workspaceId: 'workspace-1', + resourceType: 'credential_group', + resourceId: 'group-1', + }) + expect(mocks.loadUsers).toHaveBeenCalledWith(context) + expect(mocks.loadWorkflows).toHaveBeenCalledWith(context) + expect(mocks.loadPermissionGroups).toHaveBeenCalledWith(context) + }) + + 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() + expect(mocks.loadUsers).not.toHaveBeenCalled() + expect(mocks.loadWorkflows).not.toHaveBeenCalled() + expect(mocks.loadPermissionGroups).not.toHaveBeenCalled() + }) + + it('requires current workspace-admin permission', async () => { + mocks.resolvePermission.mockResolvedValue('write') + + await expect( + updateCredentialGroupAccess.execute({ + principal, + input: { ...target, expectedRevision: 1, document }, + }) + ).rejects.toMatchObject({ code: 'forbidden' }) + expect(mocks.writePolicy).not.toHaveBeenCalled() + }) + + it('validates every principal and persists the exact allow/deny document', async () => { + const result = await updateCredentialGroupAccess.execute({ + principal, + input: { ...target, expectedRevision: 1, document }, + }) + + 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, + }) + 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 () => { + mocks.writePolicy.mockRejectedValue(new ResourcePolicyRevisionConflictError()) + + await expect( + updateCredentialGroupAccess.execute({ + principal, + 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 new file mode 100644 index 00000000000..e01dad58c16 --- /dev/null +++ b/apps/sim/lib/credential-groups/application/manage-access.ts @@ -0,0 +1,114 @@ +import { AuditAction, AuditResourceType } from '@sim/audit' +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 { + loadApplicableResourcePolicyPermissionGroups, + loadResourcePolicyUsers, + loadResourcePolicyWorkflows, + validateResourcePolicyPrincipals, +} from '@/lib/resource-policies/management' +import { + ResourcePolicyRevisionConflictError, + requireResourcePolicy, + writeResourcePolicy, +} from '@/lib/resource-policies/repository' +import { + parseResourcePolicyDocument, + type ResourcePolicyDocument, +} from '@/lib/resource-policies/types' + +interface CredentialGroupAccessTargetInput { + assertedWorkspaceId: string + credentialGroupId: string +} + +function presentPolicy(policy: Awaited>): { + revision: number + document: ResourcePolicyDocument +} { + return { revision: policy.revision, document: policy.document } +} + +export const readCredentialGroupAccess = defineAuthorizedWorkspaceUseCase({ + operation: credentialGroupOperations.readAccess, + resolveContext: ({ input }: { input: CredentialGroupAccessTargetInput }) => + resolveCredentialGroupSettingsContext(input.credentialGroupId, input.assertedWorkspaceId), + authorizationOptions: {}, + async execute({ context }) { + const policy = await requireResourcePolicy({ + workspaceId: context.workspaceId, + resourceType: 'credential_group', + resourceId: context.credentialGroupId, + }) + const [users, workflows, permissionGroups] = await Promise.all([ + loadResourcePolicyUsers(context), + loadResourcePolicyWorkflows(context), + loadApplicableResourcePolicyPermissionGroups(context), + ]) + return { ...presentPolicy(policy), users, workflows, permissionGroups } + }, +}) + +export interface UpdateCredentialGroupAccessInput extends CredentialGroupAccessTargetInput { + expectedRevision: number + document: ResourcePolicyDocument +} + +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 < 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 document = parseResourcePolicyDocument(input.document, { + type: 'credential_group', + id: context.credentialGroupId, + }) + await validateResourcePolicyPrincipals( + document.statements.flatMap((statement) => statement.principals), + context + ) + try { + 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) + } + throw error + } + }, + projectAudit: ({ result, context }) => ({ + action: AuditAction.CREDENTIAL_GROUP_UPDATED, + resourceType: AuditResourceType.CREDENTIAL_GROUP, + resourceId: context.credentialGroupId, + resourceName: context.name, + description: 'Updated Credential Group resource policy', + metadata: { + revision: result.revision, + statementCount: result.document.statements.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/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 7828cb946e1..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 @@ -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,10 +75,7 @@ describe('resolveManagedOAuthCredentialToken', () => { vi.clearAllMocks() mocks.loadContext.mockResolvedValue(context) mocks.resolvePermission.mockResolvedValue('read') - mocks.requireEnrollmentAccess.mockResolvedValue({ - enrollmentId: 'enrollment-1', - email: 'person@example.com', - }) + mocks.requireCredentialAccess.mockResolvedValue(undefined) mocks.resolveToken.mockResolvedValue({ accessToken: 'access-token', refreshed: false }) }) @@ -106,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', @@ -124,18 +127,27 @@ describe('resolveManagedOAuthCredentialToken', () => { expect(mocks.recordAudit).toHaveBeenCalledOnce() }) - it('rejects a credential owned by another group enrollment', async () => { - mocks.requireEnrollmentAccess.mockResolvedValueOnce({ - 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 token resolution after any policy allow, including workflow-wide access', async () => { + mocks.requireCredentialAccess.mockResolvedValueOnce(undefined) + + 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..1e18083511a 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,13 +26,11 @@ export const resolveManagedOAuthCredentialToken = defineAuthorizedWorkspaceUseCa }, authorizationOptions: { delegation: managedOAuthCredentialDelegationPolicy }, async authorizeResource({ principal, context }) { - const access = await requireCredentialGroupEnrollmentAccess( + await requireCredentialGroupCredentialAccess( principal, - context.credentialGroupId + context, + 'credential_groups.credentials.use' ) - if (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 new file mode 100644 index 00000000000..204808a92fb --- /dev/null +++ b/apps/sim/lib/resource-policies/authorization.test.ts @@ -0,0 +1,277 @@ +/** + * @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(), + requirePolicy: vi.fn(), + resolveGroup: vi.fn(), + resolvePermission: vi.fn(), +})) + +vi.mock('@/lib/resource-policies/repository', () => ({ + requireResourcePolicy: mocks.requirePolicy, +})) + +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 { evaluateResourcePolicy } from '@/lib/resource-policies/authorization' +import type { + ResourcePolicyCondition, + ResourcePolicyPrincipal, +} from '@/lib/resource-policies/types' + +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(input: { + policyPrincipal: ResourcePolicyPrincipal + effect?: 'allow' | 'deny' + condition?: ResourcePolicyCondition +}) { + return { + id: 'policy-1', + workspaceId: 'workspace-1', + revision: 1, + createdAt: new Date(), + updatedAt: new Date(), + document: { + version: 1 as const, + resource: { type: 'credential_group' as const, id: 'group-1' }, + statements: [ + { + sid: 'ManagedStatement', + effect: input.effect ?? ('allow' as const), + principals: [input.policyPrincipal], + actions: ['credential_groups.credentials.use' as const], + ...(input.condition ? { condition: input.condition } : {}), + }, + ], + }, + } +} + +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 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( + evaluateResourcePolicy({ + ...baseInput, + principal: principal(), + resourceContext: { + 'credential_group:CredentialEnrollmentId': 'enrollment-1', + 'sim:PrincipalCredentialGroupEnrollmentId': 'enrollment-1', + }, + }) + ).resolves.toEqual({ + decision: 'allow', + statementSid: 'sim:CredentialGroupActorCredential', + }) + await expect( + 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.requirePolicy.mockResolvedValue( + policy({ + policyPrincipal: { + type: 'external_identity', + provider: 'slack', + tenantId: 'T123', + subjectId: 'U123', + }, + }) + ) + const external = principal() + external.subjectUserId = undefined + 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(evaluateResourcePolicy({ ...baseInput, principal: external })).resolves.toEqual({ + decision: 'allow', + statementSid: 'ManagedStatement', + }) + }) + + 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(evaluateResourcePolicy({ ...baseInput, principal: principal() })).rejects.toThrow( + 'Required resource policy is missing' + ) + }) + + it('matches live workspace roles and effective Access Control Groups', async () => { + mocks.requirePolicy.mockResolvedValue( + policy({ policyPrincipal: { type: 'workspace_role', minimumRole: 'write' } }) + ) + mocks.resolvePermission.mockResolvedValue('admin') + + await expect( + evaluateResourcePolicy({ ...baseInput, principal: principal() }) + ).resolves.toMatchObject({ decision: 'allow' }) + + mocks.requirePolicy.mockResolvedValue( + policy({ + policyPrincipal: { + type: 'access_control_group', + accessControlGroupId: 'access-group-1', + }, + }) + ) + mocks.resolveGroup.mockResolvedValue({ permissionGroupId: 'access-group-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.requirePolicy.mockResolvedValue( + policy({ policyPrincipal: { type: 'user', userId: 'user-1' } }) + ) + + await expect( + evaluateResourcePolicy({ + ...baseInput, + principal: { kind: 'session', userId: 'user-1', sessionId: 'session-1' }, + }) + ).resolves.toMatchObject({ decision: 'allow' }) + }) + + it('fails fast when executor delegation has lost its original principal', async () => { + mocks.requirePolicy.mockResolvedValue( + policy({ policyPrincipal: { type: 'user', userId: 'user-1' } }) + ) + const unbound = principal() + unbound.delegationContext!.principal = undefined + + await expect(evaluateResourcePolicy({ ...baseInput, principal: unbound })).rejects.toThrow( + 'bound workflow execution principal' + ) + }) + + it('fails fast when the resource adapter omits required action context', async () => { + await expect( + 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 new file mode 100644 index 00000000000..82fb82bef66 --- /dev/null +++ b/apps/sim/lib/resource-policies/authorization.ts @@ -0,0 +1,182 @@ +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 { 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, + ResourcePolicyPrincipal, + ResourcePolicyResourceType, +} from '@/lib/resource-policies/types' +import { resolveWorkspaceGroup } from '@/ee/access-control/utils/permission-check' + +export interface ResourcePolicyAuthorizationInput { + principal: Principal + context: WorkspaceAuthorizationContext + resourceType: ResourcePolicyResourceType + resourceId: string + action: ResourcePolicyAction + resourceContext: Readonly> +} + +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, + } +} + +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 + } + 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}`) + } + 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 evaluateResourcePolicy( + input: ResourcePolicyAuthorizationInput +): 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, + }) + const execution = resolveAuthorizationPrincipal(input.principal) + const principalSubject = resolvePrincipalSubject(execution.invoker) + let workspacePermission: ReturnType | undefined + let accessControlGroupId: Promise | undefined + + 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.workspaceId, + input.context.workspaceOrganizationId + ) + 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 + } + } + } + + 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 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/limits.ts b/apps/sim/lib/resource-policies/limits.ts new file mode 100644 index 00000000000..4b3f5866486 --- /dev/null +++ b/apps/sim/lib/resource-policies/limits.ts @@ -0,0 +1 @@ +export const RESOURCE_POLICY_PRINCIPAL_CATALOG_LIMIT = 500 diff --git a/apps/sim/lib/resource-policies/management.test.ts b/apps/sim/lib/resource-policies/management.test.ts new file mode 100644 index 00000000000..520498cd626 --- /dev/null +++ b/apps/sim/lib/resource-policies/management.test.ts @@ -0,0 +1,277 @@ +/** + * @vitest-environment node + */ +import { + dbChainMockFns, + flattenMockConditions, + hasMockCondition, + queueTableRows, + resetDbChainMock, + schemaMock, +} from '@sim/testing' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { RESOURCE_POLICY_PRINCIPAL_CATALOG_LIMIT } from '@/lib/resource-policies/limits' +import { + loadApplicableResourcePolicyPermissionGroups, + loadResourcePolicyUsers, + loadResourcePolicyWorkflows, +} from '@/lib/resource-policies/management' + +const context = { + workspaceId: 'workspace-1', + workspaceOrganizationId: 'organization-1', + allowPersonalApiKeys: true, +} + +describe('resource policy management catalogs', () => { + beforeEach(() => { + vi.clearAllMocks() + resetDbChainMock() + }) + + it('loads only organization groups that are default or bound to the canonical workspace', async () => { + const rows = [ + { id: 'permission-group-1', name: 'Default' }, + { id: 'permission-group-2', name: 'Engineering' }, + ] + queueTableRows(schemaMock.permissionGroup, rows) + + await expect(loadApplicableResourcePolicyPermissionGroups(context)).resolves.toEqual(rows) + + const joinCondition = dbChainMockFns.leftJoin.mock.calls.at(-1)?.[1] + expect( + hasMockCondition( + joinCondition, + (condition) => + condition.type === 'eq' && + condition.left === schemaMock.permissionGroupWorkspace.workspaceId && + condition.right === context.workspaceId + ) + ).toBe(true) + expect( + hasMockCondition( + joinCondition, + (condition) => + condition.type === 'eq' && + condition.left === schemaMock.permissionGroupWorkspace.organizationId && + condition.right === context.workspaceOrganizationId + ) + ).toBe(true) + + const whereCondition = dbChainMockFns.where.mock.calls.at(-1)?.[0] + expect( + hasMockCondition( + whereCondition, + (condition) => + condition.type === 'eq' && + condition.left === schemaMock.permissionGroup.organizationId && + condition.right === context.workspaceOrganizationId + ) + ).toBe(true) + const applicability = flattenMockConditions(whereCondition).find( + (condition) => condition.type === 'or' + ) + expect(applicability?.conditions).toEqual([ + { + type: 'eq', + left: schemaMock.permissionGroup.isDefault, + right: true, + }, + { + type: 'isNotNull', + column: schemaMock.permissionGroupWorkspace.id, + }, + ]) + expect(dbChainMockFns.limit).toHaveBeenCalledWith(RESOURCE_POLICY_PRINCIPAL_CATALOG_LIMIT + 1) + expect(dbChainMockFns.orderBy).toHaveBeenCalledWith( + { type: 'asc', column: schemaMock.permissionGroup.name }, + { type: 'asc', column: schemaMock.permissionGroup.id } + ) + }) + + it('returns an empty catalog without querying when the workspace has no organization', async () => { + await expect( + loadApplicableResourcePolicyPermissionGroups({ + ...context, + workspaceOrganizationId: null, + }) + ).resolves.toEqual([]) + expect(dbChainMockFns.select).not.toHaveBeenCalled() + }) + + it('fails instead of truncating a catalog that exceeds the response bound', async () => { + queueTableRows( + schemaMock.permissionGroup, + Array.from({ length: RESOURCE_POLICY_PRINCIPAL_CATALOG_LIMIT + 1 }, (_, index) => ({ + id: `permission-group-${index}`, + name: `Group ${index}`, + })) + ) + + await expect(loadApplicableResourcePolicyPermissionGroups(context)).rejects.toThrow( + `exceeds the ${RESOURCE_POLICY_PRINCIPAL_CATALOG_LIMIT} row limit` + ) + }) + + it('loads active workflows only from the canonical workspace with a hard bound', async () => { + const rows = [ + { id: 'workflow-1', name: 'Alpha' }, + { id: 'workflow-2', name: 'Beta' }, + ] + queueTableRows(schemaMock.workflow, rows) + + await expect(loadResourcePolicyWorkflows(context)).resolves.toEqual(rows) + + const whereCondition = dbChainMockFns.where.mock.calls.at(-1)?.[0] + expect( + hasMockCondition( + whereCondition, + (condition) => + condition.type === 'eq' && + condition.left === schemaMock.workflow.workspaceId && + condition.right === context.workspaceId + ) + ).toBe(true) + expect( + hasMockCondition( + whereCondition, + (condition) => + condition.type === 'isNull' && condition.column === schemaMock.workflow.archivedAt + ) + ).toBe(true) + expect(dbChainMockFns.orderBy).toHaveBeenCalledWith( + { type: 'asc', column: schemaMock.workflow.name }, + { type: 'asc', column: schemaMock.workflow.id } + ) + expect(dbChainMockFns.limit).toHaveBeenCalledWith(RESOURCE_POLICY_PRINCIPAL_CATALOG_LIMIT + 1) + }) + + it('fails instead of truncating the workflow catalog', async () => { + queueTableRows( + schemaMock.workflow, + Array.from({ length: RESOURCE_POLICY_PRINCIPAL_CATALOG_LIMIT + 1 }, (_, index) => ({ + id: `workflow-${index}`, + name: `Workflow ${index}`, + })) + ) + + await expect(loadResourcePolicyWorkflows(context)).rejects.toThrow( + `exceeds the ${RESOURCE_POLICY_PRINCIPAL_CATALOG_LIMIT} row limit` + ) + }) + + it('combines explicit workspace users with derived organization admins', async () => { + queueTableRows(schemaMock.permissions, [ + { userId: 'user-2', name: 'Beta User', email: 'beta@example.com' }, + { userId: 'user-1', name: 'Alpha User', email: 'alpha@example.com' }, + ]) + queueTableRows(schemaMock.member, [ + { userId: 'user-3', name: 'Admin User', email: 'admin@example.com' }, + { userId: 'user-1', name: 'Alpha User', email: 'alpha@example.com' }, + ]) + + await expect(loadResourcePolicyUsers(context)).resolves.toEqual([ + { userId: 'user-3', name: 'Admin User', email: 'admin@example.com' }, + { userId: 'user-1', name: 'Alpha User', email: 'alpha@example.com' }, + { userId: 'user-2', name: 'Beta User', email: 'beta@example.com' }, + ]) + + const whereConditions = dbChainMockFns.where.mock.calls.map(([condition]) => condition) + expect( + whereConditions.some( + (condition) => + hasMockCondition( + condition, + (entry) => + entry.type === 'eq' && + entry.left === schemaMock.permissions.entityType && + entry.right === 'workspace' + ) && + hasMockCondition( + condition, + (entry) => + entry.type === 'eq' && + entry.left === schemaMock.permissions.entityId && + entry.right === context.workspaceId + ) + ) + ).toBe(true) + expect( + whereConditions.some( + (condition) => + hasMockCondition( + condition, + (entry) => + entry.type === 'eq' && + entry.left === schemaMock.member.organizationId && + entry.right === context.workspaceOrganizationId + ) && + hasMockCondition( + condition, + (entry) => + entry.type === 'inArray' && + entry.column === schemaMock.member.role && + entry.values.includes('owner') && + entry.values.includes('admin') + ) + ) + ).toBe(true) + expect(dbChainMockFns.limit).toHaveBeenCalledTimes(2) + expect(dbChainMockFns.limit).toHaveBeenNthCalledWith( + 1, + RESOURCE_POLICY_PRINCIPAL_CATALOG_LIMIT + 1 + ) + expect(dbChainMockFns.limit).toHaveBeenNthCalledWith( + 2, + RESOURCE_POLICY_PRINCIPAL_CATALOG_LIMIT + 1 + ) + }) + + it('does not query organization membership for a workspace without an organization', async () => { + const personalContext = { ...context, workspaceOrganizationId: null } + const rows = [{ userId: 'user-1', name: 'Alpha User', email: 'alpha@example.com' }] + queueTableRows(schemaMock.permissions, rows) + + await expect(loadResourcePolicyUsers(personalContext)).resolves.toEqual(rows) + expect(dbChainMockFns.from).not.toHaveBeenCalledWith(schemaMock.member) + }) + + it('fails when either source or the combined user catalog exceeds the bound', async () => { + const oversizedExplicitUsers = Array.from( + { length: RESOURCE_POLICY_PRINCIPAL_CATALOG_LIMIT + 1 }, + (_, index) => ({ + userId: `explicit-user-${index}`, + name: `Explicit User ${index}`, + email: `explicit-${index}@example.com`, + }) + ) + queueTableRows(schemaMock.permissions, oversizedExplicitUsers) + queueTableRows(schemaMock.member, []) + + await expect(loadResourcePolicyUsers(context)).rejects.toThrow( + `exceeds the ${RESOURCE_POLICY_PRINCIPAL_CATALOG_LIMIT} row limit` + ) + + resetDbChainMock() + queueTableRows( + schemaMock.permissions, + Array.from({ length: 300 }, (_, index) => ({ + userId: `explicit-user-${index}`, + name: `Explicit User ${index}`, + email: `explicit-${index}@example.com`, + })) + ) + queueTableRows( + schemaMock.member, + Array.from({ length: 300 }, (_, index) => ({ + userId: `organization-admin-${index}`, + name: `Organization Admin ${index}`, + email: `organization-admin-${index}@example.com`, + })) + ) + + await expect(loadResourcePolicyUsers(context)).rejects.toThrow( + `exceeds the ${RESOURCE_POLICY_PRINCIPAL_CATALOG_LIMIT} row limit` + ) + }) +}) diff --git a/apps/sim/lib/resource-policies/management.ts b/apps/sim/lib/resource-policies/management.ts new file mode 100644 index 00000000000..a2aed54d7cd --- /dev/null +++ b/apps/sim/lib/resource-policies/management.ts @@ -0,0 +1,235 @@ +import { db } from '@sim/db' +import { + member, + permissionGroup, + permissionGroupWorkspace, + permissions, + user, + workflow, +} from '@sim/db/schema' +import { ORG_ADMIN_ROLES, resolveEffectiveWorkspacePermission } from '@sim/platform-authz/workspace' +import { and, asc, eq, inArray, isNotNull, isNull, or } from 'drizzle-orm' +import type { WorkspaceAuthorizationContext } from '@/lib/core/application' +import { OrchestrationError } from '@/lib/core/orchestration/types' +import { mapWithConcurrency } from '@/lib/core/utils/concurrency' +import { RESOURCE_POLICY_PRINCIPAL_CATALOG_LIMIT } from '@/lib/resource-policies/limits' +import type { ResourcePolicyPrincipal } from '@/lib/resource-policies/types' + +const POLICY_REFERENCE_VALIDATION_CONCURRENCY = 10 + +export interface ResourcePolicyPermissionGroup { + id: string + name: string +} + +export interface ResourcePolicyUser { + userId: string + name: string + email: string +} + +export interface ResourcePolicyWorkflow { + id: string + name: string +} + +function requireBoundedPrincipalCatalog(catalog: string, rows: T[]): T[] { + if (rows.length > RESOURCE_POLICY_PRINCIPAL_CATALOG_LIMIT) { + throw new Error( + `Resource policy ${catalog} catalog exceeds the ${RESOURCE_POLICY_PRINCIPAL_CATALOG_LIMIT} row limit` + ) + } + return rows +} + +export async function loadResourcePolicyUsers( + context: WorkspaceAuthorizationContext +): Promise { + const explicitUsersQuery = db + .select({ userId: user.id, name: user.name, email: user.email }) + .from(permissions) + .innerJoin(user, eq(permissions.userId, user.id)) + .where( + and(eq(permissions.entityType, 'workspace'), eq(permissions.entityId, context.workspaceId)) + ) + .orderBy(asc(user.email), asc(user.id)) + .limit(RESOURCE_POLICY_PRINCIPAL_CATALOG_LIMIT + 1) + + const organizationAdminsQuery = context.workspaceOrganizationId + ? db + .select({ userId: user.id, name: user.name, email: user.email }) + .from(member) + .innerJoin(user, eq(member.userId, user.id)) + .where( + and( + eq(member.organizationId, context.workspaceOrganizationId), + inArray(member.role, [...ORG_ADMIN_ROLES]) + ) + ) + .orderBy(asc(user.email), asc(user.id)) + .limit(RESOURCE_POLICY_PRINCIPAL_CATALOG_LIMIT + 1) + : Promise.resolve([]) + + const [explicitUsers, organizationAdmins] = await Promise.all([ + explicitUsersQuery, + organizationAdminsQuery, + ]) + requireBoundedPrincipalCatalog('user', explicitUsers) + requireBoundedPrincipalCatalog('user', organizationAdmins) + + const usersById = new Map() + for (const row of [...explicitUsers, ...organizationAdmins]) { + usersById.set(row.userId, row) + } + const users = [...usersById.values()].sort( + (left, right) => + left.email.localeCompare(right.email) || left.userId.localeCompare(right.userId) + ) + return requireBoundedPrincipalCatalog('user', users) +} + +export async function loadResourcePolicyWorkflows( + context: WorkspaceAuthorizationContext +): Promise { + const rows = await db + .select({ id: workflow.id, name: workflow.name }) + .from(workflow) + .where(and(eq(workflow.workspaceId, context.workspaceId), isNull(workflow.archivedAt))) + .orderBy(asc(workflow.name), asc(workflow.id)) + .limit(RESOURCE_POLICY_PRINCIPAL_CATALOG_LIMIT + 1) + + return requireBoundedPrincipalCatalog('workflow', rows) +} + +export async function loadApplicableResourcePolicyPermissionGroups( + context: WorkspaceAuthorizationContext +): Promise { + if (!context.workspaceOrganizationId) return [] + + const rows = await db + .select({ id: permissionGroup.id, name: permissionGroup.name }) + .from(permissionGroup) + .leftJoin( + permissionGroupWorkspace, + and( + eq(permissionGroupWorkspace.permissionGroupId, permissionGroup.id), + eq(permissionGroupWorkspace.workspaceId, context.workspaceId), + eq(permissionGroupWorkspace.organizationId, context.workspaceOrganizationId) + ) + ) + .where( + and( + eq(permissionGroup.organizationId, context.workspaceOrganizationId), + or(eq(permissionGroup.isDefault, true), isNotNull(permissionGroupWorkspace.id)) + ) + ) + .orderBy(asc(permissionGroup.name), asc(permissionGroup.id)) + .limit(RESOURCE_POLICY_PRINCIPAL_CATALOG_LIMIT + 1) + + return requireBoundedPrincipalCatalog('permission group', rows) +} + +async function requireUserPrincipal( + principal: Extract, + context: WorkspaceAuthorizationContext +) { + const permission = await resolveEffectiveWorkspacePermission( + principal.userId, + context.workspaceId, + context.workspaceOrganizationId + ) + if (!permission) { + throw new OrchestrationError('validation', 'Policy user does not have workspace access') + } +} + +async function requireWorkflowPrincipal( + principal: Extract, + context: WorkspaceAuthorizationContext +) { + const [row] = await db + .select({ id: workflow.id }) + .from(workflow) + .where( + and( + eq(workflow.id, principal.workflowId), + eq(workflow.workspaceId, context.workspaceId), + isNull(workflow.archivedAt) + ) + ) + .limit(1) + if (!row) throw new OrchestrationError('validation', 'Policy workflow was not found') +} + +async function requireAccessControlGroupPrincipal( + principal: Extract, + context: WorkspaceAuthorizationContext +) { + if (!context.workspaceOrganizationId) { + throw new OrchestrationError( + 'validation', + 'Access Control Group principals require an organization workspace' + ) + } + const [row] = await db + .select({ id: permissionGroup.id, isDefault: permissionGroup.isDefault }) + .from(permissionGroup) + .where( + and( + eq(permissionGroup.id, principal.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, principal.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 validateResourcePolicyPrincipals( + principals: ResourcePolicyPrincipal[], + context: WorkspaceAuthorizationContext +): Promise { + 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 requireUserPrincipal(principal, context) + case 'workflow': + return requireWorkflowPrincipal(principal, context) + case 'access_control_group': + return requireAccessControlGroupPrincipal(principal, context) + case 'any': + case 'workspace_role': + case 'external_identity': + return + } + } + ) +} diff --git a/apps/sim/lib/resource-policies/presentation.test.ts b/apps/sim/lib/resource-policies/presentation.test.ts new file mode 100644 index 00000000000..fd2575ce4f8 --- /dev/null +++ b/apps/sim/lib/resource-policies/presentation.test.ts @@ -0,0 +1,102 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it } from 'vitest' +import { + getResourcePolicyActionPresentation, + getResourcePolicyActionPresentations, + getResourcePolicyConditionOperatorPresentation, + getResourcePolicyConditionPresentation, + getResourcePolicyConditionPresentations, + getResourcePolicyPrincipalPresentation, + getResourcePolicyResourcePresentation, + RESOURCE_POLICY_ACTION_PRESENTATION, + RESOURCE_POLICY_CONDITION_OPERATOR_PRESENTATION, + RESOURCE_POLICY_CONDITION_PRESENTATION, + RESOURCE_POLICY_PRINCIPAL_PRESENTATION, + RESOURCE_POLICY_RESOURCE_PRESENTATION, +} from '@/lib/resource-policies/presentation' +import { + GLOBAL_RESOURCE_POLICY_CONDITION_KEYS, + RESOURCE_POLICY_ACTIONS, + RESOURCE_POLICY_DEFINITIONS, + RESOURCE_POLICY_RESOURCE_TYPES, +} from '@/lib/resource-policies/registry' +import { RESOURCE_POLICY_CONDITION_OPERATORS } from '@/lib/resource-policies/types' + +describe('resource policy presentation registry', () => { + it('covers every registered resource, action, principal, and condition key', () => { + expect(Object.keys(RESOURCE_POLICY_RESOURCE_PRESENTATION)).toEqual( + RESOURCE_POLICY_RESOURCE_TYPES + ) + expect(Object.keys(RESOURCE_POLICY_ACTION_PRESENTATION)).toEqual(RESOURCE_POLICY_ACTIONS) + expect(Object.keys(RESOURCE_POLICY_CONDITION_OPERATOR_PRESENTATION)).toEqual( + RESOURCE_POLICY_CONDITION_OPERATORS + ) + expect(Object.keys(RESOURCE_POLICY_PRINCIPAL_PRESENTATION)).toEqual([ + 'any', + 'user', + 'workspace_role', + 'access_control_group', + 'workflow', + 'external_identity', + ]) + expect(Object.keys(RESOURCE_POLICY_CONDITION_PRESENTATION)).toEqual([ + ...Object.keys(GLOBAL_RESOURCE_POLICY_CONDITION_KEYS), + ...Object.keys(RESOURCE_POLICY_DEFINITIONS.credential_group.conditionKeys), + ]) + }) + + it('exposes labels, valid operators, and enumerated condition values', () => { + expect(getResourcePolicyResourcePresentation('credential_group').label).toBe('Credential group') + expect( + getResourcePolicyActionPresentation('credential_group', 'credential_groups.credentials.use') + .label + ).toBe('Use credentials') + expect(getResourcePolicyPrincipalPresentation('workspace_role').valueOptions).toEqual([ + expect.objectContaining({ value: 'read', label: 'Readers and above' }), + expect.objectContaining({ value: 'write', label: 'Writers and admins' }), + expect.objectContaining({ value: 'admin', label: 'Admins only' }), + ]) + + const workflowMode = getResourcePolicyConditionPresentation( + 'credential_group', + 'sim:WorkflowMode' + ) + expect(workflowMode.valueType).toBe('string') + expect(workflowMode.operators).toEqual([ + 'StringEquals', + 'StringNotEquals', + 'StringLike', + 'StringNotLike', + 'Null', + ]) + expect(workflowMode.valueOptions).toEqual([ + expect.objectContaining({ value: 'draft', label: 'Draft' }), + expect.objectContaining({ value: 'deployment', label: 'Deployed' }), + ]) + expect(getResourcePolicyConditionOperatorPresentation('StringEquals').label).toBe('Equals') + }) + + it('lists only presentations that apply to the requested resource type', () => { + expect(getResourcePolicyActionPresentations('credential_group')).toHaveLength(1) + expect(getResourcePolicyConditionPresentations('credential_group')).toHaveLength(12) + }) + + it('fails fast for every unregistered lookup', () => { + expect(() => getResourcePolicyResourcePresentation('knowledge_base')).toThrow( + 'resource type knowledge_base is not registered' + ) + expect(() => + getResourcePolicyActionPresentation('credential_group', 'knowledge_bases.read') + ).toThrow('action knowledge_bases.read is not registered for resource type credential_group') + expect(() => getResourcePolicyPrincipalPresentation('organization_role')).toThrow( + 'principal type organization_role is not registered' + ) + expect(() => + getResourcePolicyConditionPresentation('credential_group', 'knowledge_base:Tag') + ).toThrow( + 'condition key knowledge_base:Tag is not registered for resource type credential_group' + ) + }) +}) diff --git a/apps/sim/lib/resource-policies/presentation.ts b/apps/sim/lib/resource-policies/presentation.ts new file mode 100644 index 00000000000..e472f46c294 --- /dev/null +++ b/apps/sim/lib/resource-policies/presentation.ts @@ -0,0 +1,368 @@ +import type { Principal, WorkflowExecutionAuthority } from '@sim/auth/principal' +import { + GLOBAL_RESOURCE_POLICY_CONDITION_KEYS, + getResourcePolicyConditionKeyDefinition, + getResourcePolicyDefinition, + RESOURCE_POLICY_ACTIONS, + RESOURCE_POLICY_DEFINITIONS, + type ResourcePolicyAction, + type ResourcePolicyConditionKey, + type ResourcePolicyContextValueType, + type ResourcePolicyResourceType, +} from '@/lib/resource-policies/registry' +import type { + ResourcePolicyConditionOperator, + ResourcePolicyPrincipal, +} from '@/lib/resource-policies/types' + +export type ResourcePolicyPrincipalType = ResourcePolicyPrincipal['type'] + +export interface ResourcePolicyPresentationOption { + value: Value + label: string + description?: string +} + +export interface ResourcePolicyResourcePresentation { + label: string + description: string +} + +export interface ResourcePolicyActionPresentation { + label: string + description: string +} + +export interface ResourcePolicyPrincipalPresentation { + label: string + description: string + valueOptions?: readonly ResourcePolicyPresentationOption[] +} + +export interface ResourcePolicyConditionKeyPresentation { + label: string + description: string + valueType: ResourcePolicyContextValueType + operators: readonly ResourcePolicyConditionOperator[] + valueOptions?: readonly ResourcePolicyPresentationOption[] +} + +export interface ResourcePolicyConditionOperatorPresentation { + label: string +} + +type PresentationOptionsByValue = { + readonly [Key in Value]: ResourcePolicyPresentationOption +} + +type WorkspaceRole = Extract['minimumRole'] +type PrincipalKind = Principal['kind'] +type PrincipalServiceId = Extract['serviceId'] +type WorkflowMode = WorkflowExecutionAuthority['mode'] + +const WORKSPACE_ROLE_OPTIONS_BY_VALUE = { + read: { + value: 'read', + label: 'Readers and above', + description: 'Readers, writers, and admins match.', + }, + write: { + value: 'write', + label: 'Writers and admins', + description: 'Writers and admins match.', + }, + admin: { + value: 'admin', + label: 'Admins only', + description: 'Only workspace admins match.', + }, +} as const satisfies PresentationOptionsByValue + +const PRINCIPAL_KIND_OPTIONS_BY_VALUE = { + session: { value: 'session', label: 'Signed-in user' }, + personal_api_key: { value: 'personal_api_key', label: 'Personal API key' }, + workspace_api_key: { value: 'workspace_api_key', label: 'Workspace API key' }, + delegated: { value: 'delegated', label: 'Delegated service' }, + system: { value: 'system', label: 'System service' }, + credential_group_enrollment: { + value: 'credential_group_enrollment', + label: 'Credential group enrollment', + }, +} as const satisfies PresentationOptionsByValue + +const PRINCIPAL_SERVICE_OPTIONS_BY_VALUE = { + copilot: { value: 'copilot', label: 'Copilot' }, + realtime: { value: 'realtime', label: 'Realtime' }, + executor: { value: 'executor', label: 'Workflow executor' }, + public_api: { value: 'public_api', label: 'Public API' }, + schedule: { value: 'schedule', label: 'Schedule' }, + internal: { value: 'internal', label: 'Internal service' }, + table: { value: 'table', label: 'Table service' }, + chat: { value: 'chat', label: 'Chat service' }, + webhook: { value: 'webhook', label: 'Webhook' }, +} as const satisfies PresentationOptionsByValue + +const WORKFLOW_MODE_OPTIONS_BY_VALUE = { + draft: { + value: 'draft', + label: 'Draft', + description: 'Matches manual runs of the editable workflow.', + }, + deployment: { + value: 'deployment', + label: 'Deployed', + description: 'Matches runs of a deployed workflow.', + }, +} as const satisfies PresentationOptionsByValue + +export const RESOURCE_POLICY_RESOURCE_PRESENTATION = { + credential_group: { + label: 'Credential group', + description: 'Controls who can use credentials enrolled in this group.', + }, +} as const satisfies Record + +export const RESOURCE_POLICY_ACTION_PRESENTATION = { + 'credential_groups.credentials.use': { + label: 'Use credentials', + description: 'Assume and use credentials enrolled in the credential group.', + }, +} as const satisfies Record + +export const RESOURCE_POLICY_PRINCIPAL_PRESENTATION = { + any: { + label: 'Anyone', + description: 'Matches every principal.', + }, + user: { + label: 'User', + description: 'Matches a specific Sim user.', + }, + workspace_role: { + label: 'Workspace role', + description: 'Matches users whose workspace role meets a minimum level.', + valueOptions: Object.values(WORKSPACE_ROLE_OPTIONS_BY_VALUE), + }, + access_control_group: { + label: 'Permission group', + description: 'Matches users assigned to a specific permission group.', + }, + workflow: { + label: 'Workflow', + description: 'Matches runs of a specific workflow.', + }, + external_identity: { + label: 'External identity', + description: 'Matches one provider identity by provider, tenant, and subject ID.', + }, +} as const satisfies Record + +export const RESOURCE_POLICY_CONDITION_OPERATOR_PRESENTATION = { + StringEquals: { label: 'Equals' }, + StringNotEquals: { label: 'Does not equal' }, + StringLike: { label: 'Matches pattern' }, + StringNotLike: { label: 'Does not match pattern' }, + Bool: { label: 'Boolean equals' }, + Null: { label: 'Is absent or present' }, + 'ForAnyValue:StringEquals': { label: 'Any value equals' }, + 'ForAllValues:StringEquals': { label: 'Every value equals' }, +} as const satisfies Record< + ResourcePolicyConditionOperator, + ResourcePolicyConditionOperatorPresentation +> + +function operatorsForValueType( + valueType: ResourcePolicyContextValueType +): readonly ResourcePolicyConditionOperator[] { + switch (valueType) { + case 'string': + return ['StringEquals', 'StringNotEquals', 'StringLike', 'StringNotLike', 'Null'] + case 'string_list': + return ['ForAnyValue:StringEquals', 'ForAllValues:StringEquals', 'Null'] + case 'boolean': + return ['Bool', 'Null'] + } +} + +function globalConditionPresentation( + key: keyof typeof GLOBAL_RESOURCE_POLICY_CONDITION_KEYS, + presentation: Omit +): ResourcePolicyConditionKeyPresentation { + const valueType = GLOBAL_RESOURCE_POLICY_CONDITION_KEYS[key].valueType + return { ...presentation, valueType, operators: operatorsForValueType(valueType) } +} + +function resourceConditionPresentation( + resourceType: ResourcePolicyResourceType, + key: string, + presentation: Omit +): ResourcePolicyConditionKeyPresentation { + const definition = getResourcePolicyConditionKeyDefinition(resourceType, key) + if (!definition) { + throw new Error( + `Resource policy condition key ${key} is not registered for resource type ${resourceType}` + ) + } + return { + ...presentation, + valueType: definition.valueType, + operators: operatorsForValueType(definition.valueType), + } +} + +export const RESOURCE_POLICY_CONDITION_PRESENTATION = { + 'sim:PrincipalKind': globalConditionPresentation('sim:PrincipalKind', { + label: 'Actor type', + description: 'How the request was authenticated or delegated.', + valueOptions: Object.values(PRINCIPAL_KIND_OPTIONS_BY_VALUE), + }), + 'sim:PrincipalUserId': globalConditionPresentation('sim:PrincipalUserId', { + label: 'Actor user ID', + description: 'The canonical Sim user ID represented by the actor.', + }), + 'sim:PrincipalKeyId': globalConditionPresentation('sim:PrincipalKeyId', { + label: 'API key ID', + description: 'The personal or workspace API key used by the actor.', + }), + 'sim:PrincipalServiceId': globalConditionPresentation('sim:PrincipalServiceId', { + label: 'Service', + description: 'The delegated or system service making the request.', + valueOptions: Object.values(PRINCIPAL_SERVICE_OPTIONS_BY_VALUE), + }), + 'sim:PrincipalExternalProvider': globalConditionPresentation('sim:PrincipalExternalProvider', { + label: 'External provider', + description: 'The provider that established the external identity, such as Slack.', + }), + 'sim:PrincipalExternalTenantId': globalConditionPresentation('sim:PrincipalExternalTenantId', { + label: 'External tenant ID', + description: 'The provider tenant or workspace containing the external identity.', + }), + 'sim:PrincipalExternalSubjectId': globalConditionPresentation('sim:PrincipalExternalSubjectId', { + label: 'External subject ID', + description: 'The provider-stable user ID for the external identity.', + }), + 'sim:WorkflowId': globalConditionPresentation('sim:WorkflowId', { + label: 'Workflow ID', + description: 'The workflow currently exercising the permission.', + }), + 'sim:WorkflowMode': globalConditionPresentation('sim:WorkflowMode', { + label: 'Workflow mode', + description: 'Whether the workflow is running as a draft or deployment.', + valueOptions: Object.values(WORKFLOW_MODE_OPTIONS_BY_VALUE), + }), + 'sim:WorkspaceId': globalConditionPresentation('sim:WorkspaceId', { + label: 'Workspace ID', + description: 'The workspace containing the protected resource.', + }), + 'credential_group:CredentialEnrollmentId': resourceConditionPresentation( + 'credential_group', + 'credential_group:CredentialEnrollmentId', + { + label: 'Credential enrollment ID', + description: 'The credential enrollment selected for this operation.', + } + ), + 'sim:PrincipalCredentialGroupEnrollmentId': resourceConditionPresentation( + 'credential_group', + 'sim:PrincipalCredentialGroupEnrollmentId', + { + label: 'Actor enrollment ID', + description: 'The credential group enrollment represented by the acting principal.', + } + ), +} as const satisfies Record + +export const RESOURCE_POLICY_PRESENTATION = { + resources: RESOURCE_POLICY_RESOURCE_PRESENTATION, + actions: RESOURCE_POLICY_ACTION_PRESENTATION, + principals: RESOURCE_POLICY_PRINCIPAL_PRESENTATION, + operators: RESOURCE_POLICY_CONDITION_OPERATOR_PRESENTATION, + conditions: RESOURCE_POLICY_CONDITION_PRESENTATION, +} as const + +function requireResourcePolicyResourceType(resourceType: string): ResourcePolicyResourceType { + if (!Object.hasOwn(RESOURCE_POLICY_RESOURCE_PRESENTATION, resourceType)) { + throw new Error(`Resource policy resource type ${resourceType} is not registered`) + } + return resourceType as ResourcePolicyResourceType +} + +export function getResourcePolicyResourcePresentation( + resourceType: string +): ResourcePolicyResourcePresentation { + const registeredResourceType = requireResourcePolicyResourceType(resourceType) + return RESOURCE_POLICY_RESOURCE_PRESENTATION[registeredResourceType] +} + +export function getResourcePolicyActionPresentation( + resourceType: string, + action: string +): ResourcePolicyActionPresentation { + const registeredResourceType = requireResourcePolicyResourceType(resourceType) + const definition = getResourcePolicyDefinition(registeredResourceType) + if (!definition.actions.some((registeredAction) => registeredAction === action)) { + throw new Error( + `Resource policy action ${action} is not registered for resource type ${resourceType}` + ) + } + if (!Object.hasOwn(RESOURCE_POLICY_ACTION_PRESENTATION, action)) { + throw new Error(`Resource policy action ${action} has no presentation metadata`) + } + return RESOURCE_POLICY_ACTION_PRESENTATION[action as ResourcePolicyAction] +} + +export function getResourcePolicyPrincipalPresentation( + principalType: string +): ResourcePolicyPrincipalPresentation { + if (!Object.hasOwn(RESOURCE_POLICY_PRINCIPAL_PRESENTATION, principalType)) { + throw new Error(`Resource policy principal type ${principalType} is not registered`) + } + return RESOURCE_POLICY_PRINCIPAL_PRESENTATION[principalType as ResourcePolicyPrincipalType] +} + +export function getResourcePolicyConditionOperatorPresentation( + operator: ResourcePolicyConditionOperator +): ResourcePolicyConditionOperatorPresentation { + return RESOURCE_POLICY_CONDITION_OPERATOR_PRESENTATION[operator] +} + +export function getResourcePolicyConditionPresentation( + resourceType: string, + key: string +): ResourcePolicyConditionKeyPresentation { + const registeredResourceType = requireResourcePolicyResourceType(resourceType) + if (!getResourcePolicyConditionKeyDefinition(registeredResourceType, key)) { + throw new Error( + `Resource policy condition key ${key} is not registered for resource type ${resourceType}` + ) + } + if (!Object.hasOwn(RESOURCE_POLICY_CONDITION_PRESENTATION, key)) { + throw new Error(`Resource policy condition key ${key} has no presentation metadata`) + } + return RESOURCE_POLICY_CONDITION_PRESENTATION[key as ResourcePolicyConditionKey] +} + +export function getResourcePolicyActionPresentations( + resourceType: string +): readonly ResourcePolicyActionPresentation[] { + const registeredResourceType = requireResourcePolicyResourceType(resourceType) + return getResourcePolicyDefinition(registeredResourceType).actions.map((action) => + getResourcePolicyActionPresentation(registeredResourceType, action) + ) +} + +export function getResourcePolicyConditionPresentations( + resourceType: string +): readonly ResourcePolicyConditionKeyPresentation[] { + const registeredResourceType = requireResourcePolicyResourceType(resourceType) + const specificKeys = Object.keys( + RESOURCE_POLICY_DEFINITIONS[registeredResourceType].conditionKeys + ) + return [...Object.keys(GLOBAL_RESOURCE_POLICY_CONDITION_KEYS), ...specificKeys].map((key) => + getResourcePolicyConditionPresentation(registeredResourceType, key) + ) +} + +if (Object.keys(RESOURCE_POLICY_ACTION_PRESENTATION).length !== RESOURCE_POLICY_ACTIONS.length) { + throw new Error('Resource policy action presentation registry is incomplete') +} diff --git a/apps/sim/lib/resource-policies/registry.ts b/apps/sim/lib/resource-policies/registry.ts new file mode 100644 index 00000000000..14b8e77ecbb --- /dev/null +++ b/apps/sim/lib/resource-policies/registry.ts @@ -0,0 +1,115 @@ +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 = { + [ResourceType in ResourcePolicyResourceType]: keyof (typeof RESOURCE_POLICY_DEFINITIONS)[ResourceType]['conditionKeys'] +}[ResourcePolicyResourceType] +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 new file mode 100644 index 00000000000..065c39fcc91 --- /dev/null +++ b/apps/sim/lib/resource-policies/repository.ts @@ -0,0 +1,156 @@ +import { db } from '@sim/db' +import { resourcePolicy } from '@sim/db/schema' +import { and, eq } from 'drizzle-orm' +import type { DbOrTx } from '@/lib/db/types' +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 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') + this.name = 'ResourcePolicyRevisionConflictError' + } +} + +interface ResourcePolicyTarget { + workspaceId: string + resourceType: ResourcePolicyResourceType + resourceId: string +} + +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, + 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 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 + resourceId: string + expectedRevision: number + 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, + }) + + 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, + revision: input.expectedRevision + 1, + updatedBy: input.actorUserId, + updatedAt: new Date(), + }) + .where( + and(eq(resourcePolicy.id, existing.id), eq(resourcePolicy.revision, input.expectedRevision)) + ) + .returning() + if (!updated) throw new Error('Locked resource policy update returned no row') + return { + id: updated.id, + workspaceId: updated.workspaceId, + revision: updated.revision, + document, + createdAt: updated.createdAt, + updatedAt: updated.updatedAt, + } + }) +} + +export async function deleteResourcePolicyForResource( + input: ResourcePolicyTarget, + executor: DbOrTx +): Promise { + const deleted = await executor + .delete(resourcePolicy) + .where( + and( + eq(resourcePolicy.workspaceId, input.workspaceId), + eq(resourcePolicy.resourceType, input.resourceType), + eq(resourcePolicy.resourceId, input.resourceId) + ) + ) + .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 new file mode 100644 index 00000000000..620cb2b07c1 --- /dev/null +++ b/apps/sim/lib/resource-policies/types.test.ts @@ -0,0 +1,205 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it } from 'vitest' +import { + parseResourcePolicyDocument, + parseResourcePolicySystemStatements, +} from '@/lib/resource-policies/types' + +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 recursive condition expressions and unknown fields', () => { + expect(() => + parseResourcePolicyDocument( + { + ...policy(), + statements: [ + { + ...policy().statements[0], + condition: { all: [{ StringEquals: { 'sim:WorkflowMode': 'deployment' } }] }, + }, + ], + }, + { type: 'credential_group', id: 'group-1' } + ) + ).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 blank string condition values', () => { + for (const value of ['', ' ', ['deployment', '']]) { + expect(() => + parseResourcePolicyDocument( + { + ...policy(), + statements: [ + { + ...policy().statements[0], + condition: { StringEquals: { 'sim:WorkflowMode': value } }, + }, + ], + }, + { type: 'credential_group', id: 'group-1' } + ) + ).toThrow('Condition values must not be blank') + } + }) + + 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( + { + ...policy(), + statements: [ + { + ...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() + }) + + 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( + { + ...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 new file mode 100644 index 00000000000..1a93dace928 --- /dev/null +++ b/apps/sim/lib/resource-policies/types.ts @@ -0,0 +1,336 @@ +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 { + RESOURCE_POLICY_ACTIONS, + RESOURCE_POLICY_RESOURCE_TYPES, + type ResourcePolicyAction, + type ResourcePolicyConditionKey, + type ResourcePolicyResourceType, +} from '@/lib/resource-policies/registry' + +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 userPrincipalSchema = z + .object({ type: z.literal('user'), userId: z.string().min(1).max(128) }) + .strict() +const workspaceRolePrincipalSchema = z + .object({ + type: z.literal('workspace_role'), + minimumRole: z.enum(['read', 'write', 'admin']), + }) + .strict() +const accessControlGroupPrincipalSchema = z + .object({ + type: z.literal('access_control_group'), + accessControlGroupId: z.string().min(1).max(128), + }) + .strict() +const workflowPrincipalSchema = z + .object({ type: z.literal('workflow'), workflowId: z.string().min(1).max(128) }) + .strict() +const externalIdentityPrincipalSchema = 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() +const anyPrincipalSchema = z.object({ type: z.literal('any') }).strict() + +export const resourcePolicyPrincipalSchema = z.discriminatedUnion('type', [ + anyPrincipalSchema, + userPrincipalSchema, + workspaceRolePrincipalSchema, + accessControlGroupPrincipalSchema, + workflowPrincipalSchema, + externalIdentityPrincipalSchema, +]) + +export type ResourcePolicyPrincipal = z.output + +const nonBlankConditionStringSchema = z + .string() + .max(1024) + .refine((value) => value.trim().length > 0, 'Condition values must not be blank') +const stringConditionValueSchema = z.union([ + nonBlankConditionStringSchema, + z.array(nonBlankConditionStringSchema).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 resourcePolicyConditionSchema = z + .object({ + 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((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: [operator], + message: `Resource policy condition operator ${operator} must contain at least one key`, + }) + } + 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 type ResourcePolicyConditionOperator = keyof ResourcePolicyCondition + +const RESOURCE_POLICY_CONDITION_OPERATOR_ORDER = { + StringEquals: true, + StringNotEquals: true, + StringLike: true, + StringNotLike: true, + Bool: true, + Null: true, + 'ForAnyValue:StringEquals': true, + 'ForAllValues:StringEquals': true, +} as const satisfies Record + +export const RESOURCE_POLICY_CONDITION_OPERATORS = Object.keys( + RESOURCE_POLICY_CONDITION_OPERATOR_ORDER +) as ResourcePolicyConditionOperator[] + +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: ['statements', statementIndex, 'condition', operator, key], + message: `Condition key ${key} does not apply to ${resourceType}`, + }) + continue + } + const expectedType = expectedConditionValueType(operator) + if (expectedType !== 'any' && definition.valueType !== expectedType) { + ctx.addIssue({ + code: 'custom', + path: ['statements', statementIndex, 'condition', operator, key], + message: `${operator} cannot evaluate ${definition.valueType} condition key ${key}`, + }) + } + 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: ['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 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 +} + +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/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..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 is implemented; group-wide resource grants are proposed +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 with a verified actor may list and use only that actor's credentials. +> 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 @@ -41,18 +41,18 @@ 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) -> 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,63 +60,47 @@ List Credentials Its contract is: -> Return every credential that this execution is currently authorized to 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 workspace access, and evaluates the current resource policy. +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. -```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. +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. -## Credential use authorization +## Credential-use chokepoint -OAuth token material is resolved only inside a protected application operation. Every use performs a fresh authorization check: +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; @@ -124,69 +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.list -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.list", - "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.list", - "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 -| Execution | 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 | +For `credential_groups.credentials.use`, evaluation combines the hidden system statement with all stored statements: -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. +```text +matching stored deny + -> deny + +otherwise matching stored or system allow + -> allow + +otherwise + -> implicit deny +``` -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. +Principals within a statement are ORed. Condition operators and keys are ANDed. Statement ordering never overrides explicit-deny precedence. + +| 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 @@ -194,38 +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 list/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 +## Management boundary and raw JSON UI -Credential Group creation, options, invitations, enrollment lifecycle, and resource-policy changes remain control-plane operations requiring current workspace-admin authorization and audit. +Credential Group creation, options, invitations, enrollment lifecycle, and resource-policy changes are control-plane operations requiring a current workspace-admin session 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. +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. -## Current implementation delta +The hidden actor-own system statement is explained but not displayed. Policy management does not automatically grant the administrator credential use. -The branch already: +## Current scope -- 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; -- rechecks the same enrollment when resolving managed OAuth tokens; -- fails actorless execution instead of substituting a billing or workflow owner. +The implementation: -Remaining work is: +- 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. -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..4cc1a862b6f 100644 --- a/design/resource-policies.md +++ b/design/resource-policies.md @@ -1,12 +1,12 @@ # Resource Policies -Status: proposed reusable authorization model +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,195 +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.list", - "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.list", - "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 -## Example Knowledge Base policy +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 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. - -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. +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. -Workspace administrators can manage policies through audited control-plane operations. Policy-management authority does not imply data-read authority. +## System statements -## Runtime subject resolution +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. -The evaluator receives trusted execution context and resolves current subjects: +Credential Groups currently add this effective statement: -```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 +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. -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. +## Resource registry -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. +The resource registry is the extension point for future protected resources. Each resource type registers: -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. +- 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. -Workflows do not implicitly inherit a human Access Control Group. Deployed workflows receive their own execution boundary and explicit resource grants. - -## Workflow authority and subworkflows - -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 @@ -228,18 +276,41 @@ resource_policy workspace_id resource_type resource_id - version - document_json + revision + document created_by + updated_by created_at 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 + +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. + +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. -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. +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. -Policy writes require current workspace-admin authorization, exact schema validation, semantic audit, and optimistic concurrency through the current policy version or update timestamp. +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 @@ -248,41 +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. - -## 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 list and 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/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/migrations/0303_luxuriant_payback.sql b/packages/db/migrations/0303_luxuriant_payback.sql new file mode 100644 index 00000000000..6cb13a08186 --- /dev/null +++ b/packages/db/migrations/0303_luxuriant_payback.sql @@ -0,0 +1,63 @@ +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");--> 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/migrations/meta/0303_snapshot.json b/packages/db/migrations/meta/0303_snapshot.json new file mode 100644 index 00000000000..eba1dc38628 --- /dev/null +++ b/packages/db/migrations/meta/0303_snapshot.json @@ -0,0 +1,20262 @@ +{ + "id": "3a69818c-ebde-4785-8fd1-9e5e4773d7e7", + "prevId": "9b3e8fbc-d5ed-4cce-8d8a-be1ccaa5b902", + "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 + }, + "trace_child_runs": { + "name": "trace_child_runs", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "custom_block_organization_id_idx": { + "name": "custom_block_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_block_workflow_id_idx": { + "name": "custom_block_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_block_organization_type_unique": { + "name": "custom_block_organization_type_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "custom_block_organization_id_organization_id_fk": { + "name": "custom_block_organization_id_organization_id_fk", + "tableFrom": "custom_block", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "custom_block_workflow_id_workflow_id_fk": { + "name": "custom_block_workflow_id_workflow_id_fk", + "tableFrom": "custom_block", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "custom_block_created_by_user_id_fk": { + "name": "custom_block_created_by_user_id_fk", + "tableFrom": "custom_block", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_tools": { + "name": "custom_tools", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema": { + "name": "schema", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "custom_tools_workspace_id_idx": { + "name": "custom_tools_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_tools_workspace_title_unique": { + "name": "custom_tools_workspace_title_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "title", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "custom_tools_workspace_id_workspace_id_fk": { + "name": "custom_tools_workspace_id_workspace_id_fk", + "tableFrom": "custom_tools", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "custom_tools_user_id_user_id_fk": { + "name": "custom_tools_user_id_user_id_fk", + "tableFrom": "custom_tools", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.data_drain_runs": { + "name": "data_drain_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "drain_id": { + "name": "drain_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "data_drain_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "data_drain_run_trigger", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "rows_exported": { + "name": "rows_exported", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "bytes_written": { + "name": "bytes_written", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cursor_before": { + "name": "cursor_before", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cursor_after": { + "name": "cursor_after", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "locators": { + "name": "locators", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + } + }, + "indexes": { + "data_drain_runs_drain_started_idx": { + "name": "data_drain_runs_drain_started_idx", + "columns": [ + { + "expression": "drain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "data_drain_runs_drain_id_data_drains_id_fk": { + "name": "data_drain_runs_drain_id_data_drains_id_fk", + "tableFrom": "data_drain_runs", + "tableTo": "data_drains", + "columnsFrom": ["drain_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.data_drains": { + "name": "data_drains", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "data_drain_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "destination_type": { + "name": "destination_type", + "type": "data_drain_destination", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "destination_config": { + "name": "destination_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "destination_credentials": { + "name": "destination_credentials", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schedule_cadence": { + "name": "schedule_cadence", + "type": "data_drain_cadence", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "cursor": { + "name": "cursor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_success_at": { + "name": "last_success_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "data_drains_org_idx": { + "name": "data_drains_org_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "data_drains_due_idx": { + "name": "data_drains_due_idx", + "columns": [ + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "data_drains_org_name_unique": { + "name": "data_drains_org_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "data_drains_organization_id_organization_id_fk": { + "name": "data_drains_organization_id_organization_id_fk", + "tableFrom": "data_drains", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "data_drains_created_by_user_id_fk": { + "name": "data_drains_created_by_user_id_fk", + "tableFrom": "data_drains", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.docs_embeddings": { + "name": "docs_embeddings", + "schema": "", + "columns": { + "chunk_id": { + "name": "chunk_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chunk_text": { + "name": "chunk_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_document": { + "name": "source_document", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_link": { + "name": "source_link", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_text": { + "name": "header_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_level": { + "name": "header_level", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": true + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "chunk_text_tsv": { + "name": "chunk_text_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"docs_embeddings\".\"chunk_text\")", + "type": "stored" + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "docs_emb_source_document_idx": { + "name": "docs_emb_source_document_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_header_level_idx": { + "name": "docs_emb_header_level_idx", + "columns": [ + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_source_header_idx": { + "name": "docs_emb_source_header_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_model_idx": { + "name": "docs_emb_model_idx", + "columns": [ + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_created_at_idx": { + "name": "docs_emb_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_embedding_vector_hnsw_idx": { + "name": "docs_embedding_vector_hnsw_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "docs_emb_metadata_gin_idx": { + "name": "docs_emb_metadata_gin_idx", + "columns": [ + { + "expression": "metadata", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "docs_emb_chunk_text_fts_idx": { + "name": "docs_emb_chunk_text_fts_idx", + "columns": [ + { + "expression": "chunk_text_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "docs_embedding_not_null_check": { + "name": "docs_embedding_not_null_check", + "value": "\"embedding\" IS NOT NULL" + }, + "docs_header_level_check": { + "name": "docs_header_level_check", + "value": "\"header_level\" >= 1 AND \"header_level\" <= 6" + } + }, + "isRLSEnabled": false + }, + "public.document": { + "name": "document", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_url": { + "name": "file_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storage_key": { + "name": "storage_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "file_size": { + "name": "file_size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_count": { + "name": "chunk_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "character_count": { + "name": "character_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "processing_status": { + "name": "processing_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "processing_attempts": { + "name": "processing_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "processing_queued_at": { + "name": "processing_queued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_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 + }, + "sync_lock_token": { + "name": "sync_lock_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_lock_lease_at": { + "name": "sync_lock_lease_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "kc_knowledge_base_id_idx": { + "name": "kc_knowledge_base_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_status_next_sync_idx": { + "name": "kc_status_next_sync_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_sync_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_archived_at_partial_idx": { + "name": "kc_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_deleted_at_partial_idx": { + "name": "kc_deleted_at_partial_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "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": {} + }, + "kcsl_started_at_partial_idx": { + "name": "kcsl_started_at_partial_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector_sync_log\".\"status\" = 'started'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_connector_sync_log_connector_id_knowledge_connector_id_fk": { + "name": "knowledge_connector_sync_log_connector_id_knowledge_connector_id_fk", + "tableFrom": "knowledge_connector_sync_log", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.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()" + }, + "heartbeat_at": { + "name": "heartbeat_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "table_run_dispatches_active_idx": { + "name": "table_run_dispatches_active_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "table_run_dispatches_watchdog_idx": { + "name": "table_run_dispatches_watchdog_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "requested_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "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', 'unrecorded')" + } + }, + "isRLSEnabled": false + }, + "public.workspace_files": { + "name": "workspace_files", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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 f5a552a82f2..ebd3f437343 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -2115,6 +2115,13 @@ "when": 1787432959648, "tag": "0302_fat_sentry", "breakpoints": true + }, + { + "idx": 303, + "version": "7", + "when": 1787516167081, + "tag": "0303_luxuriant_payback", + "breakpoints": true } ] } diff --git a/packages/db/schema.ts b/packages/db/schema.ts index dfe5f69af46..15d2e9c83a9 100644 --- a/packages/db/schema.ts +++ b/packages/db/schema.ts @@ -4287,6 +4287,32 @@ export const permissionGroupMember = pgTable( }) ) +/** Versioned statement 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/packages/db/script-migrations-paused-billing-attribution.test.ts b/packages/db/script-migrations-paused-billing-attribution.test.ts index 6d53d3146e1..1eb6bb4ada7 100644 --- a/packages/db/script-migrations-paused-billing-attribution.test.ts +++ b/packages/db/script-migrations-paused-billing-attribution.test.ts @@ -444,6 +444,7 @@ describe('script migration registry', () => { '0005_repair_unknown_table_row_provenance', '0006_repair_unknown_table_row_provenance_second_pass', '0007_repair_unknown_workspace_file_provenance', + '0008_backfill_credential_group_resource_policies', ]) }) }) diff --git a/packages/db/script-migrations/0008_backfill_credential_group_resource_policies.test.ts b/packages/db/script-migrations/0008_backfill_credential_group_resource_policies.test.ts new file mode 100644 index 00000000000..d347e1c0599 --- /dev/null +++ b/packages/db/script-migrations/0008_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 './0008_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/0303_luxuriant_payback.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/0008_backfill_credential_group_resource_policies.ts b/packages/db/script-migrations/0008_backfill_credential_group_resource_policies.ts new file mode 100644 index 00000000000..89d78ce4ba7 --- /dev/null +++ b/packages/db/script-migrations/0008_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: '0008_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 b845d7f4ebc..c70d2b524c1 100644 --- a/packages/db/script-migrations/index.ts +++ b/packages/db/script-migrations/index.ts @@ -6,6 +6,7 @@ import { backfillForkKnowledgeBaseFileOwnership } from './0004_backfill_fork_kb_ import { repairUnknownTableRowProvenance } from './0005_repair_unknown_table_row_provenance' import { repairUnknownTableRowProvenanceSecondPass } from './0006_repair_unknown_table_row_provenance_second_pass' import { repairUnknownWorkspaceFileProvenance } from './0007_repair_unknown_workspace_file_provenance' +import { backfillCredentialGroupResourcePolicies } from './0008_backfill_credential_group_resource_policies' import type { ScriptMigration } from './types' export type { ScriptMigration } from './types' @@ -23,6 +24,7 @@ export const scriptMigrations: readonly ScriptMigration[] = [ repairUnknownTableRowProvenance, repairUnknownTableRowProvenanceSecondPass, repairUnknownWorkspaceFileProvenance, + backfillCredentialGroupResourcePolicies, ] /** diff --git a/packages/emcn/src/components/chip-dropdown/chip-dropdown.test.tsx b/packages/emcn/src/components/chip-dropdown/chip-dropdown.test.tsx new file mode 100644 index 00000000000..e6a916f868e --- /dev/null +++ b/packages/emcn/src/components/chip-dropdown/chip-dropdown.test.tsx @@ -0,0 +1,48 @@ +/** + * @vitest-environment jsdom + */ +import { act } from 'react' +import { createRoot, type Root } from 'react-dom/client' +import { afterEach, describe, expect, it } from 'vitest' +import { ChipDropdown } from './chip-dropdown' + +let root: Root | null = null +let container: HTMLDivElement | null = null + +function mount(fullWidth: boolean): HTMLButtonElement { + ;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true + container = document.createElement('div') + document.body.appendChild(container) + root = createRoot(container) + act(() => + root?.render( + + ) + ) + + const trigger = container.querySelector('button') + if (!trigger) throw new Error('ChipDropdown did not render a trigger') + return trigger +} + +afterEach(() => { + if (root) act(() => root?.unmount()) + container?.remove() + root = null + container = null +}) + +describe('ChipDropdown', () => { + it('fills its container when fullWidth is enabled', () => { + expect(mount(true).className).toContain('w-full') + }) + + it('keeps its intrinsic width by default', () => { + expect(mount(false).className).not.toContain('w-full') + }) +}) diff --git a/packages/emcn/src/components/chip-select/chip-select.tsx b/packages/emcn/src/components/chip-select/chip-select.tsx index 94b4da1885e..e2cb64787e5 100644 --- a/packages/emcn/src/components/chip-select/chip-select.tsx +++ b/packages/emcn/src/components/chip-select/chip-select.tsx @@ -252,7 +252,7 @@ export function ChipSelect({ className={cn( chipVariants({ variant: 'filled', fullWidth }), TRIGGER_BORDER_CLASS, - fullWidth ? 'w-full justify-between' : 'w-fit max-w-[240px]', + fullWidth ? 'justify-between' : 'w-fit max-w-[240px]', className )} > diff --git a/packages/emcn/src/components/chip/chip.tsx b/packages/emcn/src/components/chip/chip.tsx index aa80a5d225a..a52b3740e1e 100644 --- a/packages/emcn/src/components/chip/chip.tsx +++ b/packages/emcn/src/components/chip/chip.tsx @@ -68,7 +68,7 @@ const chipVariants = cva( border: `shadow-[0_0_0_1px_rgba(28,40,64,0.08),0_1px_3px_0_rgba(28,40,64,0.1)] ${chipHoverSurfaceClass} dark:shadow-[0_0_0_1px_var(--border-1),0_1px_3px_0_rgba(0,0,0,0.3)]`, }, active: { true: '', false: '' }, - fullWidth: { true: 'flex', false: 'inline-flex' }, + fullWidth: { true: 'flex w-full', false: 'inline-flex' }, }, compoundVariants: [ { variant: ['default', 'filled'], active: false, className: chipHoverSurfaceClass }, diff --git a/packages/testing/src/mocks/schema.mock.ts b/packages/testing/src/mocks/schema.mock.ts index 96004529138..b500f8ea7e3 100644 --- a/packages/testing/src/mocks/schema.mock.ts +++ b/packages/testing/src/mocks/schema.mock.ts @@ -1183,6 +1183,18 @@ export const schemaMock = { createdAt: 'credentialGroup.createdAt', updatedAt: 'credentialGroup.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, }, diff --git a/scripts/check-api-validation-contracts.ts b/scripts/check-api-validation-contracts.ts index 3f66bbc8a2f..26a4d82153d 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: 1162, - zodRoutes: 1162, + totalRoutes: 1163, + zodRoutes: 1163, nonZodRoutes: 0, } as const