From 8ce56fe1f224cf4a2bd6f075707323ed34f636b8 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Wed, 22 Apr 2026 15:30:37 -0700 Subject: [PATCH 1/4] fix(auth): add api key auth via sha256 hash lookup (#4266) * fix(auth): add api key auth via sha256 hash lookup * Remove promise all logic * Restore feature flag * fix feature flag * Combine auth and hash gate --- apps/sim/app/api/users/me/api-keys/route.ts | 2 + .../app/api/workspaces/[id]/api-keys/route.ts | 2 + apps/sim/lib/api-key/auth.ts | 2 + apps/sim/lib/api-key/crypto.test.ts | 89 + apps/sim/lib/api-key/crypto.ts | 15 +- apps/sim/lib/api-key/service.test.ts | 189 + apps/sim/lib/api-key/service.ts | 106 +- apps/sim/lib/workspaces/utils.ts | 2 +- .../db/migrations/0195_normal_white_queen.sql | 2 + .../db/migrations/meta/0195_snapshot.json | 15231 ++++++++++++++++ packages/db/migrations/meta/_journal.json | 7 + packages/db/package.json | 1 + packages/db/schema.ts | 2 + .../db/scripts/backfill-api-key-hash.test.ts | 78 + packages/db/scripts/backfill-api-key-hash.ts | 231 + packages/db/vitest.config.ts | 9 + packages/testing/src/mocks/schema.mock.ts | 1 + 17 files changed, 15954 insertions(+), 15 deletions(-) create mode 100644 apps/sim/lib/api-key/crypto.test.ts create mode 100644 apps/sim/lib/api-key/service.test.ts create mode 100644 packages/db/migrations/0195_normal_white_queen.sql create mode 100644 packages/db/migrations/meta/0195_snapshot.json create mode 100644 packages/db/scripts/backfill-api-key-hash.test.ts create mode 100644 packages/db/scripts/backfill-api-key-hash.ts create mode 100644 packages/db/vitest.config.ts diff --git a/apps/sim/app/api/users/me/api-keys/route.ts b/apps/sim/app/api/users/me/api-keys/route.ts index bd6d6a0fed9..098f9fe3029 100644 --- a/apps/sim/app/api/users/me/api-keys/route.ts +++ b/apps/sim/app/api/users/me/api-keys/route.ts @@ -5,6 +5,7 @@ import { generateShortId } from '@sim/utils/id' import { and, eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { createApiKey, getApiKeyDisplayFormat } from '@/lib/api-key/auth' +import { hashApiKey } from '@/lib/api-key/crypto' import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { getSession } from '@/lib/auth' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' @@ -102,6 +103,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { workspaceId: null, name, key: encryptedKey, + keyHash: hashApiKey(plainKey), type: 'personal', createdAt: new Date(), updatedAt: new Date(), diff --git a/apps/sim/app/api/workspaces/[id]/api-keys/route.ts b/apps/sim/app/api/workspaces/[id]/api-keys/route.ts index 451757ad949..b9404581796 100644 --- a/apps/sim/app/api/workspaces/[id]/api-keys/route.ts +++ b/apps/sim/app/api/workspaces/[id]/api-keys/route.ts @@ -6,6 +6,7 @@ import { and, eq, inArray } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { createApiKey, getApiKeyDisplayFormat } from '@/lib/api-key/auth' +import { hashApiKey } from '@/lib/api-key/crypto' import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { getSession } from '@/lib/auth' import { PlatformEvents } from '@/lib/core/telemetry' @@ -145,6 +146,7 @@ export const POST = withRouteHandler( createdBy: userId, name, key: encryptedKey, + keyHash: hashApiKey(plainKey), type: 'workspace', createdAt: new Date(), updatedAt: new Date(), diff --git a/apps/sim/lib/api-key/auth.ts b/apps/sim/lib/api-key/auth.ts index ac194d3a5e5..8c359565630 100644 --- a/apps/sim/lib/api-key/auth.ts +++ b/apps/sim/lib/api-key/auth.ts @@ -8,6 +8,7 @@ import { encryptApiKey, generateApiKey, generateEncryptedApiKey, + hashApiKey, isEncryptedApiKeyFormat, isLegacyApiKeyFormat, } from '@/lib/api-key/crypto' @@ -256,6 +257,7 @@ export async function createWorkspaceApiKey(params: { createdBy: params.userId, name: params.name, key: encryptedKey, + keyHash: hashApiKey(plainKey), type: 'workspace', createdAt: new Date(), updatedAt: new Date(), diff --git a/apps/sim/lib/api-key/crypto.test.ts b/apps/sim/lib/api-key/crypto.test.ts new file mode 100644 index 00000000000..f6243b876e4 --- /dev/null +++ b/apps/sim/lib/api-key/crypto.test.ts @@ -0,0 +1,89 @@ +/** + * Tests for the API-key crypto primitives. + * + * `hashApiKey` is the foundation of both the new hash-first authentication + * path and the `backfill-api-key-hash` script — the backfill is idempotent + * precisely because `hashApiKey` is deterministic and the encrypted round-trip + * recovers the same plain-text key on every run. + * + * @vitest-environment node + */ +import { randomBytes } from 'crypto' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { mockEnv } = vi.hoisted(() => ({ + mockEnv: { API_ENCRYPTION_KEY: undefined as string | undefined }, +})) + +vi.mock('@/lib/core/config/env', () => ({ + env: mockEnv, +})) + +import { + decryptApiKey, + encryptApiKey, + hashApiKey, + isEncryptedApiKeyFormat, + isLegacyApiKeyFormat, +} from '@/lib/api-key/crypto' + +const FIXED_ENCRYPTION_KEY = '0'.repeat(64) + +describe('hashApiKey', () => { + it('is deterministic — same input produces same hash', () => { + const h1 = hashApiKey('sk-sim-example') + const h2 = hashApiKey('sk-sim-example') + expect(h1).toBe(h2) + }) + + it('produces a 64-char hex SHA-256 digest', () => { + const hash = hashApiKey('sk-sim-example') + expect(hash).toMatch(/^[0-9a-f]{64}$/) + }) + + it('produces different hashes for different inputs', () => { + expect(hashApiKey('sk-sim-a')).not.toBe(hashApiKey('sk-sim-b')) + }) + + it('matches the published SHA-256 vector for the empty string', () => { + expect(hashApiKey('')).toBe('e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855') + }) +}) + +describe('backfill idempotency — encrypted round-trip', () => { + beforeEach(() => { + mockEnv.API_ENCRYPTION_KEY = FIXED_ENCRYPTION_KEY + }) + + it('re-running the backfill on the same row yields the same keyHash', async () => { + const plainKey = `sk-sim-${randomBytes(12).toString('hex')}` + const { encrypted } = await encryptApiKey(plainKey) + + const { decrypted: first } = await decryptApiKey(encrypted) + const { decrypted: second } = await decryptApiKey(encrypted) + + expect(first).toBe(plainKey) + expect(second).toBe(plainKey) + expect(hashApiKey(first)).toBe(hashApiKey(second)) + }) + + it('is stable whether the stored key is legacy plain text or encrypted', async () => { + const plainKey = 'sim_legacy-format-key' + const { encrypted } = await encryptApiKey(plainKey) + + const { decrypted } = await decryptApiKey(encrypted) + expect(hashApiKey(decrypted)).toBe(hashApiKey(plainKey)) + }) +}) + +describe('api-key format helpers', () => { + it('treats sk-sim- prefix as the encrypted format', () => { + expect(isEncryptedApiKeyFormat('sk-sim-abc')).toBe(true) + expect(isLegacyApiKeyFormat('sk-sim-abc')).toBe(false) + }) + + it('treats sim_ prefix as the legacy format', () => { + expect(isLegacyApiKeyFormat('sim_abc')).toBe(true) + expect(isEncryptedApiKeyFormat('sim_abc')).toBe(false) + }) +}) diff --git a/apps/sim/lib/api-key/crypto.ts b/apps/sim/lib/api-key/crypto.ts index aeee9097218..3e3118ccbb8 100644 --- a/apps/sim/lib/api-key/crypto.ts +++ b/apps/sim/lib/api-key/crypto.ts @@ -1,4 +1,4 @@ -import { createCipheriv, createDecipheriv, randomBytes } from 'crypto' +import { createCipheriv, createDecipheriv, createHash, randomBytes } from 'crypto' import { createLogger } from '@sim/logger' import { env } from '@/lib/core/config/env' @@ -131,3 +131,16 @@ export function isEncryptedApiKeyFormat(apiKey: string): boolean { export function isLegacyApiKeyFormat(apiKey: string): boolean { return apiKey.startsWith('sim_') && !apiKey.startsWith('sk-sim-') } + +/** + * Deterministically hashes a plain-text API key for indexed lookup. The hash + * column has a unique index so authentication can match an incoming key via a + * single `WHERE key_hash = $hash` lookup instead of scanning and decrypting + * every stored encrypted key. + * + * @param plainKey - The plain-text API key as presented by the client + * @returns The hex-encoded SHA-256 digest + */ +export function hashApiKey(plainKey: string): string { + return createHash('sha256').update(plainKey, 'utf8').digest('hex') +} diff --git a/apps/sim/lib/api-key/service.test.ts b/apps/sim/lib/api-key/service.test.ts new file mode 100644 index 00000000000..c38b4a7d550 --- /dev/null +++ b/apps/sim/lib/api-key/service.test.ts @@ -0,0 +1,189 @@ +/** + * Tests for authenticateApiKeyFromHeader. + * + * The path was rewritten to look up rows by the SHA-256 hash of the incoming + * API key. A fallback loop — full scan + decrypt — is preserved while the + * `key_hash` backfill runs, and emits a warn log whenever it actually matches + * a row so we can tell when it's safe to delete. + * + * @vitest-environment node + */ +import { dbChainMock, dbChainMockFns } from '@sim/testing' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +vi.mock('@sim/db', () => dbChainMock) + +const { serviceLogger } = vi.hoisted(() => { + const logger = { + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + debug: vi.fn(), + trace: vi.fn(), + fatal: vi.fn(), + child: vi.fn(), + withMetadata: vi.fn(), + } + logger.child.mockReturnValue(logger) + logger.withMetadata.mockReturnValue(logger) + return { serviceLogger: logger } +}) + +vi.mock('@sim/logger', () => ({ + createLogger: vi.fn(() => serviceLogger), + logger: serviceLogger, + runWithRequestContext: vi.fn((_ctx: unknown, fn: () => T): T => fn()), + getRequestContext: vi.fn(() => undefined), +})) + +const { mockAuthenticateApiKey } = vi.hoisted(() => ({ + mockAuthenticateApiKey: vi.fn(), +})) + +vi.mock('@/lib/api-key/auth', () => ({ + authenticateApiKey: mockAuthenticateApiKey, +})) + +const { mockGetWorkspaceBillingSettings } = vi.hoisted(() => ({ + mockGetWorkspaceBillingSettings: vi.fn(), +})) + +vi.mock('@/lib/workspaces/utils', () => ({ + getWorkspaceBillingSettings: mockGetWorkspaceBillingSettings, +})) + +const { mockGetUserEntityPermissions } = vi.hoisted(() => ({ + mockGetUserEntityPermissions: vi.fn(), +})) + +vi.mock('@/lib/workspaces/permissions/utils', () => ({ + getUserEntityPermissions: mockGetUserEntityPermissions, +})) + +import { hashApiKey } from '@/lib/api-key/crypto' +import { authenticateApiKeyFromHeader } from '@/lib/api-key/service' + +const warnSpy = serviceLogger.warn + +function personalKeyRecord(overrides: Partial> = {}) { + return { + id: 'key-1', + userId: 'user-1', + workspaceId: null as string | null, + type: 'personal', + key: 'encrypted:stored:value', + expiresAt: null as Date | null, + ...overrides, + } +} + +describe('authenticateApiKeyFromHeader', () => { + beforeEach(() => { + vi.clearAllMocks() + mockAuthenticateApiKey.mockReset() + mockGetWorkspaceBillingSettings.mockReset() + mockGetUserEntityPermissions.mockReset() + }) + + it('returns error when no header is provided', async () => { + const result = await authenticateApiKeyFromHeader('') + expect(result).toEqual({ success: false, error: 'API key required' }) + expect(dbChainMockFns.where).not.toHaveBeenCalled() + }) + + it('resolves on the fast path when the hash lookup finds a row', async () => { + const record = personalKeyRecord() + dbChainMockFns.where.mockResolvedValueOnce([record]) + + const result = await authenticateApiKeyFromHeader('sk-sim-plain-key', { + userId: 'user-1', + }) + + expect(result).toEqual({ + success: true, + userId: 'user-1', + keyId: 'key-1', + keyType: 'personal', + workspaceId: undefined, + }) + expect(dbChainMockFns.where).toHaveBeenCalledTimes(1) + expect(mockAuthenticateApiKey).not.toHaveBeenCalled() + expect(warnSpy).not.toHaveBeenCalled() + }) + + it('returns invalid when the hash lookup finds a row that fails scope checks', async () => { + const record = personalKeyRecord({ userId: 'other-user' }) + dbChainMockFns.where.mockResolvedValueOnce([record]) + + const result = await authenticateApiKeyFromHeader('sk-sim-plain-key', { + userId: 'user-1', + }) + + expect(result).toEqual({ success: false, error: 'Invalid API key' }) + expect(dbChainMockFns.where).toHaveBeenCalledTimes(1) + expect(mockAuthenticateApiKey).not.toHaveBeenCalled() + }) + + it('falls back to the decrypt loop when no row matches the hash, and warns on success', async () => { + const record = personalKeyRecord() + dbChainMockFns.where.mockResolvedValueOnce([]).mockResolvedValueOnce([record]) + mockAuthenticateApiKey.mockResolvedValueOnce(true) + + const result = await authenticateApiKeyFromHeader('sk-sim-plain-key', { + userId: 'user-1', + }) + + expect(result).toEqual({ + success: true, + userId: 'user-1', + keyId: 'key-1', + keyType: 'personal', + workspaceId: undefined, + }) + expect(dbChainMockFns.where).toHaveBeenCalledTimes(2) + expect(mockAuthenticateApiKey).toHaveBeenCalledWith( + 'sk-sim-plain-key', + 'encrypted:stored:value' + ) + expect(warnSpy).toHaveBeenCalledWith('API key matched via fallback decrypt loop', { + keyId: 'key-1', + }) + }) + + it('returns invalid when the hash lookup misses and the fallback scan also misses', async () => { + dbChainMockFns.where.mockResolvedValueOnce([]).mockResolvedValueOnce([]) + + const result = await authenticateApiKeyFromHeader('sk-sim-plain-key', { + userId: 'user-1', + }) + + expect(result).toEqual({ success: false, error: 'Invalid API key' }) + expect(dbChainMockFns.where).toHaveBeenCalledTimes(2) + expect(mockAuthenticateApiKey).not.toHaveBeenCalled() + expect(warnSpy).not.toHaveBeenCalled() + }) + + it('returns invalid when the hash lookup misses and every fallback candidate fails decrypt comparison', async () => { + const record = personalKeyRecord() + dbChainMockFns.where.mockResolvedValueOnce([]).mockResolvedValueOnce([record]) + mockAuthenticateApiKey.mockResolvedValueOnce(false) + + const result = await authenticateApiKeyFromHeader('sk-sim-plain-key', { + userId: 'user-1', + }) + + expect(result).toEqual({ success: false, error: 'Invalid API key' }) + expect(mockAuthenticateApiKey).toHaveBeenCalledTimes(1) + expect(warnSpy).not.toHaveBeenCalled() + }) + + it('queries by the sha256 hash of the incoming header on the fast path', async () => { + dbChainMockFns.where.mockResolvedValueOnce([personalKeyRecord()]) + + await authenticateApiKeyFromHeader('sk-sim-plain-key', { userId: 'user-1' }) + + const [filter] = dbChainMockFns.where.mock.calls[0] + const expected = hashApiKey('sk-sim-plain-key') + expect(JSON.stringify(filter)).toContain(expected) + }) +}) diff --git a/apps/sim/lib/api-key/service.ts b/apps/sim/lib/api-key/service.ts index 84f19c34625..4c0709b9df6 100644 --- a/apps/sim/lib/api-key/service.ts +++ b/apps/sim/lib/api-key/service.ts @@ -3,8 +3,9 @@ import { apiKey as apiKeyTable } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { and, eq } from 'drizzle-orm' import { authenticateApiKey } from '@/lib/api-key/auth' +import { hashApiKey } from '@/lib/api-key/crypto' import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils' -import { getWorkspaceBillingSettings } from '@/lib/workspaces/utils' +import { getWorkspaceBillingSettings, type WorkspaceBillingSettings } from '@/lib/workspaces/utils' const logger = createLogger('ApiKeyService') @@ -39,8 +40,24 @@ export interface ApiKeyAuthResult { error?: string } +const INVALID = { success: false, error: 'Invalid API key' } as const + +interface HashCandidate { + id: string + userId: string + workspaceId: string | null + type: string + expiresAt: Date | null +} + /** - * Authenticate an API key from header with flexible filtering options + * Authenticate an API key from header with flexible filtering options. + * + * Tries the hash lookup first. If that misses (legacy row not yet backfilled, + * or writer missed the hash column), falls back to the original scan+decrypt + * loop. The fallback emits a warn log whenever it actually matches a row so + * we can confirm the fast path is covering 100% of traffic before deleting + * the fallback block below in a follow-up PR. */ export async function authenticateApiKeyFromHeader( apiKeyHeader: string, @@ -51,10 +68,7 @@ export async function authenticateApiKeyFromHeader( } try { - let workspaceSettings: { - billedAccountUserId: string | null - allowPersonalApiKeys: boolean - } | null = null + let workspaceSettings: WorkspaceBillingSettings | null = null if (options.workspaceId) { workspaceSettings = await getWorkspaceBillingSettings(options.workspaceId) @@ -63,7 +77,13 @@ export async function authenticateApiKeyFromHeader( } } - // Build query based on options + const hashResult = await authenticateApiKeyByHash(apiKeyHeader, options, workspaceSettings) + if (hashResult !== null) return hashResult + + // LEGACY FALLBACK — delete once `logger.warn('API key matched via fallback + // decrypt loop', ...)` count stays at zero in prod. The block below is the + // pre-hash-lookup implementation, preserved verbatim as a safety net while + // the `key_hash` backfill rolls out. let query = db .select({ id: apiKeyTable.id, @@ -75,7 +95,6 @@ export async function authenticateApiKeyFromHeader( }) .from(apiKeyTable) - // Apply filters const conditions = [] if (options.userId) { @@ -85,8 +104,6 @@ export async function authenticateApiKeyFromHeader( if (options.keyTypes?.length) { if (options.keyTypes.length === 1) { conditions.push(eq(apiKeyTable.type, options.keyTypes[0])) - } else { - // For multiple types, we'll filter in memory since drizzle's inArray is complex here } } @@ -118,9 +135,7 @@ export async function authenticateApiKeyFromHeader( const permissionCache = new Map() - // Authenticate each key for (const storedKey of filteredRecords) { - // Skip expired keys if (storedKey.expiresAt && storedKey.expiresAt < new Date()) { continue } @@ -151,6 +166,7 @@ export async function authenticateApiKeyFromHeader( try { const isValid = await authenticateApiKey(apiKeyHeader, storedKey.key) if (isValid) { + logger.warn('API key matched via fallback decrypt loop', { keyId: storedKey.id }) return { success: true, userId: storedKey.userId, @@ -164,13 +180,77 @@ export async function authenticateApiKeyFromHeader( } } - return { success: false, error: 'Invalid API key' } + return INVALID } catch (error) { logger.error('API key authentication error:', error) return { success: false, error: 'Authentication failed' } } } +/** + * Fast path: look up a single row by `sha256(apiKeyHeader)` and apply the + * scope / expiry / permission gates. Returns `null` when no row matched the + * hash (caller should fall through to the legacy scan+decrypt loop). A hash + * hit that fails a gate returns a concrete `INVALID` — the key definitely + * belongs to that row, it's just not authorized in this scope. + */ +async function authenticateApiKeyByHash( + apiKeyHeader: string, + options: ApiKeyAuthOptions, + workspaceSettings: WorkspaceBillingSettings | null +): Promise { + const keyHash = hashApiKey(apiKeyHeader) + const rows: HashCandidate[] = await db + .select({ + id: apiKeyTable.id, + userId: apiKeyTable.userId, + workspaceId: apiKeyTable.workspaceId, + type: apiKeyTable.type, + expiresAt: apiKeyTable.expiresAt, + }) + .from(apiKeyTable) + .where(eq(apiKeyTable.keyHash, keyHash)) + + if (rows.length === 0) return null + + const record = rows[0] + const keyType = record.type as 'personal' | 'workspace' + + if (options.userId && record.userId !== options.userId) return INVALID + if (options.keyTypes?.length && !options.keyTypes.includes(keyType)) return INVALID + if (record.expiresAt && record.expiresAt < new Date()) return INVALID + + if ( + options.workspaceId && + keyType === 'workspace' && + record.workspaceId !== options.workspaceId + ) { + return INVALID + } + + if (options.workspaceId && keyType === 'personal') { + if (!workspaceSettings?.allowPersonalApiKeys) return INVALID + if (!record.userId) return INVALID + + const permission = await getUserEntityPermissions( + record.userId, + 'workspace', + options.workspaceId + ) + if (permission === null) return INVALID + } + + logger.debug('API key matched via hash lookup', { keyId: record.id, keyType }) + + return { + success: true, + userId: record.userId, + keyId: record.id, + keyType, + workspaceId: record.workspaceId || options.workspaceId || undefined, + } +} + /** * Update the last used timestamp for an API key */ diff --git a/apps/sim/lib/workspaces/utils.ts b/apps/sim/lib/workspaces/utils.ts index ff84484a7d0..f1ddf97c444 100644 --- a/apps/sim/lib/workspaces/utils.ts +++ b/apps/sim/lib/workspaces/utils.ts @@ -5,7 +5,7 @@ import { and, desc, eq, isNull, ne, sql } from 'drizzle-orm' const logger = createLogger('WorkspaceUtils') -interface WorkspaceBillingSettings { +export interface WorkspaceBillingSettings { billedAccountUserId: string | null allowPersonalApiKeys: boolean } diff --git a/packages/db/migrations/0195_normal_white_queen.sql b/packages/db/migrations/0195_normal_white_queen.sql new file mode 100644 index 00000000000..52583158a3e --- /dev/null +++ b/packages/db/migrations/0195_normal_white_queen.sql @@ -0,0 +1,2 @@ +ALTER TABLE "api_key" ADD COLUMN "key_hash" text;--> statement-breakpoint +CREATE UNIQUE INDEX "api_key_key_hash_idx" ON "api_key" USING btree ("key_hash"); \ No newline at end of file diff --git a/packages/db/migrations/meta/0195_snapshot.json b/packages/db/migrations/meta/0195_snapshot.json new file mode 100644 index 00000000000..768a1d56674 --- /dev/null +++ b/packages/db/migrations/meta/0195_snapshot.json @@ -0,0 +1,15231 @@ +{ + "id": "5723e663-7612-43e8-92e5-e60781645d03", + "prevId": "dce19071-748a-432a-86f3-6dd4021df35f", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.a2a_agent": { + "name": "a2a_agent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'1.0.0'" + }, + "capabilities": { + "name": "capabilities", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "skills": { + "name": "skills", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "authentication": { + "name": "authentication", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "signatures": { + "name": "signatures", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "is_published": { + "name": "is_published", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "published_at": { + "name": "published_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "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": { + "a2a_agent_workflow_id_idx": { + "name": "a2a_agent_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_created_by_idx": { + "name": "a2a_agent_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_workspace_workflow_unique": { + "name": "a2a_agent_workspace_workflow_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"a2a_agent\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_archived_at_idx": { + "name": "a2a_agent_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_agent_workspace_archived_partial_idx": { + "name": "a2a_agent_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": "\"a2a_agent\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "a2a_agent_workspace_id_workspace_id_fk": { + "name": "a2a_agent_workspace_id_workspace_id_fk", + "tableFrom": "a2a_agent", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "a2a_agent_workflow_id_workflow_id_fk": { + "name": "a2a_agent_workflow_id_workflow_id_fk", + "tableFrom": "a2a_agent", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "a2a_agent_created_by_user_id_fk": { + "name": "a2a_agent_created_by_user_id_fk", + "tableFrom": "a2a_agent", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.a2a_push_notification_config": { + "name": "a2a_push_notification_config", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_schemes": { + "name": "auth_schemes", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "auth_credentials": { + "name": "auth_credentials", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "a2a_push_notification_config_task_unique": { + "name": "a2a_push_notification_config_task_unique", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "a2a_push_notification_config_task_id_a2a_task_id_fk": { + "name": "a2a_push_notification_config_task_id_a2a_task_id_fk", + "tableFrom": "a2a_push_notification_config", + "tableTo": "a2a_task", + "columnsFrom": ["task_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.a2a_task": { + "name": "a2a_task", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "a2a_task_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'submitted'" + }, + "messages": { + "name": "messages", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "artifacts": { + "name": "artifacts", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "a2a_task_agent_id_idx": { + "name": "a2a_task_agent_id_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_session_id_idx": { + "name": "a2a_task_session_id_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_status_idx": { + "name": "a2a_task_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_execution_id_idx": { + "name": "a2a_task_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "a2a_task_created_at_idx": { + "name": "a2a_task_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "a2a_task_agent_id_a2a_agent_id_fk": { + "name": "a2a_task_agent_id_a2a_agent_id_fk", + "tableFrom": "a2a_task", + "tableTo": "a2a_agent", + "columnsFrom": ["agent_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.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": {} + } + }, + "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_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.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": "'[]'" + }, + "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": {} + } + }, + "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 + }, + "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 + }, + "messages": { + "name": "messages", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'claude-3-7-sonnet-latest'" + }, + "conversation_id": { + "name": "conversation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "preview_yaml": { + "name": "preview_yaml", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "plan_artifact": { + "name": "plan_artifact", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "resources": { + "name": "resources", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "last_seen_at": { + "name": "last_seen_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": {} + } + }, + "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_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_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 + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_workspace_id_idx": { + "name": "credential_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_type_idx": { + "name": "credential_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_provider_id_idx": { + "name": "credential_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_account_id_idx": { + "name": "credential_account_id_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_env_owner_user_id_idx": { + "name": "credential_env_owner_user_id_idx", + "columns": [ + { + "expression": "env_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_account_unique": { + "name": "credential_workspace_account_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "account_id IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_env_unique": { + "name": "credential_workspace_env_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "type = 'env_workspace'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_workspace_personal_env_unique": { + "name": "credential_workspace_personal_env_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "env_owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "type = 'env_personal'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_workspace_id_workspace_id_fk": { + "name": "credential_workspace_id_workspace_id_fk", + "tableFrom": "credential", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_account_id_account_id_fk": { + "name": "credential_account_id_account_id_fk", + "tableFrom": "credential", + "tableTo": "account", + "columnsFrom": ["account_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_env_owner_user_id_user_id_fk": { + "name": "credential_env_owner_user_id_user_id_fk", + "tableFrom": "credential", + "tableTo": "user", + "columnsFrom": ["env_owner_user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_created_by_user_id_fk": { + "name": "credential_created_by_user_id_fk", + "tableFrom": "credential", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "credential_oauth_source_check": { + "name": "credential_oauth_source_check", + "value": "(type <> 'oauth') OR (account_id IS NOT NULL AND provider_id IS NOT NULL)" + }, + "credential_workspace_env_source_check": { + "name": "credential_workspace_env_source_check", + "value": "(type <> 'env_workspace') OR (env_key IS NOT NULL AND env_owner_user_id IS NULL)" + }, + "credential_personal_env_source_check": { + "name": "credential_personal_env_source_check", + "value": "(type <> 'env_personal') OR (env_key IS NOT NULL AND env_owner_user_id IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.credential_member": { + "name": "credential_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "credential_member_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "status": { + "name": "status", + "type": "credential_member_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_member_user_id_idx": { + "name": "credential_member_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_role_idx": { + "name": "credential_member_role_idx", + "columns": [ + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_status_idx": { + "name": "credential_member_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_member_unique": { + "name": "credential_member_unique", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_member_credential_id_credential_id_fk": { + "name": "credential_member_credential_id_credential_id_fk", + "tableFrom": "credential_member", + "tableTo": "credential", + "columnsFrom": ["credential_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_member_user_id_user_id_fk": { + "name": "credential_member_user_id_user_id_fk", + "tableFrom": "credential_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_member_invited_by_user_id_fk": { + "name": "credential_member_invited_by_user_id_fk", + "tableFrom": "credential_member", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_set": { + "name": "credential_set", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_set_created_by_idx": { + "name": "credential_set_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_org_name_unique": { + "name": "credential_set_org_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_provider_id_idx": { + "name": "credential_set_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_set_organization_id_organization_id_fk": { + "name": "credential_set_organization_id_organization_id_fk", + "tableFrom": "credential_set", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_created_by_user_id_fk": { + "name": "credential_set_created_by_user_id_fk", + "tableFrom": "credential_set", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_set_invitation": { + "name": "credential_set_invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_set_id": { + "name": "credential_set_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "credential_set_invitation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "accepted_by_user_id": { + "name": "accepted_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_set_invitation_set_id_idx": { + "name": "credential_set_invitation_set_id_idx", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_invitation_token_idx": { + "name": "credential_set_invitation_token_idx", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_invitation_status_idx": { + "name": "credential_set_invitation_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_invitation_expires_at_idx": { + "name": "credential_set_invitation_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_set_invitation_credential_set_id_credential_set_id_fk": { + "name": "credential_set_invitation_credential_set_id_credential_set_id_fk", + "tableFrom": "credential_set_invitation", + "tableTo": "credential_set", + "columnsFrom": ["credential_set_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_invitation_invited_by_user_id_fk": { + "name": "credential_set_invitation_invited_by_user_id_fk", + "tableFrom": "credential_set_invitation", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_invitation_accepted_by_user_id_user_id_fk": { + "name": "credential_set_invitation_accepted_by_user_id_user_id_fk", + "tableFrom": "credential_set_invitation", + "tableTo": "user", + "columnsFrom": ["accepted_by_user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "credential_set_invitation_token_unique": { + "name": "credential_set_invitation_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_set_member": { + "name": "credential_set_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_set_id": { + "name": "credential_set_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "credential_set_member_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_set_member_user_id_idx": { + "name": "credential_set_member_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_member_unique": { + "name": "credential_set_member_unique", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_member_status_idx": { + "name": "credential_set_member_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_set_member_credential_set_id_credential_set_id_fk": { + "name": "credential_set_member_credential_set_id_credential_set_id_fk", + "tableFrom": "credential_set_member", + "tableTo": "credential_set", + "columnsFrom": ["credential_set_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_member_user_id_user_id_fk": { + "name": "credential_set_member_user_id_user_id_fk", + "tableFrom": "credential_set_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_member_invited_by_user_id_fk": { + "name": "credential_set_member_invited_by_user_id_fk", + "tableFrom": "credential_set_member", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_tools": { + "name": "custom_tools", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema": { + "name": "schema", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "custom_tools_workspace_id_idx": { + "name": "custom_tools_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_tools_workspace_title_unique": { + "name": "custom_tools_workspace_title_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "title", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "custom_tools_workspace_id_workspace_id_fk": { + "name": "custom_tools_workspace_id_workspace_id_fk", + "tableFrom": "custom_tools", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "custom_tools_user_id_user_id_fk": { + "name": "custom_tools_user_id_user_id_fk", + "tableFrom": "custom_tools", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.docs_embeddings": { + "name": "docs_embeddings", + "schema": "", + "columns": { + "chunk_id": { + "name": "chunk_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chunk_text": { + "name": "chunk_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_document": { + "name": "source_document", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_link": { + "name": "source_link", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_text": { + "name": "header_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_level": { + "name": "header_level", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": true + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "chunk_text_tsv": { + "name": "chunk_text_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"docs_embeddings\".\"chunk_text\")", + "type": "stored" + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "docs_emb_source_document_idx": { + "name": "docs_emb_source_document_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_header_level_idx": { + "name": "docs_emb_header_level_idx", + "columns": [ + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_source_header_idx": { + "name": "docs_emb_source_header_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_model_idx": { + "name": "docs_emb_model_idx", + "columns": [ + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_created_at_idx": { + "name": "docs_emb_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_embedding_vector_hnsw_idx": { + "name": "docs_embedding_vector_hnsw_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "docs_emb_metadata_gin_idx": { + "name": "docs_emb_metadata_gin_idx", + "columns": [ + { + "expression": "metadata", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "docs_emb_chunk_text_fts_idx": { + "name": "docs_emb_chunk_text_fts_idx", + "columns": [ + { + "expression": "chunk_text_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "docs_embedding_not_null_check": { + "name": "docs_embedding_not_null_check", + "value": "\"embedding\" IS NOT NULL" + }, + "docs_header_level_check": { + "name": "docs_header_level_check", + "value": "\"header_level\" >= 1 AND \"header_level\" <= 6" + } + }, + "isRLSEnabled": false + }, + "public.document": { + "name": "document", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_url": { + "name": "file_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_size": { + "name": "file_size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_count": { + "name": "chunk_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "character_count": { + "name": "character_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "processing_status": { + "name": "processing_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_completed_at": { + "name": "processing_completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_error": { + "name": "processing_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "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 + }, + "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_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" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.embedding": { + "name": "embedding", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_index": { + "name": "chunk_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "chunk_hash": { + "name": "chunk_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_length": { + "name": "content_length", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "start_offset": { + "name": "start_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "end_offset": { + "name": "end_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number1": { + "name": "number1", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number2": { + "name": "number2", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number3": { + "name": "number3", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number4": { + "name": "number4", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number5": { + "name": "number5", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "date1": { + "name": "date1", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "date2": { + "name": "date2", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "boolean1": { + "name": "boolean1", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean2": { + "name": "boolean2", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean3": { + "name": "boolean3", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "content_tsv": { + "name": "content_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"embedding\".\"content\")", + "type": "stored" + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "emb_kb_id_idx": { + "name": "emb_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_id_idx": { + "name": "emb_doc_id_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_chunk_idx": { + "name": "emb_doc_chunk_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chunk_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_model_idx": { + "name": "emb_kb_model_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_enabled_idx": { + "name": "emb_kb_enabled_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_enabled_idx": { + "name": "emb_doc_enabled_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "embedding_vector_hnsw_idx": { + "name": "embedding_vector_hnsw_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "emb_tag1_idx": { + "name": "emb_tag1_idx", + "columns": [ + { + "expression": "tag1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag2_idx": { + "name": "emb_tag2_idx", + "columns": [ + { + "expression": "tag2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag3_idx": { + "name": "emb_tag3_idx", + "columns": [ + { + "expression": "tag3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag4_idx": { + "name": "emb_tag4_idx", + "columns": [ + { + "expression": "tag4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag5_idx": { + "name": "emb_tag5_idx", + "columns": [ + { + "expression": "tag5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag6_idx": { + "name": "emb_tag6_idx", + "columns": [ + { + "expression": "tag6", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag7_idx": { + "name": "emb_tag7_idx", + "columns": [ + { + "expression": "tag7", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number1_idx": { + "name": "emb_number1_idx", + "columns": [ + { + "expression": "number1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number2_idx": { + "name": "emb_number2_idx", + "columns": [ + { + "expression": "number2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number3_idx": { + "name": "emb_number3_idx", + "columns": [ + { + "expression": "number3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number4_idx": { + "name": "emb_number4_idx", + "columns": [ + { + "expression": "number4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number5_idx": { + "name": "emb_number5_idx", + "columns": [ + { + "expression": "number5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_date1_idx": { + "name": "emb_date1_idx", + "columns": [ + { + "expression": "date1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_date2_idx": { + "name": "emb_date2_idx", + "columns": [ + { + "expression": "date2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean1_idx": { + "name": "emb_boolean1_idx", + "columns": [ + { + "expression": "boolean1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean2_idx": { + "name": "emb_boolean2_idx", + "columns": [ + { + "expression": "boolean2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean3_idx": { + "name": "emb_boolean3_idx", + "columns": [ + { + "expression": "boolean3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_content_fts_idx": { + "name": "emb_content_fts_idx", + "columns": [ + { + "expression": "content_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "embedding_knowledge_base_id_knowledge_base_id_fk": { + "name": "embedding_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "embedding", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "embedding_document_id_document_id_fk": { + "name": "embedding_document_id_document_id_fk", + "tableFrom": "embedding", + "tableTo": "document", + "columnsFrom": ["document_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "embedding_not_null_check": { + "name": "embedding_not_null_check", + "value": "\"embedding\" IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.environment": { + "name": "environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "environment_user_id_user_id_fk": { + "name": "environment_user_id_user_id_fk", + "tableFrom": "environment", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "environment_user_id_unique": { + "name": "environment_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.form": { + "name": "form", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "customizations": { + "name": "customizations", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_emails": { + "name": "allowed_emails", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "show_branding": { + "name": "show_branding", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "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": { + "form_identifier_idx": { + "name": "form_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"form\".\"archived_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "form_workflow_id_idx": { + "name": "form_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "form_user_id_idx": { + "name": "form_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "form_archived_at_partial_idx": { + "name": "form_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"form\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "form_workflow_id_workflow_id_fk": { + "name": "form_workflow_id_workflow_id_fk", + "tableFrom": "form", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "form_user_id_user_id_fk": { + "name": "form_user_id_user_id_fk", + "tableFrom": "form", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.idempotency_key": { + "name": "idempotency_key", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "result": { + "name": "result", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idempotency_key_created_at_idx": { + "name": "idempotency_key_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "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 + }, + "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_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.jwks": { + "name": "jwks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base": { + "name": "knowledge_base", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "embedding_dimension": { + "name": "embedding_dimension", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1536 + }, + "chunking_config": { + "name": "chunking_config", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{\"maxSize\": 1024, \"minSize\": 1, \"overlap\": 200}'" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kb_user_id_idx": { + "name": "kb_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_id_idx": { + "name": "kb_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_user_workspace_idx": { + "name": "kb_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_deleted_at_idx": { + "name": "kb_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "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": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base_tag_definitions": { + "name": "knowledge_base_tag_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag_slot": { + "name": "tag_slot", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "field_type": { + "name": "field_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kb_tag_definitions_kb_slot_idx": { + "name": "kb_tag_definitions_kb_slot_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tag_slot", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_display_name_idx": { + "name": "kb_tag_definitions_kb_display_name_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "display_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_id_idx": { + "name": "kb_tag_definitions_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk": { + "name": "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "knowledge_base_tag_definitions", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_connector": { + "name": "knowledge_connector", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connector_type": { + "name": "connector_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_config": { + "name": "source_config", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "sync_mode": { + "name": "sync_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'full'" + }, + "sync_interval_minutes": { + "name": "sync_interval_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1440 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_sync_at": { + "name": "last_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_sync_error": { + "name": "last_sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_sync_doc_count": { + "name": "last_sync_doc_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "next_sync_at": { + "name": "next_sync_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "consecutive_failures": { + "name": "consecutive_failures", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "kc_knowledge_base_id_idx": { + "name": "kc_knowledge_base_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_status_next_sync_idx": { + "name": "kc_status_next_sync_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_sync_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_archived_at_partial_idx": { + "name": "kc_archived_at_partial_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "kc_deleted_at_partial_idx": { + "name": "kc_deleted_at_partial_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"knowledge_connector\".\"deleted_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_connector_knowledge_base_id_knowledge_base_id_fk": { + "name": "knowledge_connector_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "knowledge_connector", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_connector_sync_log": { + "name": "knowledge_connector_sync_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connector_id": { + "name": "connector_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "docs_added": { + "name": "docs_added", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_updated": { + "name": "docs_updated", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_deleted": { + "name": "docs_deleted", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_unchanged": { + "name": "docs_unchanged", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "docs_failed": { + "name": "docs_failed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "kcsl_connector_id_idx": { + "name": "kcsl_connector_id_idx", + "columns": [ + { + "expression": "connector_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_connector_sync_log_connector_id_knowledge_connector_id_fk": { + "name": "knowledge_connector_sync_log_connector_id_knowledge_connector_id_fk", + "tableFrom": "knowledge_connector_sync_log", + "tableTo": "knowledge_connector", + "columnsFrom": ["connector_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_servers": { + "name": "mcp_servers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "transport": { + "name": "transport", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "headers": { + "name": "headers", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "timeout": { + "name": "timeout", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 30000 + }, + "retries": { + "name": "retries", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3 + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "last_connected": { + "name": "last_connected", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "connection_status": { + "name": "connection_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'disconnected'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_config": { + "name": "status_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "tool_count": { + "name": "tool_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_tools_refresh": { + "name": "last_tools_refresh", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_requests": { + "name": "total_requests", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mcp_servers_workspace_enabled_idx": { + "name": "mcp_servers_workspace_enabled_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_servers_workspace_deleted_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 + }, + "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.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.oauth_access_token": { + "name": "oauth_access_token", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "oauth_access_token_access_token_idx": { + "name": "oauth_access_token_access_token_idx", + "columns": [ + { + "expression": "access_token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "oauth_access_token_refresh_token_idx": { + "name": "oauth_access_token_refresh_token_idx", + "columns": [ + { + "expression": "refresh_token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_access_token_client_id_oauth_application_client_id_fk": { + "name": "oauth_access_token_client_id_oauth_application_client_id_fk", + "tableFrom": "oauth_access_token", + "tableTo": "oauth_application", + "columnsFrom": ["client_id"], + "columnsTo": ["client_id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_access_token_user_id_user_id_fk": { + "name": "oauth_access_token_user_id_user_id_fk", + "tableFrom": "oauth_access_token", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_access_token_access_token_unique": { + "name": "oauth_access_token_access_token_unique", + "nullsNotDistinct": false, + "columns": ["access_token"] + }, + "oauth_access_token_refresh_token_unique": { + "name": "oauth_access_token_refresh_token_unique", + "nullsNotDistinct": false, + "columns": ["refresh_token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_application": { + "name": "oauth_application", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redirect_urls": { + "name": "redirect_urls", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "disabled": { + "name": "disabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "user_id": { + "name": "user_id", + "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": { + "oauth_application_client_id_idx": { + "name": "oauth_application_client_id_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_application_user_id_user_id_fk": { + "name": "oauth_application_user_id_user_id_fk", + "tableFrom": "oauth_application", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_application_client_id_unique": { + "name": "oauth_application_client_id_unique", + "nullsNotDistinct": false, + "columns": ["client_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_consent": { + "name": "oauth_consent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "consent_given": { + "name": "consent_given", + "type": "boolean", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "oauth_consent_user_client_idx": { + "name": "oauth_consent_user_client_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_consent_client_id_oauth_application_client_id_fk": { + "name": "oauth_consent_client_id_oauth_application_client_id_fk", + "tableFrom": "oauth_consent", + "tableTo": "oauth_application", + "columnsFrom": ["client_id"], + "columnsTo": ["client_id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_consent_user_id_user_id_fk": { + "name": "oauth_consent_user_id_user_id_fk", + "tableFrom": "oauth_consent", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "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 + }, + "whitelabel_settings": { + "name": "whitelabel_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 + }, + "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.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": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.paused_executions": { + "name": "paused_executions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_snapshot": { + "name": "execution_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "pause_points": { + "name": "pause_points", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "total_pause_count": { + "name": "total_pause_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "resumed_count": { + "name": "resumed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'paused'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "paused_at": { + "name": "paused_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "paused_executions_workflow_id_idx": { + "name": "paused_executions_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_status_idx": { + "name": "paused_executions_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_execution_id_unique": { + "name": "paused_executions_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "paused_executions_workflow_id_workflow_id_fk": { + "name": "paused_executions_workflow_id_workflow_id_fk", + "tableFrom": "paused_executions", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.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 + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "auto_add_new_members": { + "name": "auto_add_new_members", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "permission_group_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_workspace_name_unique": { + "name": "permission_group_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": {} + }, + "permission_group_workspace_auto_add_unique": { + "name": "permission_group_workspace_auto_add_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "auto_add_new_members = true", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_workspace_id_workspace_id_fk": { + "name": "permission_group_workspace_id_workspace_id_fk", + "tableFrom": "permission_group", + "tableTo": "workspace", + "columnsFrom": ["workspace_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 + }, + "workspace_id": { + "name": "workspace_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_workspace_user_unique": { + "name": "permission_group_member_workspace_user_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_member_permission_group_id_permission_group_id_fk": { + "name": "permission_group_member_permission_group_id_permission_group_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "permission_group", + "columnsFrom": ["permission_group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_workspace_id_workspace_id_fk": { + "name": "permission_group_member_workspace_id_workspace_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_user_id_user_id_fk": { + "name": "permission_group_member_user_id_user_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_assigned_by_user_id_fk": { + "name": "permission_group_member_assigned_by_user_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "user", + "columnsFrom": ["assigned_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permissions": { + "name": "permissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission_type": { + "name": "permission_type", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permissions_user_id_idx": { + "name": "permissions_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_entity_idx": { + "name": "permissions_entity_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_type_idx": { + "name": "permissions_user_entity_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_permission_idx": { + "name": "permissions_user_entity_permission_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_idx": { + "name": "permissions_user_entity_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_unique_constraint": { + "name": "permissions_unique_constraint", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permissions_user_id_user_id_fk": { + "name": "permissions_user_id_user_id_fk", + "tableFrom": "permissions", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rate_limit_bucket": { + "name": "rate_limit_bucket", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tokens": { + "name": "tokens", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resume_queue": { + "name": "resume_queue", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "paused_execution_id": { + "name": "paused_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_execution_id": { + "name": "parent_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "new_execution_id": { + "name": "new_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "context_id": { + "name": "context_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resume_input": { + "name": "resume_input", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "queued_at": { + "name": "queued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "resume_queue_parent_status_idx": { + "name": "resume_queue_parent_status_idx", + "columns": [ + { + "expression": "parent_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "resume_queue_new_execution_idx": { + "name": "resume_queue_new_execution_idx", + "columns": [ + { + "expression": "new_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "resume_queue_paused_execution_id_paused_executions_id_fk": { + "name": "resume_queue_paused_execution_id_paused_executions_id_fk", + "tableFrom": "resume_queue", + "tableTo": "paused_executions", + "columnsFrom": ["paused_execution_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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 + }, + "error_notifications_enabled": { + "name": "error_notifications_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "snap_to_grid_size": { + "name": "snap_to_grid_size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "show_action_bar": { + "name": "show_action_bar", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "copilot_enabled_models": { + "name": "copilot_enabled_models", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "copilot_auto_allowed_tools": { + "name": "copilot_auto_allowed_tools", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "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.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.sso_provider": { + "name": "sso_provider", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oidc_config": { + "name": "oidc_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "saml_config": { + "name": "saml_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "sso_provider_provider_id_idx": { + "name": "sso_provider_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_domain_idx": { + "name": "sso_provider_domain_idx", + "columns": [ + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_user_id_idx": { + "name": "sso_provider_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_organization_id_idx": { + "name": "sso_provider_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sso_provider_user_id_user_id_fk": { + "name": "sso_provider_user_id_user_id_fk", + "tableFrom": "sso_provider", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sso_provider_organization_id_organization_id_fk": { + "name": "sso_provider_organization_id_organization_id_fk", + "tableFrom": "sso_provider", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.subscription": { + "name": "subscription", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "period_start": { + "name": "period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "period_end": { + "name": "period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancel_at_period_end": { + "name": "cancel_at_period_end", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "seats": { + "name": "seats", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "trial_start": { + "name": "trial_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trial_end": { + "name": "trial_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "subscription_reference_status_idx": { + "name": "subscription_reference_status_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "check_enterprise_metadata": { + "name": "check_enterprise_metadata", + "value": "plan != 'enterprise' OR metadata IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.template_creators": { + "name": "template_creators", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "reference_type": { + "name": "reference_type", + "type": "template_creator_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "profile_image_url": { + "name": "profile_image_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "verified": { + "name": "verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "template_creators_reference_idx": { + "name": "template_creators_reference_idx", + "columns": [ + { + "expression": "reference_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_creators_reference_id_idx": { + "name": "template_creators_reference_id_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_creators_created_by_idx": { + "name": "template_creators_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "template_creators_created_by_user_id_fk": { + "name": "template_creators_created_by_user_id_fk", + "tableFrom": "template_creators", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.template_stars": { + "name": "template_stars", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "template_id": { + "name": "template_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "starred_at": { + "name": "starred_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "template_stars_user_id_idx": { + "name": "template_stars_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_template_id_idx": { + "name": "template_stars_template_id_idx", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_user_template_idx": { + "name": "template_stars_user_template_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_template_user_idx": { + "name": "template_stars_template_user_idx", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_starred_at_idx": { + "name": "template_stars_starred_at_idx", + "columns": [ + { + "expression": "starred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_template_starred_at_idx": { + "name": "template_stars_template_starred_at_idx", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "starred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_user_template_unique": { + "name": "template_stars_user_template_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "template_stars_user_id_user_id_fk": { + "name": "template_stars_user_id_user_id_fk", + "tableFrom": "template_stars", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "template_stars_template_id_templates_id_fk": { + "name": "template_stars_template_id_templates_id_fk", + "tableFrom": "template_stars", + "tableTo": "templates", + "columnsFrom": ["template_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.templates": { + "name": "templates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "creator_id": { + "name": "creator_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "views": { + "name": "views", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "stars": { + "name": "stars", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "template_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "tags": { + "name": "tags", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "required_credentials": { + "name": "required_credentials", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "state": { + "name": "state", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "og_image_url": { + "name": "og_image_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "templates_status_idx": { + "name": "templates_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_creator_id_idx": { + "name": "templates_creator_id_idx", + "columns": [ + { + "expression": "creator_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_views_idx": { + "name": "templates_views_idx", + "columns": [ + { + "expression": "views", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_stars_idx": { + "name": "templates_stars_idx", + "columns": [ + { + "expression": "stars", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_status_views_idx": { + "name": "templates_status_views_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "views", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_status_stars_idx": { + "name": "templates_status_stars_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stars", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_created_at_idx": { + "name": "templates_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_updated_at_idx": { + "name": "templates_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "templates_workflow_id_workflow_id_fk": { + "name": "templates_workflow_id_workflow_id_fk", + "tableFrom": "templates", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "templates_creator_id_template_creators_id_fk": { + "name": "templates_creator_id_template_creators_id_fk", + "tableFrom": "templates", + "tableTo": "template_creators", + "columnsFrom": ["creator_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.usage_log": { + "name": "usage_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "usage_log_category", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "usage_log_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "cost": { + "name": "cost", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "usage_log_user_created_at_idx": { + "name": "usage_log_user_created_at_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_source_idx": { + "name": "usage_log_source_idx", + "columns": [ + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workspace_id_idx": { + "name": "usage_log_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workflow_id_idx": { + "name": "usage_log_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "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": {} + } + }, + "foreignKeys": { + "usage_log_user_id_user_id_fk": { + "name": "usage_log_user_id_user_id_fk", + "tableFrom": "usage_log", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "usage_log_workspace_id_workspace_id_fk": { + "name": "usage_log_workspace_id_workspace_id_fk", + "tableFrom": "usage_log", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "usage_log_workflow_id_workflow_id_fk": { + "name": "usage_log_workflow_id_workflow_id_fk", + "tableFrom": "usage_log", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "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_a2a_executions": { + "name": "total_a2a_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 + } + }, + "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 + }, + "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 + }, + "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_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_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_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 + }, + "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_table_id_idx": { + "name": "user_table_rows_table_id_idx", + "columns": [ + { + "expression": "table_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_table_rows_data_gin_idx": { + "name": "user_table_rows_data_gin_idx", + "columns": [ + { + "expression": "data", + "isExpression": false, + "asc": 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": {} + } + }, + "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 + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_config": { + "name": "provider_config", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_failed_at": { + "name": "last_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "credential_set_id": { + "name": "credential_set_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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": {} + }, + "idx_webhook_on_workflow_id_block_id": { + "name": "idx_webhook_on_workflow_id_block_id", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_workflow_deployment_idx": { + "name": "webhook_workflow_deployment_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_credential_set_id_idx": { + "name": "webhook_credential_set_id_idx", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "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": {} + } + }, + "foreignKeys": { + "webhook_workflow_id_workflow_id_fk": { + "name": "webhook_workflow_id_workflow_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webhook_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "webhook_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webhook_credential_set_id_credential_set_id_fk": { + "name": "webhook_credential_set_id_credential_set_id_fk", + "tableFrom": "webhook", + "tableTo": "credential_set", + "columnsFrom": ["credential_set_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow": { + "name": "workflow", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#3972F6'" + }, + "last_synced": { + "name": "last_synced", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "is_deployed": { + "name": "is_deployed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deployed_at": { + "name": "deployed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "is_public_api": { + "name": "is_public_api", + "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_workflow_folder_id_fk": { + "name": "workflow_folder_id_workflow_folder_id_fk", + "tableFrom": "workflow", + "tableTo": "workflow_folder", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_blocks": { + "name": "workflow_blocks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position_x": { + "name": "position_x", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "position_y": { + "name": "position_y", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "horizontal_handles": { + "name": "horizontal_handles", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "is_wide": { + "name": "is_wide", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "advanced_mode": { + "name": "advanced_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "trigger_mode": { + "name": "trigger_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "locked": { + "name": "locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "height": { + "name": "height", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "sub_blocks": { + "name": "sub_blocks", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "outputs": { + "name": "outputs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_blocks_workflow_id_idx": { + "name": "workflow_blocks_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_blocks_type_idx": { + "name": "workflow_blocks_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_blocks_workflow_id_workflow_id_fk": { + "name": "workflow_blocks_workflow_id_workflow_id_fk", + "tableFrom": "workflow_blocks", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_checkpoints": { + "name": "workflow_checkpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_state": { + "name": "workflow_state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_checkpoints_user_id_idx": { + "name": "workflow_checkpoints_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_workflow_id_idx": { + "name": "workflow_checkpoints_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_chat_id_idx": { + "name": "workflow_checkpoints_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_message_id_idx": { + "name": "workflow_checkpoints_message_id_idx", + "columns": [ + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_user_workflow_idx": { + "name": "workflow_checkpoints_user_workflow_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_workflow_chat_idx": { + "name": "workflow_checkpoints_workflow_chat_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_created_at_idx": { + "name": "workflow_checkpoints_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_chat_created_at_idx": { + "name": "workflow_checkpoints_chat_created_at_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_checkpoints_user_id_user_id_fk": { + "name": "workflow_checkpoints_user_id_user_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_checkpoints_workflow_id_workflow_id_fk": { + "name": "workflow_checkpoints_workflow_id_workflow_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_checkpoints_chat_id_copilot_chats_id_fk": { + "name": "workflow_checkpoints_chat_id_copilot_chats_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_deployment_version": { + "name": "workflow_deployment_version", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_deployment_version_workflow_version_unique": { + "name": "workflow_deployment_version_workflow_version_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_version_workflow_active_idx": { + "name": "workflow_deployment_version_workflow_active_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_version_created_at_idx": { + "name": "workflow_deployment_version_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_deployment_version_workflow_id_workflow_id_fk": { + "name": "workflow_deployment_version_workflow_id_workflow_id_fk", + "tableFrom": "workflow_deployment_version", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_edges": { + "name": "workflow_edges", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_block_id": { + "name": "source_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_block_id": { + "name": "target_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_handle": { + "name": "source_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_handle": { + "name": "target_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_edges_workflow_id_idx": { + "name": "workflow_edges_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_workflow_source_idx": { + "name": "workflow_edges_workflow_source_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_workflow_target_idx": { + "name": "workflow_edges_workflow_target_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_edges_workflow_id_workflow_id_fk": { + "name": "workflow_edges_workflow_id_workflow_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_edges_source_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_source_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": ["source_block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_edges_target_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_target_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": ["target_block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_logs": { + "name": "workflow_execution_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_snapshot_id": { + "name": "state_snapshot_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_duration_ms": { + "name": "total_duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "execution_data": { + "name": "execution_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "cost": { + "name": "cost", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "files": { + "name": "files", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_execution_logs_workflow_id_idx": { + "name": "workflow_execution_logs_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_state_snapshot_id_idx": { + "name": "workflow_execution_logs_state_snapshot_id_idx", + "columns": [ + { + "expression": "state_snapshot_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_deployment_version_id_idx": { + "name": "workflow_execution_logs_deployment_version_id_idx", + "columns": [ + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_trigger_idx": { + "name": "workflow_execution_logs_trigger_idx", + "columns": [ + { + "expression": "trigger", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_level_idx": { + "name": "workflow_execution_logs_level_idx", + "columns": [ + { + "expression": "level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_started_at_idx": { + "name": "workflow_execution_logs_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_execution_id_unique": { + "name": "workflow_execution_logs_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workflow_started_at_idx": { + "name": "workflow_execution_logs_workflow_started_at_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_started_at_idx": { + "name": "workflow_execution_logs_workspace_started_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "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": {} + } + }, + "foreignKeys": { + "workflow_execution_logs_workflow_id_workflow_id_fk": { + "name": "workflow_execution_logs_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workflow_execution_logs_workspace_id_workspace_id_fk": { + "name": "workflow_execution_logs_workspace_id_workspace_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk": { + "name": "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow_execution_snapshots", + "columnsFrom": ["state_snapshot_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_snapshots": { + "name": "workflow_execution_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state_hash": { + "name": "state_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_data": { + "name": "state_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_snapshots_workflow_id_idx": { + "name": "workflow_snapshots_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_hash_idx": { + "name": "workflow_snapshots_hash_idx", + "columns": [ + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_workflow_hash_idx": { + "name": "workflow_snapshots_workflow_hash_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_created_at_idx": { + "name": "workflow_snapshots_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_execution_snapshots_workflow_id_workflow_id_fk": { + "name": "workflow_execution_snapshots_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_snapshots", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_folder": { + "name": "workflow_folder", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'#6B7280'" + }, + "is_expanded": { + "name": "is_expanded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_folder_user_idx": { + "name": "workflow_folder_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_workspace_parent_idx": { + "name": "workflow_folder_workspace_parent_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_parent_sort_idx": { + "name": "workflow_folder_parent_sort_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_archived_at_idx": { + "name": "workflow_folder_archived_at_idx", + "columns": [ + { + "expression": "archived_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_workspace_archived_partial_idx": { + "name": "workflow_folder_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_folder\".\"archived_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_folder_user_id_user_id_fk": { + "name": "workflow_folder_user_id_user_id_fk", + "tableFrom": "workflow_folder", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_folder_workspace_id_workspace_id_fk": { + "name": "workflow_folder_workspace_id_workspace_id_fk", + "tableFrom": "workflow_folder", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_mcp_server": { + "name": "workflow_mcp_server", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_public": { + "name": "is_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "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": "'{}'" + }, + "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 + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_ran_at": { + "name": "last_ran_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_queued_at": { + "name": "last_queued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'UTC'" + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_failed_at": { + "name": "last_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "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 + }, + "job_history": { + "name": "job_history", + "type": "jsonb", + "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": {} + } + }, + "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_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 + }, + "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 + }, + "log_retention_hours": { + "name": "log_retention_hours", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "soft_delete_retention_hours": { + "name": "soft_delete_retention_hours", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "task_cleanup_hours": { + "name": "task_cleanup_hours", + "type": "integer", + "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": { + "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": {} + } + }, + "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" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_byok_keys": { + "name": "workspace_byok_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_byok_provider_unique": { + "name": "workspace_byok_provider_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_byok_workspace_idx": { + "name": "workspace_byok_workspace_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_byok_keys_workspace_id_workspace_id_fk": { + "name": "workspace_byok_keys_workspace_id_workspace_id_fk", + "tableFrom": "workspace_byok_keys", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_byok_keys_created_by_user_id_fk": { + "name": "workspace_byok_keys_created_by_user_id_fk", + "tableFrom": "workspace_byok_keys", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_environment": { + "name": "workspace_environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_environment_workspace_unique": { + "name": "workspace_environment_workspace_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_environment_workspace_id_workspace_id_fk": { + "name": "workspace_environment_workspace_id_workspace_id_fk", + "tableFrom": "workspace_environment", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file": { + "name": "workspace_file", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "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_files": { + "name": "workspace_files", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "context": { + "name": "context", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "original_name": { + "name": "original_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "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_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_name_active_unique": { + "name": "workspace_files_workspace_name_active_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": 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_key_idx": { + "name": "workspace_files_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_user_id_idx": { + "name": "workspace_files_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_id_idx": { + "name": "workspace_files_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_context_idx": { + "name": "workspace_files_context_idx", + "columns": [ + { + "expression": "context", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "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_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_notification_delivery": { + "name": "workspace_notification_delivery", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "subscription_id": { + "name": "subscription_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "notification_delivery_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_attempt_at": { + "name": "last_attempt_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "response_status": { + "name": "response_status", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "response_body": { + "name": "response_body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_notification_delivery_subscription_id_idx": { + "name": "workspace_notification_delivery_subscription_id_idx", + "columns": [ + { + "expression": "subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_notification_delivery_execution_id_idx": { + "name": "workspace_notification_delivery_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_notification_delivery_status_idx": { + "name": "workspace_notification_delivery_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_notification_delivery_next_attempt_idx": { + "name": "workspace_notification_delivery_next_attempt_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_notification_delivery_subscription_id_workspace_notification_subscription_id_fk": { + "name": "workspace_notification_delivery_subscription_id_workspace_notification_subscription_id_fk", + "tableFrom": "workspace_notification_delivery", + "tableTo": "workspace_notification_subscription", + "columnsFrom": ["subscription_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_notification_delivery_workflow_id_workflow_id_fk": { + "name": "workspace_notification_delivery_workflow_id_workflow_id_fk", + "tableFrom": "workspace_notification_delivery", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_notification_subscription": { + "name": "workspace_notification_subscription", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "notification_type": { + "name": "notification_type", + "type": "notification_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "workflow_ids": { + "name": "workflow_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "all_workflows": { + "name": "all_workflows", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "level_filter": { + "name": "level_filter", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY['info', 'error']::text[]" + }, + "trigger_filter": { + "name": "trigger_filter", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY['api', 'webhook', 'schedule', 'manual', 'chat']::text[]" + }, + "include_final_output": { + "name": "include_final_output", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "include_trace_spans": { + "name": "include_trace_spans", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "include_rate_limits": { + "name": "include_rate_limits", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "include_usage_data": { + "name": "include_usage_data", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "webhook_config": { + "name": "webhook_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "email_recipients": { + "name": "email_recipients", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "slack_config": { + "name": "slack_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "alert_config": { + "name": "alert_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "last_alert_at": { + "name": "last_alert_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_notification_workspace_id_idx": { + "name": "workspace_notification_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_notification_active_idx": { + "name": "workspace_notification_active_idx", + "columns": [ + { + "expression": "active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_notification_type_idx": { + "name": "workspace_notification_type_idx", + "columns": [ + { + "expression": "notification_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_notification_subscription_workspace_id_workspace_id_fk": { + "name": "workspace_notification_subscription_workspace_id_workspace_id_fk", + "tableFrom": "workspace_notification_subscription", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_notification_subscription_created_by_user_id_fk": { + "name": "workspace_notification_subscription_created_by_user_id_fk", + "tableFrom": "workspace_notification_subscription", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.a2a_task_status": { + "name": "a2a_task_status", + "schema": "public", + "values": [ + "submitted", + "working", + "input-required", + "completed", + "failed", + "canceled", + "rejected", + "auth-required", + "unknown" + ] + }, + "public.academy_cert_status": { + "name": "academy_cert_status", + "schema": "public", + "values": ["active", "revoked", "expired"] + }, + "public.billing_blocked_reason": { + "name": "billing_blocked_reason", + "schema": "public", + "values": ["payment_failed", "dispute"] + }, + "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.credential_member_role": { + "name": "credential_member_role", + "schema": "public", + "values": ["admin", "member"] + }, + "public.credential_member_status": { + "name": "credential_member_status", + "schema": "public", + "values": ["active", "pending", "revoked"] + }, + "public.credential_set_invitation_status": { + "name": "credential_set_invitation_status", + "schema": "public", + "values": ["pending", "accepted", "expired", "cancelled"] + }, + "public.credential_set_member_status": { + "name": "credential_set_member_status", + "schema": "public", + "values": ["active", "pending", "revoked"] + }, + "public.credential_type": { + "name": "credential_type", + "schema": "public", + "values": ["oauth", "env_workspace", "env_personal", "service_account"] + }, + "public.invitation_kind": { + "name": "invitation_kind", + "schema": "public", + "values": ["organization", "workspace"] + }, + "public.invitation_status": { + "name": "invitation_status", + "schema": "public", + "values": ["pending", "accepted", "rejected", "cancelled", "expired"] + }, + "public.notification_delivery_status": { + "name": "notification_delivery_status", + "schema": "public", + "values": ["pending", "in_progress", "success", "failed"] + }, + "public.notification_type": { + "name": "notification_type", + "schema": "public", + "values": ["webhook", "email", "slack"] + }, + "public.permission_type": { + "name": "permission_type", + "schema": "public", + "values": ["admin", "write", "read"] + }, + "public.template_creator_type": { + "name": "template_creator_type", + "schema": "public", + "values": ["user", "organization"] + }, + "public.template_status": { + "name": "template_status", + "schema": "public", + "values": ["pending", "approved", "rejected"] + }, + "public.usage_log_category": { + "name": "usage_log_category", + "schema": "public", + "values": ["model", "fixed"] + }, + "public.usage_log_source": { + "name": "usage_log_source", + "schema": "public", + "values": [ + "workflow", + "wand", + "copilot", + "workspace-chat", + "mcp_copilot", + "mothership_block", + "knowledge-base", + "voice-input" + ] + }, + "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 64fd8b510c4..4fed0e4931e 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -1359,6 +1359,13 @@ "when": 1776800382630, "tag": "0194_careless_pete_wisdom", "breakpoints": true + }, + { + "idx": 195, + "version": "7", + "when": 1776883116756, + "tag": "0195_normal_white_queen", + "breakpoints": true } ] } diff --git a/packages/db/package.json b/packages/db/package.json index 147f89bb887..3fb830fce12 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -22,6 +22,7 @@ "db:push": "bunx drizzle-kit push --config=./drizzle.config.ts", "db:migrate": "bun --env-file=.env run ./scripts/migrate.ts", "db:studio": "bunx drizzle-kit studio --config=./drizzle.config.ts", + "test": "vitest run", "type-check": "tsc --noEmit", "lint": "biome check --write --unsafe .", "lint:check": "biome check .", diff --git a/packages/db/schema.ts b/packages/db/schema.ts index 037649b75f7..54b301813fd 100644 --- a/packages/db/schema.ts +++ b/packages/db/schema.ts @@ -734,6 +734,7 @@ export const apiKey = pgTable( createdBy: text('created_by').references(() => user.id, { onDelete: 'set null' }), name: text('name').notNull(), key: text('key').notNull().unique(), + keyHash: text('key_hash'), type: text('type').notNull().default('personal'), lastUsed: timestamp('last_used'), createdAt: timestamp('created_at').notNull().defaultNow(), @@ -747,6 +748,7 @@ export const apiKey = pgTable( ), workspaceTypeIdx: index('api_key_workspace_type_idx').on(table.workspaceId, table.type), userTypeIdx: index('api_key_user_type_idx').on(table.userId, table.type), + keyHashIdx: uniqueIndex('api_key_key_hash_idx').on(table.keyHash), }) ) diff --git a/packages/db/scripts/backfill-api-key-hash.test.ts b/packages/db/scripts/backfill-api-key-hash.test.ts new file mode 100644 index 00000000000..7f518241e96 --- /dev/null +++ b/packages/db/scripts/backfill-api-key-hash.test.ts @@ -0,0 +1,78 @@ +/** + * @vitest-environment node + * + * Tests for the pure helpers used by `backfill-api-key-hash.ts`. The script + * itself does I/O against Postgres and is not tested here; idempotency is a + * property of the helpers — running them twice on the same input produces the + * same output, so re-running the script after an interruption recomputes the + * same hash for every row. + */ +import { createCipheriv } from 'crypto' +import { describe, expect, it } from 'vitest' +import { deriveKeyHashForStoredKey, hashApiKey, isEncryptedKey } from './backfill-api-key-hash' + +const FIXED_ENCRYPTION_KEY = '0'.repeat(64) + +function encryptForTest(plainKey: string, keyHex: string): string { + const key = Buffer.from(keyHex, 'hex') + const iv = Buffer.from('11'.repeat(16), 'hex') + const cipher = createCipheriv('aes-256-gcm', key, iv, { authTagLength: 16 }) + let encrypted = cipher.update(plainKey, 'utf8', 'hex') + encrypted += cipher.final('hex') + const authTag = cipher.getAuthTag() + return `${iv.toString('hex')}:${encrypted}:${authTag.toString('hex')}` +} + +describe('hashApiKey', () => { + it('is deterministic', () => { + expect(hashApiKey('sim_abc')).toBe(hashApiKey('sim_abc')) + }) + + it('returns a 64-char hex digest', () => { + expect(hashApiKey('sim_abc')).toMatch(/^[0-9a-f]{64}$/) + }) +}) + +describe('isEncryptedKey', () => { + it('detects "iv:encrypted:authTag" format', () => { + expect(isEncryptedKey('aa:bb:cc')).toBe(true) + }) + + it('rejects plain text', () => { + expect(isEncryptedKey('sim_plain')).toBe(false) + }) + + it('rejects strings with the wrong number of colons', () => { + expect(isEncryptedKey('a:b')).toBe(false) + expect(isEncryptedKey('a:b:c:d')).toBe(false) + }) +}) + +describe('deriveKeyHashForStoredKey — backfill idempotency', () => { + it('hashes a legacy plain-text row', () => { + const hash = deriveKeyHashForStoredKey('sim_legacy-plain', null) + expect(hash).toBe(hashApiKey('sim_legacy-plain')) + }) + + it('decrypts + hashes an encrypted row, matching the plaintext hash', () => { + const plainKey = 'sk-sim-some-example-key' + const encrypted = encryptForTest(plainKey, FIXED_ENCRYPTION_KEY) + + const hash = deriveKeyHashForStoredKey(encrypted, FIXED_ENCRYPTION_KEY) + expect(hash).toBe(hashApiKey(plainKey)) + }) + + it('produces the same hash when re-run on the same row', () => { + const plainKey = 'sk-sim-idempotent' + const encrypted = encryptForTest(plainKey, FIXED_ENCRYPTION_KEY) + + const first = deriveKeyHashForStoredKey(encrypted, FIXED_ENCRYPTION_KEY) + const second = deriveKeyHashForStoredKey(encrypted, FIXED_ENCRYPTION_KEY) + expect(first).toBe(second) + }) + + it('throws when the row looks encrypted but no encryption key is supplied', () => { + const encrypted = encryptForTest('sk-sim-x', FIXED_ENCRYPTION_KEY) + expect(() => deriveKeyHashForStoredKey(encrypted, null)).toThrow(/API_ENCRYPTION_KEY/) + }) +}) diff --git a/packages/db/scripts/backfill-api-key-hash.ts b/packages/db/scripts/backfill-api-key-hash.ts new file mode 100644 index 00000000000..0d6640d9da7 --- /dev/null +++ b/packages/db/scripts/backfill-api-key-hash.ts @@ -0,0 +1,231 @@ +#!/usr/bin/env bun + +/** + * Backfills the `key_hash` column on the `api_key` table. + * + * The authentication hot path is being rewritten to look up API keys by the + * SHA-256 hash of the plain-text key — a single indexed equality lookup rather + * than a full-table scan + AES-GCM decrypt loop. This script populates + * `key_hash` for every existing row so the new fast path can match historic + * keys. + * + * For each row where `key_hash IS NULL`: + * - If `key` is in encrypted format (iv:encrypted:authTag), decrypt it using + * `API_ENCRYPTION_KEY` to recover the plain-text key. + * - Otherwise treat `key` as legacy plain text. + * - Compute `sha256(plainKey)` and update the row. + * + * The script is idempotent: it only touches rows where `key_hash IS NULL`, and + * re-running after a partial failure continues where it left off. + * + * Usage: + * POSTGRES_URL=... API_ENCRYPTION_KEY=... \ + * bun run packages/db/scripts/backfill-api-key-hash.ts + * # or + * POSTGRES_URL=... API_ENCRYPTION_KEY=... \ + * bun run packages/db/scripts/backfill-api-key-hash.ts --dry-run + */ + +import { createCipheriv, createDecipheriv, createHash } from 'crypto' +import { and, eq, isNull, sql } from 'drizzle-orm' +import { drizzle } from 'drizzle-orm/postgres-js' +import postgres from 'postgres' +import { apiKey } from '../schema' + +const BATCH_SIZE = 500 + +export function isEncryptedKey(storedKey: string): boolean { + return storedKey.includes(':') && storedKey.split(':').length === 3 +} + +export function hashApiKey(plainKey: string): string { + return createHash('sha256').update(plainKey, 'utf8').digest('hex') +} + +function decryptApiKey(encryptedValue: string, apiEncryptionKey: string): string { + const parts = encryptedValue.split(':') + if (parts.length !== 3) { + return encryptedValue + } + + const key = Buffer.from(apiEncryptionKey, 'hex') + const [ivHex, encrypted, authTagHex] = parts + if (!ivHex || !encrypted || !authTagHex) { + throw new Error('Invalid encrypted api_key format. Expected "iv:encrypted:authTag"') + } + + const iv = Buffer.from(ivHex, 'hex') + const authTag = Buffer.from(authTagHex, 'hex') + + const decipher = createDecipheriv('aes-256-gcm', key, iv, { authTagLength: 16 }) + decipher.setAuthTag(authTag) + + let decrypted = decipher.update(encrypted, 'hex', 'utf8') + decrypted += decipher.final('utf8') + return decrypted +} + +/** + * Computes the hash to write to `key_hash` for a row during backfill. Pure: + * no I/O, no globals — safe to import from tests. Throws when the stored + * value looks encrypted but the caller has no encryption key. + */ +export function deriveKeyHashForStoredKey( + storedKey: string, + apiEncryptionKey: string | null +): string { + if (isEncryptedKey(storedKey)) { + if (!apiEncryptionKey) { + throw new Error('API_ENCRYPTION_KEY is required to decrypt an encrypted stored key') + } + return hashApiKey(decryptApiKey(storedKey, apiEncryptionKey)) + } + return hashApiKey(storedKey) +} + +interface BackfillStats { + scanned: number + updated: number + skippedEncryptedNoKey: number + failed: number +} + +export async function runBackfill(): Promise { + const dryRun = process.argv.includes('--dry-run') + + const connectionString = process.env.POSTGRES_URL ?? process.env.DATABASE_URL + if (!connectionString) { + console.error('Missing POSTGRES_URL or DATABASE_URL environment variable') + process.exit(1) + } + + const apiEncryptionKey = process.env.API_ENCRYPTION_KEY ?? null + if (!apiEncryptionKey) { + console.warn( + 'API_ENCRYPTION_KEY is not set. Rows whose stored key is encrypted will fail to decrypt. ' + + 'Only rows whose stored key is already plain text will be backfilled in this run.' + ) + } else if (apiEncryptionKey.length !== 64) { + console.error('API_ENCRYPTION_KEY must be a 64-character hex string (32 bytes)') + process.exit(1) + } + + assertCryptoRoundTrip(apiEncryptionKey) + + const postgresClient = postgres(connectionString, { + prepare: false, + idle_timeout: 20, + connect_timeout: 30, + max: 5, + onnotice: () => {}, + }) + const db = drizzle(postgresClient) + + const stats: BackfillStats = { + scanned: 0, + updated: 0, + skippedEncryptedNoKey: 0, + failed: 0, + } + + try { + const [{ count: pendingBefore }] = await db + .select({ count: sql`count(*)::int` }) + .from(apiKey) + .where(isNull(apiKey.keyHash)) + + console.log( + `Backfill starting — ${pendingBefore} row(s) with NULL key_hash${dryRun ? ' [DRY RUN]' : ''}` + ) + + for (;;) { + const rows = await db + .select({ id: apiKey.id, key: apiKey.key }) + .from(apiKey) + .where(isNull(apiKey.keyHash)) + .limit(BATCH_SIZE) + + if (rows.length === 0) break + + await Promise.all( + rows.map(async (row) => { + stats.scanned += 1 + try { + if (isEncryptedKey(row.key) && !apiEncryptionKey) { + stats.skippedEncryptedNoKey += 1 + return + } + + const keyHash = deriveKeyHashForStoredKey(row.key, apiEncryptionKey) + + if (dryRun) { + stats.updated += 1 + return + } + + await db + .update(apiKey) + .set({ keyHash }) + .where(and(eq(apiKey.id, row.id), isNull(apiKey.keyHash))) + + stats.updated += 1 + } catch (error) { + stats.failed += 1 + console.error( + `Failed to backfill api_key id=${row.id}: ${error instanceof Error ? error.message : String(error)}` + ) + } + }) + ) + + console.log( + ` progress: scanned=${stats.scanned} updated=${stats.updated} skipped=${stats.skippedEncryptedNoKey} failed=${stats.failed}` + ) + if (dryRun) break + } + + const [{ count: pendingAfter }] = await db + .select({ count: sql`count(*)::int` }) + .from(apiKey) + .where(isNull(apiKey.keyHash)) + + console.log('Backfill complete.') + console.log(` scanned: ${stats.scanned}`) + console.log(` updated: ${stats.updated}`) + console.log(` skipped (no api key): ${stats.skippedEncryptedNoKey}`) + console.log(` failed: ${stats.failed}`) + console.log(` remaining null: ${pendingAfter}`) + + if (stats.failed > 0 || pendingAfter > 0) { + process.exitCode = 1 + } + } finally { + await postgresClient.end({ timeout: 5 }).catch(() => {}) + } +} + +/** Fails fast if the AES-GCM round-trip disagrees with itself in this env. */ +function assertCryptoRoundTrip(apiEncryptionKey: string | null): void { + if (!apiEncryptionKey) return + const key = Buffer.from(apiEncryptionKey, 'hex') + const sample = 'sk-sim-roundtrip-test-value' + const iv = Buffer.from('00'.repeat(16), 'hex') + const cipher = createCipheriv('aes-256-gcm', key, iv, { authTagLength: 16 }) + let encrypted = cipher.update(sample, 'utf8', 'hex') + encrypted += cipher.final('hex') + const authTag = cipher.getAuthTag() + const assembled = `${iv.toString('hex')}:${encrypted}:${authTag.toString('hex')}` + const roundTripped = decryptApiKey(assembled, apiEncryptionKey) + if (roundTripped !== sample) { + throw new Error('Crypto self-test failed — refusing to run backfill') + } +} + +if ((import.meta as { main?: boolean }).main) { + try { + await runBackfill() + } catch (error) { + console.error('Backfill aborted:', error instanceof Error ? error.message : error) + process.exitCode = 1 + } +} diff --git a/packages/db/vitest.config.ts b/packages/db/vitest.config.ts new file mode 100644 index 00000000000..ce441ea3306 --- /dev/null +++ b/packages/db/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + globals: false, + environment: 'node', + include: ['scripts/**/*.test.ts'], + }, +}) diff --git a/packages/testing/src/mocks/schema.mock.ts b/packages/testing/src/mocks/schema.mock.ts index cb1a9fac6b1..6a632feca15 100644 --- a/packages/testing/src/mocks/schema.mock.ts +++ b/packages/testing/src/mocks/schema.mock.ts @@ -331,6 +331,7 @@ export const schemaMock = { createdBy: 'createdBy', name: 'name', key: 'key', + keyHash: 'keyHash', type: 'type', lastUsed: 'lastUsed', createdAt: 'createdAt', From f7ab39984cce9b2f2469bbbbbe118254e0e24b24 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Wed, 22 Apr 2026 16:52:56 -0700 Subject: [PATCH 2/4] feat(ui): add thinking ui to mothership (#4254) * feat(ui): Add thinking ui * fix tests * Remove duplicate helper for block timing * fix lint * fix endedAt timestamp bug * fix stuck subagent thinking --- apps/sim/app/api/copilot/chat/stop/route.ts | 2 + .../components/agent-group/agent-group.tsx | 49 ++++-- .../message-content/components/index.ts | 1 + .../components/thinking-block/index.ts | 1 + .../thinking-block/thinking-block.tsx | 123 +++++++++++++ .../message-content/message-content.tsx | 93 +++++++++- .../[workspaceId]/home/hooks/use-chat.ts | 163 +++++++++++++++--- .../app/workspace/[workspaceId]/home/types.ts | 3 + apps/sim/hooks/use-auto-scroll.ts | 18 ++ apps/sim/lib/copilot/chat/display-message.ts | 9 + .../copilot/chat/persisted-message.test.ts | 4 +- .../sim/lib/copilot/chat/persisted-message.ts | 33 +++- apps/sim/lib/copilot/request/go/stream.ts | 30 ++++ apps/sim/lib/copilot/request/handlers/tool.ts | 25 +++ .../sim/lib/copilot/request/handlers/types.ts | 6 + apps/sim/lib/copilot/request/types.ts | 1 + 16 files changed, 520 insertions(+), 41 deletions(-) create mode 100644 apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/thinking-block/index.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/thinking-block/thinking-block.tsx diff --git a/apps/sim/app/api/copilot/chat/stop/route.ts b/apps/sim/app/api/copilot/chat/stop/route.ts index 2df009774cd..5feed89a58e 100644 --- a/apps/sim/app/api/copilot/chat/stop/route.ts +++ b/apps/sim/app/api/copilot/chat/stop/route.ts @@ -52,6 +52,8 @@ const ContentBlockSchema = z.object({ lifecycle: z.enum(['start', 'end']).optional(), status: z.enum(['complete', 'error', 'cancelled']).optional(), toolCall: StoredToolCallSchema.optional(), + timestamp: z.number().optional(), + endedAt: z.number().optional(), }) const StopSchema = z.object({ diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group.tsx index 4f1d8dc5b87..12128e905f2 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/agent-group.tsx @@ -5,10 +5,12 @@ import { ChevronDown, Expandable, ExpandableContent, PillsRing } from '@/compone import { cn } from '@/lib/core/utils/cn' import type { ToolCallData } from '../../../../types' import { getAgentIcon } from '../../utils' +import { ThinkingBlock } from '../thinking-block' import { ToolCallItem } from './tool-call-item' export type AgentGroupItem = | { type: 'text'; content: string } + | { type: 'thinking'; content: string; startedAt?: number; endedAt?: number } | { type: 'tool'; data: ToolCallData } interface AgentGroupProps { @@ -16,6 +18,7 @@ interface AgentGroupProps { agentLabel: string items: AgentGroupItem[] isDelegating?: boolean + isStreaming?: boolean autoCollapse?: boolean defaultExpanded?: boolean } @@ -35,6 +38,7 @@ export function AgentGroup({ agentLabel, items, isDelegating = false, + isStreaming = false, autoCollapse = false, defaultExpanded = false, }: AgentGroupProps) { @@ -110,16 +114,39 @@ export function AgentGroup({
- {items.map((item, idx) => - item.type === 'tool' ? ( - - ) : ( + {items.map((item, idx) => { + if (item.type === 'tool') { + return ( + + ) + } + if (item.type === 'thinking') { + const elapsedMs = + item.startedAt !== undefined && item.endedAt !== undefined + ? item.endedAt - item.startedAt + : undefined + if (elapsedMs !== undefined && elapsedMs <= 3000) return null + return ( +
+ +
+ ) + } + return ( ) - )} + })}
diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/index.ts b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/index.ts index 67b1b0fd82c..b2b5eaf99ed 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/index.ts @@ -3,3 +3,4 @@ export { AgentGroup, CircleStop } from './agent-group' export { ChatContent } from './chat-content' export { Options } from './options' export { PendingTagIndicator, parseSpecialTags, SpecialTags } from './special-tags' +export { ThinkingBlock } from './thinking-block' diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/thinking-block/index.ts b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/thinking-block/index.ts new file mode 100644 index 00000000000..4b82db6a47d --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/thinking-block/index.ts @@ -0,0 +1 @@ +export { ThinkingBlock } from './thinking-block' diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/thinking-block/thinking-block.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/thinking-block/thinking-block.tsx new file mode 100644 index 00000000000..d0ada76b080 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/thinking-block/thinking-block.tsx @@ -0,0 +1,123 @@ +'use client' + +import { useEffect, useLayoutEffect, useRef, useState } from 'react' +import { ChevronDown, Expandable, ExpandableContent } from '@/components/emcn' +import { BrainIcon } from '@/components/icons' +import { cn } from '@/lib/core/utils/cn' + +interface ThinkingBlockProps { + content: string + isActive: boolean + isStreaming?: boolean + startedAt?: number + endedAt?: number +} + +const MIN_VISIBLE_THINKING_MS = 3000 + +export function ThinkingBlock({ + content, + isActive, + isStreaming = false, + startedAt, + endedAt, +}: ThinkingBlockProps) { + // Start collapsed so the `Expandable` plays its height-open animation + // when `expanded` flips to true below — otherwise the panel mounts + // already-open and jumps up with its full content in one frame. + const [expanded, setExpanded] = useState(false) + const panelRef = useRef(null) + const wasActiveRef = useRef(null) + // Suppress active thinking until it exceeds MIN_VISIBLE_THINKING_MS. + // Completed-<=threshold is filtered upstream in message-content, so if + // we're mounted with isActive=false we've already passed that gate. + const [thresholdReached, setThresholdReached] = useState(() => { + if (!isActive || startedAt === undefined) return true + return Date.now() - startedAt > MIN_VISIBLE_THINKING_MS + }) + + useEffect(() => { + if (thresholdReached) return + if (!isActive || startedAt === undefined) { + setThresholdReached(true) + return + } + const remainingMs = Math.max(0, MIN_VISIBLE_THINKING_MS - (Date.now() - startedAt)) + const id = window.setTimeout(() => setThresholdReached(true), remainingMs + 50) + return () => window.clearTimeout(id) + }, [isActive, startedAt, thresholdReached]) + + useEffect(() => { + // Wait until the threshold has actually been reached — otherwise this + // effect fires during the 3-second hidden period (while the component + // returns null) and sets `expanded` to true before the panel is even + // rendered, so the Collapsible mounts already-open with no animation. + if (!thresholdReached) return + if (wasActiveRef.current === isActive) return + // On first run (wasActiveRef === null): open if the stream is live — + // even when thinking itself has already ended — so a mid-stream refresh + // shows the thinking panel open while the rest of the response is still + // being generated. Subsequent runs only react to the isActive transition + // (auto-collapse when thinking ends). + const isFirstRun = wasActiveRef.current === null + wasActiveRef.current = isActive + const target = isFirstRun ? isActive || isStreaming : isActive + // Defer to the next frame so Radix Collapsible paints the closed state + // first, then sees the transition to open. Without this, React can batch + // the mount + flip into a single commit and the animation never plays. + const id = window.requestAnimationFrame(() => setExpanded(target)) + return () => window.cancelAnimationFrame(id) + }, [isActive, isStreaming, thresholdReached]) + + useLayoutEffect(() => { + if (!isActive || !expanded) return + const el = panelRef.current + if (!el) return + el.scrollTop = el.scrollHeight + }, [content, isActive, expanded]) + + if (!thresholdReached) return null + + const elapsedMs = + startedAt !== undefined && endedAt !== undefined && endedAt >= startedAt + ? endedAt - startedAt + : undefined + const elapsedSeconds = + elapsedMs !== undefined ? Math.max(1, Math.round(elapsedMs / 1000)) : undefined + const label = isActive + ? 'Thinking' + : elapsedSeconds !== undefined + ? `Thought for ${elapsedSeconds}s` + : 'Thought' + + return ( +
+ + + + +
+
+ {content} +
+
+
+
+
+ ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx index 8e64e1203fc..3223a9a54ce 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx @@ -10,7 +10,14 @@ import { ClientToolCallState } from '@/lib/copilot/tools/client/tool-call-state' import type { ContentBlock, MothershipResource, OptionItem, ToolCallData } from '../../types' import { SUBAGENT_LABELS, TOOL_UI_METADATA } from '../../types' import type { AgentGroupItem } from './components' -import { AgentGroup, ChatContent, CircleStop, Options, PendingTagIndicator } from './components' +import { + AgentGroup, + ChatContent, + CircleStop, + Options, + PendingTagIndicator, + ThinkingBlock, +} from './components' const FILE_SUBAGENT_ID = 'file' @@ -19,6 +26,14 @@ interface TextSegment { content: string } +interface ThinkingSegment { + type: 'thinking' + id: string + content: string + startedAt?: number + endedAt?: number +} + interface AgentGroupSegment { type: 'agent_group' id: string @@ -38,7 +53,12 @@ interface StoppedSegment { type: 'stopped' } -type MessageSegment = TextSegment | AgentGroupSegment | OptionsSegment | StoppedSegment +type MessageSegment = + | TextSegment + | ThinkingSegment + | AgentGroupSegment + | OptionsSegment + | StoppedSegment const SUBAGENT_KEYS = new Set(Object.keys(SUBAGENT_LABELS)) @@ -156,6 +176,46 @@ function parseBlocks(blocks: ContentBlock[]): MessageSegment[] { continue } + if (block.type === 'subagent_thinking') { + if (!block.content || !group) continue + group.isDelegating = false + const lastItem = group.items[group.items.length - 1] + if (lastItem?.type === 'thinking' && lastItem.endedAt === undefined) { + lastItem.content += block.content + if (block.endedAt !== undefined) lastItem.endedAt = block.endedAt + } else { + group.items.push({ + type: 'thinking', + content: block.content, + startedAt: block.timestamp, + endedAt: block.endedAt, + }) + } + continue + } + + if (block.type === 'thinking') { + if (!block.content?.trim()) continue + if (group) { + pushGroup(group) + group = null + } + const last = segments[segments.length - 1] + if (last?.type === 'thinking' && last.endedAt === undefined) { + last.content += block.content + if (block.endedAt !== undefined) last.endedAt = block.endedAt + } else { + segments.push({ + type: 'thinking', + id: `thinking-${i}`, + content: block.content, + startedAt: block.timestamp, + endedAt: block.endedAt, + }) + } + continue + } + if (block.type === 'text') { if (!block.content) continue if (block.subagent) { @@ -383,7 +443,9 @@ export function MessageContent({ const hasSubagentEnded = blocks.some((b) => b.type === 'subagent_end') const showTrailingThinking = - isStreaming && !hasTrailingContent && (hasSubagentEnded || allLastGroupToolsDone) + isStreaming && + !hasTrailingContent && + (lastSegment.type === 'thinking' || hasSubagentEnded || allLastGroupToolsDone) const lastOpenSubagentGroupId = [...segments] .reverse() .find( @@ -405,6 +467,30 @@ export function MessageContent({ onWorkspaceResourceSelect={onWorkspaceResourceSelect} /> ) + case 'thinking': { + const isActive = + isStreaming && i === segments.length - 1 && segment.endedAt === undefined + const elapsedMs = + segment.startedAt !== undefined && segment.endedAt !== undefined + ? segment.endedAt - segment.startedAt + : undefined + // Hide completed thinking that took 3s or less — quick thinking + // isn't worth the visual noise. Still show while active (unknown + // duration yet) and still show when timing is missing (old + // persisted blocks) so we don't drop historical content. + if (elapsedMs !== undefined && elapsedMs <= 3000) return null + return ( +
+ +
+ ) + } case 'agent_group': { const toolItems = segment.items.filter((item) => item.type === 'tool') const allToolsDone = @@ -419,6 +505,7 @@ export function MessageContent({ agentLabel={segment.agentLabel} items={segment.items} isDelegating={segment.isDelegating} + isStreaming={isStreaming} autoCollapse={allToolsDone && hasFollowingText} defaultExpanded={segment.id === lastOpenSubagentGroupId} /> diff --git a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts index 576e9e55e20..d77a3632771 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts @@ -11,7 +11,7 @@ import type { PersistedFileAttachment, PersistedMessage, } from '@/lib/copilot/chat/persisted-message' -import { normalizeMessage } from '@/lib/copilot/chat/persisted-message' +import { normalizeMessage, withBlockTiming } from '@/lib/copilot/chat/persisted-message' import { resolveStreamToolOutcome } from '@/lib/copilot/chat/stream-tool-outcome' import { MOTHERSHIP_CHAT_API_PATH, STREAM_STORAGE_KEY } from '@/lib/copilot/constants' import type { @@ -26,6 +26,7 @@ import { MothershipStreamV1SessionKind, MothershipStreamV1SpanLifecycleEvent, MothershipStreamV1SpanPayloadKind, + MothershipStreamV1TextChannel, MothershipStreamV1ToolOutcome, MothershipStreamV1ToolPhase, MothershipStreamV1ToolStatus, @@ -699,11 +700,37 @@ function parseStreamBatchResponse(value: unknown): StreamBatchResponse { } function toRawPersistedContentBlock(block: ContentBlock): Record | null { + const persisted = toRawPersistedContentBlockBody(block) + return persisted ? withBlockTiming(persisted, block) : null +} + +function toRawPersistedContentBlockBody(block: ContentBlock): Record | null { switch (block.type) { case 'text': return { type: MothershipStreamV1EventType.text, ...(block.subagent ? { lane: 'subagent' } : {}), + channel: MothershipStreamV1TextChannel.assistant, + content: block.content ?? '', + } + case 'thinking': + return { + type: MothershipStreamV1EventType.text, + channel: MothershipStreamV1TextChannel.thinking, + content: block.content ?? '', + } + case 'subagent_thinking': + return { + type: MothershipStreamV1EventType.text, + lane: 'subagent', + channel: MothershipStreamV1TextChannel.thinking, + content: block.content ?? '', + } + case 'subagent_text': + return { + type: MothershipStreamV1EventType.text, + lane: 'subagent', + channel: MothershipStreamV1TextChannel.assistant, content: block.content ?? '', } case 'tool_call': @@ -773,22 +800,27 @@ function buildAssistantSnapshotMessage(params: { } function markMessageStopped(message: PersistedMessage): PersistedMessage { - if (!message.contentBlocks?.some((block) => block.toolCall?.state === 'executing')) { + const hasExecutingTool = message.contentBlocks?.some( + (block) => block.toolCall?.state === 'executing' + ) + const hasOpenBlock = message.contentBlocks?.some((block) => block.endedAt === undefined) + if (!hasExecutingTool && !hasOpenBlock) { return message } - const nextBlocks = message.contentBlocks.map((block) => { - if (block.toolCall?.state !== 'executing') { - return block + const stopTs = Date.now() + const nextBlocks = (message.contentBlocks ?? []).map((block) => { + const stamped = block.endedAt === undefined ? { ...block, endedAt: stopTs } : block + if (stamped.toolCall?.state !== 'executing') { + return stamped } - return { - ...block, + ...stamped, toolCall: { - ...block.toolCall, + ...stamped.toolCall, state: 'cancelled' as const, display: { - ...(block.toolCall.display ?? {}), + ...(stamped.toolCall.display ?? {}), title: 'Stopped by user', }, }, @@ -1716,10 +1748,34 @@ export function useChat( streamingBlocksRef.current = [] } - const ensureTextBlock = (subagentName?: string): ContentBlock => { + const toEventMs = (ts: string | undefined): number => { + if (ts) { + const parsed = Date.parse(ts) + if (Number.isFinite(parsed)) return parsed + } + return Date.now() + } + + const stampBlockEnd = (block: ContentBlock | undefined, ts?: string) => { + if (block && block.endedAt === undefined) block.endedAt = toEventMs(ts) + } + + const ensureTextBlock = (subagentName: string | undefined, ts?: string): ContentBlock => { const last = blocks[blocks.length - 1] if (last?.type === 'text' && last.subagent === subagentName) return last - const b: ContentBlock = { type: 'text', content: '' } + stampBlockEnd(last, ts) + const b: ContentBlock = { type: 'text', content: '', timestamp: toEventMs(ts) } + if (subagentName) b.subagent = subagentName + blocks.push(b) + return b + } + + const ensureThinkingBlock = (subagentName: string | undefined, ts?: string): ContentBlock => { + const targetType = subagentName ? 'subagent_thinking' : 'thinking' + const last = blocks[blocks.length - 1] + if (last?.type === targetType && last.subagent === subagentName) return last + stampBlockEnd(last, ts) + const b: ContentBlock = { type: targetType, content: '', timestamp: toEventMs(ts) } if (subagentName) b.subagent = subagentName blocks.push(b) return b @@ -1737,9 +1793,9 @@ export function useChat( return activeSubagent } - const appendInlineErrorTag = (tag: string, subagentName?: string) => { + const appendInlineErrorTag = (tag: string, subagentName?: string, ts?: string) => { if (runningText.includes(tag)) return - const tb = ensureTextBlock(subagentName) + const tb = ensureTextBlock(subagentName, ts) const prefix = runningText.length > 0 && !runningText.endsWith('\n') ? '\n' : '' tb.content = `${tb.content ?? ''}${prefix}${tag}` runningText += `${prefix}${tag}` @@ -1950,13 +2006,20 @@ export function useChat( case MothershipStreamV1EventType.text: { const chunk = parsed.payload.text if (chunk) { + const eventTs = typeof parsed.ts === 'string' ? parsed.ts : undefined + if (parsed.payload.channel === MothershipStreamV1TextChannel.thinking) { + const tb = ensureThinkingBlock(scopedSubagent, eventTs) + tb.content = (tb.content ?? '') + chunk + flushText() + break + } const contentSource: 'main' | 'subagent' = scopedSubagent ? 'subagent' : 'main' const needsBoundaryNewline = lastContentSource !== null && lastContentSource !== contentSource && runningText.length > 0 && !runningText.endsWith('\n') - const tb = ensureTextBlock(scopedSubagent) + const tb = ensureTextBlock(scopedSubagent, eventTs) const normalizedChunk = needsBoundaryNewline ? `\n${chunk}` : chunk tb.content = (tb.content ?? '') + normalizedChunk runningText += normalizedChunk @@ -2170,6 +2233,7 @@ export function useChat( output: payload.output, error: typeof payload.error === 'string' ? payload.error : undefined, } + stampBlockEnd(blocks[idx]) flush() if (tc.name === ReadTool.id && tc.status === 'success') { @@ -2292,6 +2356,7 @@ export function useChat( } if (!toolMap.has(id)) { + stampBlockEnd(blocks[blocks.length - 1]) toolMap.set(id, blocks.length) blocks.push({ type: 'tool_call', @@ -2303,6 +2368,7 @@ export function useChat( params: args, calledBy: scopedSubagent, }, + timestamp: Date.now(), }) if (name === ReadTool.id || isResourceToolName(name)) { if (args) toolArgsMap.set(id, args) @@ -2376,6 +2442,7 @@ export function useChat( if (payload.kind === MothershipStreamV1RunKind.compaction_start) { const compactionId = `compaction_${Date.now()}` activeCompactionId = compactionId + stampBlockEnd(blocks[blocks.length - 1]) toolMap.set(compactionId, blocks.length) blocks.push({ type: 'tool_call', @@ -2385,6 +2452,7 @@ export function useChat( status: 'executing', displayTitle: 'Compacting context...', }, + timestamp: Date.now(), }) flush() } else if (payload.kind === MothershipStreamV1RunKind.compaction_done) { @@ -2394,8 +2462,10 @@ export function useChat( if (idx !== undefined && blocks[idx]?.toolCall) { blocks[idx].toolCall!.status = 'success' blocks[idx].toolCall!.displayTitle = 'Compacted context' + stampBlockEnd(blocks[idx]) } else { toolMap.set(compactionId, blocks.length) + const endNow = Date.now() blocks.push({ type: 'tool_call', toolCall: { @@ -2404,6 +2474,8 @@ export function useChat( status: 'success', displayTitle: 'Compacted context', }, + timestamp: endNow, + endedAt: endNow, }) } flush() @@ -2432,7 +2504,8 @@ export function useChat( activeSubagent = name activeSubagentParentToolCallId = parentToolCallId if (!isSameActiveSubagent) { - blocks.push({ type: 'subagent', content: name }) + stampBlockEnd(blocks[blocks.length - 1]) + blocks.push({ type: 'subagent', content: name, timestamp: Date.now() }) } if (name === FILE_SUBAGENT_ID && !isSameActiveSubagent) { applyPreviewSessionUpdate({ @@ -2472,7 +2545,18 @@ export function useChat( activeSubagent = undefined activeSubagentParentToolCallId = undefined } - blocks.push({ type: 'subagent_end' }) + const endNow = Date.now() + if (name) { + for (let i = blocks.length - 1; i >= 0; i--) { + const b = blocks[i] + if (b.type === 'subagent' && b.content === name && b.endedAt === undefined) { + b.endedAt = endNow + break + } + } + } + stampBlockEnd(blocks[blocks.length - 1]) + blocks.push({ type: 'subagent_end', timestamp: endNow }) flush() } break @@ -2480,11 +2564,16 @@ export function useChat( case MothershipStreamV1EventType.error: { sawStreamError = true setError(parsed.payload.message || parsed.payload.error || 'An error occurred') - appendInlineErrorTag(buildInlineErrorTag(parsed.payload), scopedSubagent) + appendInlineErrorTag( + buildInlineErrorTag(parsed.payload), + scopedSubagent, + typeof parsed.ts === 'string' ? parsed.ts : undefined + ) break } case MothershipStreamV1EventType.complete: { sawCompleteEvent = true + stampBlockEnd(blocks[blocks.length - 1]) // `complete` is the end-of-turn marker; drain whatever // else arrived in the same TCP chunk (trailing text, // followups, run metadata) before stopping. Do NOT @@ -2888,6 +2977,10 @@ export function useChat( const sourceBlocks = overrides?.blocks ?? streamingBlocksRef.current const storedBlocks = sourceBlocks.map((block) => { + const timing = { + ...(typeof block.timestamp === 'number' ? { timestamp: block.timestamp } : {}), + ...(typeof block.endedAt === 'number' ? { endedAt: block.endedAt } : {}), + } if (block.type === 'tool_call' && block.toolCall) { const isCancelled = block.toolCall.status === 'executing' || block.toolCall.status === 'cancelled' @@ -2905,9 +2998,10 @@ export function useChat( ...(display ? { display } : {}), calledBy: block.toolCall.calledBy, }, + ...timing, } } - return { type: block.type, content: block.content } + return { type: block.type, content: block.content, ...timing } }) if (storedBlocks.length > 0) { @@ -3465,11 +3559,21 @@ export function useChat( queryClient.getQueryData(taskKeys.detail(chatIdRef.current)) ?.activeStreamId || undefined + // Snapshot the active assistant message id BEFORE clearActiveTurn() + // nulls the ref. Used below to restrict markMessageStopped to the + // in-flight turn only — historical messages from the chat history + // also lack `endedAt` on their legacy blocks (pre-timing-fields), + // and without this gate we'd corrupt them with cancelled markers. + const activeAssistantMessageId = + activeTurnRef.current?.assistantMessageId ?? + (sid ? getLiveAssistantMessageId(sid) : undefined) const stopContentSnapshot = streamingContentRef.current + const stopNow = Date.now() const stopBlocksSnapshot = streamingBlocksRef.current.map((block) => ({ ...block, ...(block.options ? { options: [...block.options] } : {}), ...(block.toolCall ? { toolCall: { ...block.toolCall } } : {}), + ...(block.endedAt === undefined ? { endedAt: stopNow } : {}), })) // Snapshot BEFORE clearActiveTurn() nulls the refs. Both // persistPartialResponse and the abort/stop fetches run inside @@ -3491,22 +3595,31 @@ export function useChat( await queryClient.cancelQueries({ queryKey: taskKeys.detail(activeChatId) }) upsertTaskChatHistory(activeChatId, (current) => ({ ...current, - messages: current.messages.map(markMessageStopped), + messages: current.messages.map((message) => + activeAssistantMessageId && message.id === activeAssistantMessageId + ? markMessageStopped(message) + : message + ), })) } else { setPendingMessages((prev) => prev.map((msg) => { - if (!msg.contentBlocks?.some((block) => block.toolCall?.status === 'executing')) { + const hasExecutingTool = msg.contentBlocks?.some( + (block) => block.toolCall?.status === 'executing' + ) + const hasOpenBlock = msg.contentBlocks?.some((block) => block.endedAt === undefined) + if (!hasExecutingTool && !hasOpenBlock) { return msg } - const updatedBlocks = msg.contentBlocks.map((block) => { - if (block.toolCall?.status !== 'executing') { - return block + const updatedBlocks = (msg.contentBlocks ?? []).map((block) => { + const stamped = block.endedAt === undefined ? { ...block, endedAt: stopNow } : block + if (stamped.toolCall?.status !== 'executing') { + return stamped } return { - ...block, + ...stamped, toolCall: { - ...block.toolCall, + ...stamped.toolCall, status: 'cancelled' as const, displayTitle: 'Stopped by user', }, diff --git a/apps/sim/app/workspace/[workspaceId]/home/types.ts b/apps/sim/app/workspace/[workspaceId]/home/types.ts index d41ea9e3d36..16aa0d80e17 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/types.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/types.ts @@ -114,6 +114,7 @@ export interface OptionItem { export const ContentBlockType = { text: 'text', + thinking: 'thinking', tool_call: 'tool_call', subagent: 'subagent', subagent_end: 'subagent_end', @@ -130,6 +131,8 @@ export interface ContentBlock { subagent?: string toolCall?: ToolCallInfo options?: OptionItem[] + timestamp?: number + endedAt?: number } export interface ChatMessageAttachment { diff --git a/apps/sim/hooks/use-auto-scroll.ts b/apps/sim/hooks/use-auto-scroll.ts index e8829af5f5c..c70ad843416 100644 --- a/apps/sim/hooks/use-auto-scroll.ts +++ b/apps/sim/hooks/use-auto-scroll.ts @@ -102,10 +102,27 @@ export function useAutoScroll( rafIdRef.current = requestAnimationFrame(guardedScroll) } + // CSS-driven height animations (e.g. Radix Collapsible expanding + // mid-stream) grow scrollHeight without triggering MutationObserver, + // so auto-scroll stops following. When any animation starts in the + // container, follow rAF for a short window so the container stays + // pinned to the bottom while the animation runs. + const onAnimationStart = () => { + if (!stickyRef.current) return + const until = performance.now() + 500 + const follow = () => { + if (performance.now() > until || !stickyRef.current) return + scrollToBottom() + requestAnimationFrame(follow) + } + requestAnimationFrame(follow) + } + el.addEventListener('wheel', onWheel, { passive: true }) el.addEventListener('touchstart', onTouchStart, { passive: true }) el.addEventListener('touchmove', onTouchMove, { passive: true }) el.addEventListener('scroll', onScroll, { passive: true }) + el.addEventListener('animationstart', onAnimationStart) const observer = new MutationObserver(onMutation) observer.observe(el, { childList: true, subtree: true, characterData: true }) @@ -115,6 +132,7 @@ export function useAutoScroll( el.removeEventListener('touchstart', onTouchStart) el.removeEventListener('touchmove', onTouchMove) el.removeEventListener('scroll', onScroll) + el.removeEventListener('animationstart', onAnimationStart) observer.disconnect() cancelAnimationFrame(rafIdRef.current) if (stickyRef.current) scrollToBottom() diff --git a/apps/sim/lib/copilot/chat/display-message.ts b/apps/sim/lib/copilot/chat/display-message.ts index a5a86c20ae3..a728701e2d9 100644 --- a/apps/sim/lib/copilot/chat/display-message.ts +++ b/apps/sim/lib/copilot/chat/display-message.ts @@ -16,6 +16,7 @@ import { ToolCallStatus, } from '@/app/workspace/[workspaceId]/home/types' import type { PersistedContentBlock, PersistedMessage } from './persisted-message' +import { withBlockTiming } from './persisted-message' const STATE_TO_STATUS: Record = { [MothershipStreamV1ToolOutcome.success]: ToolCallStatus.success, @@ -44,6 +45,11 @@ function toToolCallInfo(block: PersistedContentBlock): ToolCallInfo | undefined } function toDisplayBlock(block: PersistedContentBlock): ContentBlock | undefined { + const displayed = toDisplayBlockBody(block) + return displayed ? withBlockTiming(displayed, block) : undefined +} + +function toDisplayBlockBody(block: PersistedContentBlock): ContentBlock | undefined { switch (block.type) { case MothershipStreamV1EventType.text: if (block.lane === 'subagent') { @@ -52,6 +58,9 @@ function toDisplayBlock(block: PersistedContentBlock): ContentBlock | undefined } return { type: ContentBlockType.subagent_text, content: block.content } } + if (block.channel === 'thinking') { + return { type: ContentBlockType.thinking, content: block.content } + } return { type: ContentBlockType.text, content: block.content } case MothershipStreamV1EventType.tool: if (!toToolCallInfo(block)) return undefined diff --git a/apps/sim/lib/copilot/chat/persisted-message.test.ts b/apps/sim/lib/copilot/chat/persisted-message.test.ts index b6e3193efce..377a8c2b0b5 100644 --- a/apps/sim/lib/copilot/chat/persisted-message.test.ts +++ b/apps/sim/lib/copilot/chat/persisted-message.test.ts @@ -12,6 +12,7 @@ import { describe('persisted-message', () => { it('round-trips canonical tool blocks through normalizeMessage', () => { + const blockTimestamp = 1_700_000_000_000 const result: OrchestratorResult = { success: true, content: 'done', @@ -19,7 +20,7 @@ describe('persisted-message', () => { contentBlocks: [ { type: 'tool_call', - timestamp: Date.now(), + timestamp: blockTimestamp, calledBy: 'workflow', toolCall: { id: 'tool-1', @@ -41,6 +42,7 @@ describe('persisted-message', () => { { type: 'tool', phase: 'call', + timestamp: blockTimestamp, toolCall: { id: 'tool-1', name: 'read', diff --git a/apps/sim/lib/copilot/chat/persisted-message.ts b/apps/sim/lib/copilot/chat/persisted-message.ts index cb0518700d2..ba12391aee3 100644 --- a/apps/sim/lib/copilot/chat/persisted-message.ts +++ b/apps/sim/lib/copilot/chat/persisted-message.ts @@ -38,6 +38,8 @@ export interface PersistedContentBlock { status?: MothershipStreamV1CompletionStatus content?: string toolCall?: PersistedToolCall + timestamp?: number + endedAt?: number } export interface PersistedFileAttachment { @@ -85,7 +87,25 @@ function resolveToolState(block: ContentBlock): PersistedToolState { return tc.status as PersistedToolState } +/** + * Copy `timestamp` / `endedAt` from a source object onto a target object. + * Shared by every block mapper (persist, display, snapshot) so the timing + * metadata that drives the `Thought for Ns` chip survives the full + * persist → normalize → display round-trip — and one rule lives in one place. + */ +export function withBlockTiming(target: T, src: { timestamp?: number; endedAt?: number }): T { + const writable = target as { timestamp?: number; endedAt?: number } + if (typeof src.timestamp === 'number') writable.timestamp = src.timestamp + if (typeof src.endedAt === 'number') writable.endedAt = src.endedAt + return target +} + function mapContentBlock(block: ContentBlock): PersistedContentBlock { + const persisted = mapContentBlockBody(block) + return withBlockTiming(persisted, block) +} + +function mapContentBlockBody(block: ContentBlock): PersistedContentBlock { switch (block.type) { case 'text': return { @@ -242,6 +262,8 @@ interface RawBlock { kind?: string lifecycle?: string status?: string + timestamp?: number + endedAt?: number toolCall?: { id?: string name?: string @@ -406,7 +428,16 @@ function normalizeLegacyBlock(block: RawBlock): PersistedContentBlock { } function normalizeBlock(block: RawBlock): PersistedContentBlock { - return isCanonicalBlock(block) ? normalizeCanonicalBlock(block) : normalizeLegacyBlock(block) + const result = isCanonicalBlock(block) + ? normalizeCanonicalBlock(block) + : normalizeLegacyBlock(block) + if (typeof block.timestamp === 'number' && result.timestamp === undefined) { + result.timestamp = block.timestamp + } + if (typeof block.endedAt === 'number' && result.endedAt === undefined) { + result.endedAt = block.endedAt + } + return result } function normalizeLegacyToolCall(tc: LegacyToolCall): PersistedContentBlock { diff --git a/apps/sim/lib/copilot/request/go/stream.ts b/apps/sim/lib/copilot/request/go/stream.ts index b82362770c9..a3e42f94371 100644 --- a/apps/sim/lib/copilot/request/go/stream.ts +++ b/apps/sim/lib/copilot/request/go/stream.ts @@ -24,6 +24,10 @@ import { sseHandlers, subAgentHandlers, } from '@/lib/copilot/request/handlers' +import { + flushSubagentThinkingBlock, + flushThinkingBlock, +} from '@/lib/copilot/request/handlers/types' import { getCopilotTracer } from '@/lib/copilot/request/otel' import { eventToStreamEvent, @@ -337,6 +341,13 @@ export async function runStreamLoop( const subagentName = streamEvent.payload.agent const spanEvt = streamEvent.payload.event const isPendingPause = spanData?.pending === true + // A subagent lifecycle boundary breaks the main thinking stream. + // Flush any open thinking block into contentBlocks BEFORE we push + // the `subagent` marker, or the persisted order ends up + // [subagent, thinking] and the UI renders the subagent group + // above a thinking block that actually happened first. + flushSubagentThinkingBlock(context) + flushThinkingBlock(context) if (spanEvt === MothershipStreamV1SpanLifecycleEvent.start) { const lastParent = context.subAgentParentStack[context.subAgentParentStack.length - 1] const lastBlock = context.contentBlocks[context.contentBlocks.length - 1] @@ -377,6 +388,19 @@ export async function runStreamLoop( context.subAgentParentStack.length > 0 ? context.subAgentParentStack[context.subAgentParentStack.length - 1] : undefined + if (subagentName) { + for (let i = context.contentBlocks.length - 1; i >= 0; i--) { + const b = context.contentBlocks[i] + if ( + b.type === 'subagent' && + b.content === subagentName && + b.endedAt === undefined + ) { + b.endedAt = Date.now() + break + } + } + } return } } @@ -434,6 +458,12 @@ export async function runStreamLoop( endedOn = CopilotSseCloseReason.Aborted } } + // An abort or error can tear down the loop mid-thinking. Flush any + // open thinking blocks so partial-persistence on /chat/stop sees + // them in contentBlocks with endedAt stamped, instead of silently + // dropping the in-flight reasoning. + flushSubagentThinkingBlock(context) + flushThinkingBlock(context) clearTimeout(timeoutId) // Legacy TraceCollector span (consumed by the in-memory trace diff --git a/apps/sim/lib/copilot/request/handlers/tool.ts b/apps/sim/lib/copilot/request/handlers/tool.ts index 610464dc2e0..803c9c9ea69 100644 --- a/apps/sim/lib/copilot/request/handlers/tool.ts +++ b/apps/sim/lib/copilot/request/handlers/tool.ts @@ -36,6 +36,8 @@ import { addContentBlock, emitSyntheticToolResult, ensureTerminalToolCallState, + flushSubagentThinkingBlock, + flushThinkingBlock, getScopedParentToolCallId, getToolCallUI, getToolResultErrorMessage, @@ -130,6 +132,14 @@ export async function handleToolEvent( return } + // A tool event breaks the thinking stream. Flush any open thinking + // block into contentBlocks BEFORE we add the tool_call block, or + // contentBlocks will end up with tool_call before thinking — which + // re-renders on reload in the wrong order (Mothership group above + // the Thinking block, even though thinking happened first). + flushSubagentThinkingBlock(context) + flushThinkingBlock(context) + if (isToolResultStreamEvent(event)) { handleResultPhase(event.payload, context, parentToolCallId) return @@ -190,9 +200,24 @@ function handleResultPhase( ...(errorMessage ? { error: errorMessage } : {}), endTime, }) + stampToolCallBlockEnd(context, toolCallId, endTime) markToolResultSeen(toolCallId) } +function stampToolCallBlockEnd( + context: StreamingContext, + toolCallId: string, + endTime: number +): void { + for (let i = context.contentBlocks.length - 1; i >= 0; i--) { + const block = context.contentBlocks[i] + if (block.type === 'tool_call' && block.toolCall?.id === toolCallId) { + if (block.endedAt === undefined) block.endedAt = endTime + return + } + } +} + async function handleCallPhase( data: MothershipStreamV1ToolCallDescriptor, context: StreamingContext, diff --git a/apps/sim/lib/copilot/request/handlers/types.ts b/apps/sim/lib/copilot/request/handlers/types.ts index 9a1aa0d3967..4909410b8ae 100644 --- a/apps/sim/lib/copilot/request/handlers/types.ts +++ b/apps/sim/lib/copilot/request/handlers/types.ts @@ -51,12 +51,17 @@ export function addContentBlock( }) } +export function stampBlockEnd(block: ContentBlock): void { + if (block.endedAt === undefined) block.endedAt = Date.now() +} + /** * Flush any open thinking block into contentBlocks and clear the thinking state. * Safe to call repeatedly. */ export function flushThinkingBlock(context: StreamingContext): void { if (context.currentThinkingBlock) { + stampBlockEnd(context.currentThinkingBlock) context.contentBlocks.push(context.currentThinkingBlock) } context.isInThinkingBlock = false @@ -65,6 +70,7 @@ export function flushThinkingBlock(context: StreamingContext): void { export function flushSubagentThinkingBlock(context: StreamingContext): void { if (context.currentSubagentThinkingBlock) { + stampBlockEnd(context.currentSubagentThinkingBlock) context.contentBlocks.push(context.currentSubagentThinkingBlock) } context.currentSubagentThinkingBlock = null diff --git a/apps/sim/lib/copilot/request/types.ts b/apps/sim/lib/copilot/request/types.ts index fd296cd52ca..bbb6264fd82 100644 --- a/apps/sim/lib/copilot/request/types.ts +++ b/apps/sim/lib/copilot/request/types.ts @@ -55,6 +55,7 @@ export interface ContentBlock { toolCall?: ToolCallState calledBy?: string timestamp: number + endedAt?: number } export interface StreamingContext { From bed5e95742b6e2160588616fe0685ba3124348d2 Mon Sep 17 00:00:00 2001 From: Waleed Date: Wed, 22 Apr 2026 20:40:59 -0700 Subject: [PATCH 3/4] fix(selectors): enable search on all picker and selector subBlocks (#4269) --- apps/sim/blocks/blocks/airtable.ts | 2 -- apps/sim/blocks/blocks/asana.ts | 2 -- apps/sim/blocks/blocks/attio.ts | 2 -- apps/sim/blocks/blocks/calcom.ts | 4 ---- apps/sim/blocks/blocks/confluence.ts | 1 - apps/sim/blocks/blocks/google_bigquery.ts | 2 -- apps/sim/blocks/blocks/google_calendar.ts | 2 -- apps/sim/blocks/blocks/google_sheets.ts | 1 - apps/sim/blocks/blocks/google_tasks.ts | 1 - apps/sim/blocks/blocks/jira_service_management.ts | 2 -- apps/sim/blocks/blocks/microsoft_excel.ts | 2 -- apps/sim/blocks/blocks/microsoft_planner.ts | 1 - apps/sim/blocks/blocks/microsoft_teams.ts | 3 --- apps/sim/blocks/blocks/notion.ts | 1 - apps/sim/blocks/blocks/pipedrive.ts | 1 - apps/sim/blocks/blocks/sharepoint.ts | 1 - apps/sim/blocks/blocks/trello.ts | 1 - apps/sim/blocks/blocks/webflow.ts | 2 -- 18 files changed, 31 deletions(-) diff --git a/apps/sim/blocks/blocks/airtable.ts b/apps/sim/blocks/blocks/airtable.ts index 48e9b670bc4..ff0e3036040 100644 --- a/apps/sim/blocks/blocks/airtable.ts +++ b/apps/sim/blocks/blocks/airtable.ts @@ -62,7 +62,6 @@ export const AirtableBlock: BlockConfig = { canonicalParamId: 'baseId', serviceId: 'airtable', selectorKey: 'airtable.bases', - selectorAllowSearch: false, placeholder: 'Select Airtable base', dependsOn: ['credential'], mode: 'basic', @@ -86,7 +85,6 @@ export const AirtableBlock: BlockConfig = { canonicalParamId: 'tableId', serviceId: 'airtable', selectorKey: 'airtable.tables', - selectorAllowSearch: false, placeholder: 'Select Airtable table', dependsOn: ['credential', 'baseSelector'], mode: 'basic', diff --git a/apps/sim/blocks/blocks/asana.ts b/apps/sim/blocks/blocks/asana.ts index 4da1b84f961..da9bfe57e60 100644 --- a/apps/sim/blocks/blocks/asana.ts +++ b/apps/sim/blocks/blocks/asana.ts @@ -58,7 +58,6 @@ export const AsanaBlock: BlockConfig = { canonicalParamId: 'workspace', serviceId: 'asana', selectorKey: 'asana.workspaces', - selectorAllowSearch: false, placeholder: 'Select Asana workspace', dependsOn: ['credential'], mode: 'basic', @@ -110,7 +109,6 @@ export const AsanaBlock: BlockConfig = { canonicalParamId: 'getTasks_workspace', serviceId: 'asana', selectorKey: 'asana.workspaces', - selectorAllowSearch: false, placeholder: 'Select Asana workspace', dependsOn: ['credential'], mode: 'basic', diff --git a/apps/sim/blocks/blocks/attio.ts b/apps/sim/blocks/blocks/attio.ts index 9a6af86f6a3..056d4c4e2ba 100644 --- a/apps/sim/blocks/blocks/attio.ts +++ b/apps/sim/blocks/blocks/attio.ts @@ -96,7 +96,6 @@ export const AttioBlock: BlockConfig = { canonicalParamId: 'objectType', serviceId: 'attio', selectorKey: 'attio.objects', - selectorAllowSearch: false, placeholder: 'Select object type', dependsOn: ['credential'], mode: 'basic', @@ -570,7 +569,6 @@ Return ONLY the JSON array. No explanations, no markdown, no extra text. canonicalParamId: 'listIdOrSlug', serviceId: 'attio', selectorKey: 'attio.lists', - selectorAllowSearch: false, placeholder: 'Select Attio list', dependsOn: ['credential'], mode: 'basic', diff --git a/apps/sim/blocks/blocks/calcom.ts b/apps/sim/blocks/blocks/calcom.ts index 2b08bac4165..fd53d4b13b4 100644 --- a/apps/sim/blocks/blocks/calcom.ts +++ b/apps/sim/blocks/blocks/calcom.ts @@ -74,7 +74,6 @@ export const CalComBlock: BlockConfig = { canonicalParamId: 'eventTypeId', serviceId: 'calcom', selectorKey: 'calcom.eventTypes', - selectorAllowSearch: false, placeholder: 'Select event type', dependsOn: ['credential'], mode: 'basic', @@ -289,7 +288,6 @@ Return ONLY the IANA timezone string - no explanations or quotes.`, canonicalParamId: 'eventTypeIdParam', serviceId: 'calcom', selectorKey: 'calcom.eventTypes', - selectorAllowSearch: false, placeholder: 'Select event type', dependsOn: ['credential'], mode: 'basic', @@ -414,7 +412,6 @@ Return ONLY the IANA timezone string - no explanations or quotes.`, canonicalParamId: 'eventTypeScheduleId', serviceId: 'calcom', selectorKey: 'calcom.schedules', - selectorAllowSearch: false, placeholder: 'Select schedule', dependsOn: ['credential'], mode: 'basic', @@ -455,7 +452,6 @@ Return ONLY the IANA timezone string - no explanations or quotes.`, canonicalParamId: 'scheduleId', serviceId: 'calcom', selectorKey: 'calcom.schedules', - selectorAllowSearch: false, placeholder: 'Select schedule', dependsOn: ['credential'], mode: 'basic', diff --git a/apps/sim/blocks/blocks/confluence.ts b/apps/sim/blocks/blocks/confluence.ts index 81ab5c02c7a..bb45ee2a838 100644 --- a/apps/sim/blocks/blocks/confluence.ts +++ b/apps/sim/blocks/blocks/confluence.ts @@ -589,7 +589,6 @@ export const ConfluenceV2Block: BlockConfig = { canonicalParamId: 'spaceId', serviceId: 'confluence', selectorKey: 'confluence.spaces', - selectorAllowSearch: false, placeholder: 'Select Confluence space', dependsOn: ['credential', 'domain'], mode: 'basic', diff --git a/apps/sim/blocks/blocks/google_bigquery.ts b/apps/sim/blocks/blocks/google_bigquery.ts index 7a138b87b5a..54c668780c0 100644 --- a/apps/sim/blocks/blocks/google_bigquery.ts +++ b/apps/sim/blocks/blocks/google_bigquery.ts @@ -118,7 +118,6 @@ Return ONLY the SQL query - no explanations, no quotes, no extra text.`, canonicalParamId: 'datasetId', serviceId: 'google-bigquery', selectorKey: 'bigquery.datasets', - selectorAllowSearch: false, placeholder: 'Select BigQuery dataset', dependsOn: ['credential', 'projectId'], mode: 'basic', @@ -143,7 +142,6 @@ Return ONLY the SQL query - no explanations, no quotes, no extra text.`, canonicalParamId: 'tableId', serviceId: 'google-bigquery', selectorKey: 'bigquery.tables', - selectorAllowSearch: false, placeholder: 'Select BigQuery table', dependsOn: ['credential', 'projectId', 'datasetSelector'], mode: 'basic', diff --git a/apps/sim/blocks/blocks/google_calendar.ts b/apps/sim/blocks/blocks/google_calendar.ts index 0c984503c9a..08ec3faa9d2 100644 --- a/apps/sim/blocks/blocks/google_calendar.ts +++ b/apps/sim/blocks/blocks/google_calendar.ts @@ -68,7 +68,6 @@ export const GoogleCalendarBlock: BlockConfig = { canonicalParamId: 'calendarId', serviceId: 'google-calendar', selectorKey: 'google.calendar', - selectorAllowSearch: false, requiredScopes: getScopesForService('google-calendar'), placeholder: 'Select calendar', dependsOn: ['credential'], @@ -334,7 +333,6 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`, canonicalParamId: 'destinationCalendarId', serviceId: 'google-calendar', selectorKey: 'google.calendar', - selectorAllowSearch: false, requiredScopes: getScopesForService('google-calendar'), placeholder: 'Select destination calendar', dependsOn: ['credential'], diff --git a/apps/sim/blocks/blocks/google_sheets.ts b/apps/sim/blocks/blocks/google_sheets.ts index bf9445bb048..b2258b06c8b 100644 --- a/apps/sim/blocks/blocks/google_sheets.ts +++ b/apps/sim/blocks/blocks/google_sheets.ts @@ -387,7 +387,6 @@ export const GoogleSheetsV2Block: BlockConfig = { canonicalParamId: 'sheetName', serviceId: 'google-sheets', selectorKey: 'google.sheets', - selectorAllowSearch: false, placeholder: 'Select a sheet', required: true, dependsOn: { all: ['credential'], any: ['spreadsheetId', 'manualSpreadsheetId'] }, diff --git a/apps/sim/blocks/blocks/google_tasks.ts b/apps/sim/blocks/blocks/google_tasks.ts index 57b0ef45d27..8e3dbec0fea 100644 --- a/apps/sim/blocks/blocks/google_tasks.ts +++ b/apps/sim/blocks/blocks/google_tasks.ts @@ -64,7 +64,6 @@ export const GoogleTasksBlock: BlockConfig = { canonicalParamId: 'taskListId', serviceId: 'google-tasks', selectorKey: 'google.tasks.lists', - selectorAllowSearch: false, placeholder: 'Select task list', dependsOn: ['credential'], mode: 'basic', diff --git a/apps/sim/blocks/blocks/jira_service_management.ts b/apps/sim/blocks/blocks/jira_service_management.ts index 3a9ed8c07a1..1e7c94f825f 100644 --- a/apps/sim/blocks/blocks/jira_service_management.ts +++ b/apps/sim/blocks/blocks/jira_service_management.ts @@ -95,7 +95,6 @@ export const JiraServiceManagementBlock: BlockConfig = { canonicalParamId: 'serviceDeskId', serviceId: 'jira', selectorKey: 'jsm.serviceDesks', - selectorAllowSearch: false, placeholder: 'Select service desk', dependsOn: ['credential', 'domain'], mode: 'basic', @@ -169,7 +168,6 @@ export const JiraServiceManagementBlock: BlockConfig = { canonicalParamId: 'requestTypeId', serviceId: 'jira', selectorKey: 'jsm.requestTypes', - selectorAllowSearch: false, placeholder: 'Select request type', dependsOn: ['credential', 'domain', 'serviceDeskSelector'], mode: 'basic', diff --git a/apps/sim/blocks/blocks/microsoft_excel.ts b/apps/sim/blocks/blocks/microsoft_excel.ts index 8106d234251..ca0da941d21 100644 --- a/apps/sim/blocks/blocks/microsoft_excel.ts +++ b/apps/sim/blocks/blocks/microsoft_excel.ts @@ -428,7 +428,6 @@ export const MicrosoftExcelV2Block: BlockConfig = { canonicalParamId: 'driveId', serviceId: 'microsoft-excel', selectorKey: 'microsoft.excel.drives', - selectorAllowSearch: false, placeholder: 'Select a document library', dependsOn: ['credential', 'siteSelector', 'fileSource'], condition: { field: 'fileSource', value: 'sharepoint' }, @@ -478,7 +477,6 @@ export const MicrosoftExcelV2Block: BlockConfig = { canonicalParamId: 'sheetName', serviceId: 'microsoft-excel', selectorKey: 'microsoft.excel.sheets', - selectorAllowSearch: false, placeholder: 'Select a sheet', required: true, dependsOn: { diff --git a/apps/sim/blocks/blocks/microsoft_planner.ts b/apps/sim/blocks/blocks/microsoft_planner.ts index cfa7c8bf08b..cb7dcd1f1f8 100644 --- a/apps/sim/blocks/blocks/microsoft_planner.ts +++ b/apps/sim/blocks/blocks/microsoft_planner.ts @@ -87,7 +87,6 @@ export const MicrosoftPlannerBlock: BlockConfig = { canonicalParamId: 'planId', serviceId: 'microsoft-planner', selectorKey: 'microsoft.planner.plans', - selectorAllowSearch: false, placeholder: 'Select a plan', dependsOn: ['credential'], mode: 'basic', diff --git a/apps/sim/blocks/blocks/microsoft_teams.ts b/apps/sim/blocks/blocks/microsoft_teams.ts index 65869e81a9a..b2a882e489c 100644 --- a/apps/sim/blocks/blocks/microsoft_teams.ts +++ b/apps/sim/blocks/blocks/microsoft_teams.ts @@ -70,7 +70,6 @@ export const MicrosoftTeamsBlock: BlockConfig = { canonicalParamId: 'teamId', serviceId: 'microsoft-teams', selectorKey: 'microsoft.teams', - selectorAllowSearch: false, requiredScopes: [], placeholder: 'Select a team', dependsOn: ['credential'], @@ -117,7 +116,6 @@ export const MicrosoftTeamsBlock: BlockConfig = { canonicalParamId: 'chatId', serviceId: 'microsoft-teams', selectorKey: 'microsoft.chats', - selectorAllowSearch: false, requiredScopes: [], placeholder: 'Select a chat', dependsOn: ['credential'], @@ -148,7 +146,6 @@ export const MicrosoftTeamsBlock: BlockConfig = { canonicalParamId: 'channelId', serviceId: 'microsoft-teams', selectorKey: 'microsoft.channels', - selectorAllowSearch: false, requiredScopes: [], placeholder: 'Select a channel', dependsOn: ['credential', 'teamSelector'], diff --git a/apps/sim/blocks/blocks/notion.ts b/apps/sim/blocks/blocks/notion.ts index 4528d950f83..5afb6eeeda1 100644 --- a/apps/sim/blocks/blocks/notion.ts +++ b/apps/sim/blocks/blocks/notion.ts @@ -93,7 +93,6 @@ export const NotionBlock: BlockConfig = { canonicalParamId: 'databaseId', serviceId: 'notion', selectorKey: 'notion.databases', - selectorAllowSearch: false, placeholder: 'Select Notion database', dependsOn: ['credential'], mode: 'basic', diff --git a/apps/sim/blocks/blocks/pipedrive.ts b/apps/sim/blocks/blocks/pipedrive.ts index 4d7b8606663..789ade9061e 100644 --- a/apps/sim/blocks/blocks/pipedrive.ts +++ b/apps/sim/blocks/blocks/pipedrive.ts @@ -98,7 +98,6 @@ export const PipedriveBlock: BlockConfig = { canonicalParamId: 'pipeline_id', serviceId: 'pipedrive', selectorKey: 'pipedrive.pipelines', - selectorAllowSearch: false, placeholder: 'Select pipeline', dependsOn: ['credential'], mode: 'basic', diff --git a/apps/sim/blocks/blocks/sharepoint.ts b/apps/sim/blocks/blocks/sharepoint.ts index f7527e68315..5698d544d85 100644 --- a/apps/sim/blocks/blocks/sharepoint.ts +++ b/apps/sim/blocks/blocks/sharepoint.ts @@ -108,7 +108,6 @@ export const SharepointBlock: BlockConfig = { canonicalParamId: 'listId', serviceId: 'sharepoint', selectorKey: 'sharepoint.lists', - selectorAllowSearch: false, placeholder: 'Select a list', dependsOn: ['credential', 'siteSelector'], mode: 'basic', diff --git a/apps/sim/blocks/blocks/trello.ts b/apps/sim/blocks/blocks/trello.ts index d06251d89a5..27465ac6ae7 100644 --- a/apps/sim/blocks/blocks/trello.ts +++ b/apps/sim/blocks/blocks/trello.ts @@ -107,7 +107,6 @@ export const TrelloBlock: BlockConfig = { canonicalParamId: 'boardId', serviceId: 'trello', selectorKey: 'trello.boards', - selectorAllowSearch: false, placeholder: 'Select Trello board', dependsOn: ['credential'], mode: 'basic', diff --git a/apps/sim/blocks/blocks/webflow.ts b/apps/sim/blocks/blocks/webflow.ts index db6e7978cb3..fcd106496ab 100644 --- a/apps/sim/blocks/blocks/webflow.ts +++ b/apps/sim/blocks/blocks/webflow.ts @@ -60,7 +60,6 @@ export const WebflowBlock: BlockConfig = { canonicalParamId: 'siteId', serviceId: 'webflow', selectorKey: 'webflow.sites', - selectorAllowSearch: false, placeholder: 'Select Webflow site', dependsOn: ['credential'], mode: 'basic', @@ -82,7 +81,6 @@ export const WebflowBlock: BlockConfig = { canonicalParamId: 'collectionId', serviceId: 'webflow', selectorKey: 'webflow.collections', - selectorAllowSearch: false, placeholder: 'Select collection', dependsOn: ['credential', 'siteSelector'], mode: 'basic', From 5f0f0edd632d7e1353b0857c7a076141f566b4ca Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Wed, 22 Apr 2026 23:06:16 -0700 Subject: [PATCH 4/4] improvement(repo): separate realtime into separate app (#4262) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * improvement(repo): restructuring to make realtime image narrower scoped * improvements * chore(repo): rebase fixes and quality improvements for realtime split Addresses merge-time issues and gaps from the realtime app split: - Retarget stale vi.mock paths to @sim/workflow-persistence/subblocks - Restore README branding, fix AGENTS.md script reference - Restore TSDoc on workflow-persistence subblocks helpers - Use toError() from @sim/utils/errors in save.ts - Add vitest config + local mocks so @sim/audit tests run standalone - Move socket.io-client to devDependencies in apps/realtime - Add missing package COPY steps to docker/app.Dockerfile - Add check:boundaries/check:realtime-prune scripts and wire into CI Co-Authored-By: Claude Opus 4.7 * refactor(security): consolidate crypto primitives into @sim/security Move general-purpose crypto primitives out of apps/sim into the @sim/security package so both apps/sim and apps/realtime can share them. @sim/security exports (all pure, dependency-free): ./compare safeCompare (constant-time HMAC-wrapped equality) ./encryption encrypt/decrypt (AES-256-GCM, iv:cipher:tag format) ./hash sha256Hex ./tokens generateSecureToken (base64url) Migrate apps/sim call sites to use these + @sim/utils helpers: crypto.randomUUID() -> generateId() from @sim/utils/id createHash('sha256').digest -> sha256Hex timingSafeEqual on hashed hex -> safeCompare new Promise(setTimeout) -> sleep from @sim/utils/helpers No behavior change: encryption format, digest output, and token length are preserved exactly. * refactor(copilot): use toError in remaining otel/finalize sites Replace the last two `error instanceof Error ? error : new Error(String(error))` patterns with toError from @sim/utils/errors. Completes the sweep of clean candidates — no behavior change. * refactor(security): consolidate HMAC-SHA256 primitives into @sim/security Adds hmacSha256Hex and hmacSha256Base64 to @sim/security/hmac and migrates 15 webhook providers plus 5 other hot paths (deployment token signing, outbound webhook requests, workspace notification delivery, notification test route, Shopify OAuth callback) off bare `createHmac` calls. Secret parameter accepts `string | Buffer` to cover base64-decoded Svix-style secrets (Resend) and MS Teams' HMAC scheme. AWS SigV4 signing in S3 and Textract tools intentionally retains direct `createHmac` usage — its multi-step key derivation chain doesn't fit a generic helper. Co-Authored-By: Claude Opus 4.7 * chore(packages): post-audit test + packaging polish - Add safeCompare unit tests (identity, length mismatch, hex-nibble diff). - Add Buffer-secret cases to hmac tests to lock in Svix/MS-Teams contract. - Declare `reactflow` as a peerDependency on @sim/workflow-types — only used for type imports. - Add a barrel export to @sim/workflow-persistence for consumers that prefer package-level imports; subpath exports retained. - Document the data-field invariant in load.ts for loop/parallel subflow patching. Co-Authored-By: Claude Opus 4.7 * chore(realtime): address PR review feedback - Remove redundant SOCKET_PORT=3002 env from Dockerfile runner stage (env.PORT already defaults to 3002 via zod schema). - Reorder PORT fallback so an explicitly-set SOCKET_PORT wins over the schema default for PORT; keeps SOCKET_PORT functional as an override instead of dead code. - Add dedicated type-check CI step for @sim/realtime so TS errors surface pre-deploy (the Dockerfile runs source TS via Bun and has no implicit build-time type check). Co-Authored-By: Claude Opus 4.7 * chore(realtime): remove unused SOCKET_PORT env var SOCKET_PORT has lived in the socket server since the June 2025 refactor but was never actually set in any deploy config — docker-compose.prod, helm values/templates, .env.example, and docs all use PORT or the 3002 default exclusively. No self-hoster was ever pointed at SOCKET_PORT, so removing it is safe. Simplifies realtime port resolution to `env.PORT` (zod-validated with a 3002 default) and drops the orphaned sim-side schema entry. Co-Authored-By: Claude Opus 4.7 --------- Co-authored-by: Waleed Latif Co-authored-by: Claude Opus 4.7 --- .claude/rules/sim-testing.md | 2 +- .cursor/rules/sim-testing.mdc | 2 +- .devcontainer/post-create.sh | 14 +- .github/workflows/test-build.yml | 9 + AGENTS.md | 45 ++- apps/realtime/.env.example | 30 ++ apps/realtime/package.json | 48 +++ apps/realtime/src/auth.ts | 17 + .../socket => realtime/src}/config/socket.ts | 4 +- .../src}/database/operations.ts | 51 +-- apps/realtime/src/env.ts | 44 ++ .../src}/handlers/connection.ts | 8 +- apps/realtime/src/handlers/index.ts | 17 + .../src}/handlers/operations.ts | 16 +- .../src}/handlers/presence.ts | 4 +- .../src}/handlers/subblocks.ts | 8 +- .../src}/handlers/variables.ts | 8 +- .../src}/handlers/workflow.test.ts | 8 +- .../src}/handlers/workflow.ts | 8 +- .../socket => realtime/src}/index.test.ts | 67 +-- apps/{sim/socket => realtime/src}/index.ts | 14 +- .../src}/middleware/auth.ts | 5 +- .../src}/middleware/permissions.test.ts | 2 +- .../src}/middleware/permissions.ts | 6 +- apps/realtime/src/rooms/index.ts | 3 + .../src}/rooms/memory-manager.ts | 2 +- .../src}/rooms/redis-manager.ts | 2 +- .../socket => realtime/src}/rooms/types.ts | 0 .../socket => realtime/src}/routes/http.ts | 6 +- .../src}/tests/socket-server.test.ts | 0 apps/realtime/tsconfig.json | 11 + apps/realtime/vitest.config.ts | 27 ++ apps/realtime/vitest.setup.ts | 6 + apps/sim/AGENTS.md | 7 + .../sim/app/api/auth/forget-password/route.ts | 2 +- .../app/api/auth/oauth/credentials/route.ts | 2 +- .../api/auth/oauth/disconnect/route.test.ts | 2 +- .../app/api/auth/oauth/disconnect/route.ts | 2 +- .../api/auth/oauth2/callback/shopify/route.ts | 11 +- apps/sim/app/api/billing/credits/route.ts | 2 +- .../app/api/chat/manage/[id]/route.test.ts | 2 +- apps/sim/app/api/chat/manage/[id]/route.ts | 2 +- apps/sim/app/api/chat/utils.test.ts | 2 +- apps/sim/app/api/chat/utils.ts | 2 +- apps/sim/app/api/copilot/chat/queries.ts | 2 +- apps/sim/app/api/copilot/chat/stream/route.ts | 3 +- apps/sim/app/api/copilot/chats/route.ts | 2 +- .../copilot/checkpoints/revert/route.test.ts | 6 +- .../api/copilot/checkpoints/revert/route.ts | 2 +- .../app/api/copilot/checkpoints/route.test.ts | 4 +- apps/sim/app/api/copilot/checkpoints/route.ts | 2 +- .../[id]/invite/[invitationId]/route.ts | 2 +- .../api/credential-sets/[id]/invite/route.ts | 2 +- .../api/credential-sets/[id]/members/route.ts | 2 +- .../sim/app/api/credential-sets/[id]/route.ts | 2 +- .../credential-sets/invite/[token]/route.ts | 2 +- .../api/credential-sets/memberships/route.ts | 2 +- apps/sim/app/api/credential-sets/route.ts | 2 +- apps/sim/app/api/credentials/[id]/route.ts | 2 +- apps/sim/app/api/credentials/route.ts | 2 +- apps/sim/app/api/environment/route.ts | 2 +- .../app/api/files/serve/[...path]/route.ts | 8 +- apps/sim/app/api/files/upload/route.ts | 2 +- .../app/api/folders/[id]/duplicate/route.ts | 2 +- apps/sim/app/api/folders/[id]/route.test.ts | 2 +- apps/sim/app/api/folders/route.test.ts | 2 +- apps/sim/app/api/folders/route.ts | 2 +- apps/sim/app/api/form/manage/[id]/route.ts | 2 +- apps/sim/app/api/form/route.ts | 2 +- apps/sim/app/api/form/utils.ts | 2 +- apps/sim/app/api/guardrails/validate/route.ts | 2 +- .../app/api/invitations/[id]/accept/route.ts | 2 +- .../app/api/invitations/[id]/reject/route.ts | 2 +- .../app/api/invitations/[id]/resend/route.ts | 2 +- apps/sim/app/api/invitations/[id]/route.ts | 2 +- apps/sim/app/api/jobs/[jobId]/route.test.ts | 10 +- apps/sim/app/api/jobs/[jobId]/route.ts | 13 +- .../[connectorId]/documents/route.test.ts | 2 +- .../[connectorId]/documents/route.ts | 2 +- .../connectors/[connectorId]/route.test.ts | 2 +- .../[id]/connectors/[connectorId]/route.ts | 2 +- .../[connectorId]/sync/route.test.ts | 2 +- .../connectors/[connectorId]/sync/route.ts | 2 +- .../api/knowledge/[id]/connectors/route.ts | 2 +- .../documents/[documentId]/chunks/route.ts | 2 +- .../[id]/documents/[documentId]/route.test.ts | 2 +- .../[id]/documents/[documentId]/route.ts | 2 +- .../knowledge/[id]/documents/route.test.ts | 2 +- .../app/api/knowledge/[id]/documents/route.ts | 4 +- .../knowledge/[id]/documents/upsert/route.ts | 4 +- .../app/api/knowledge/[id]/restore/route.ts | 2 +- apps/sim/app/api/knowledge/[id]/route.test.ts | 2 +- apps/sim/app/api/knowledge/[id]/route.ts | 2 +- apps/sim/app/api/knowledge/route.test.ts | 2 +- apps/sim/app/api/knowledge/route.ts | 2 +- .../app/api/knowledge/search/route.test.ts | 18 +- apps/sim/app/api/knowledge/search/route.ts | 2 +- apps/sim/app/api/mcp/copilot/route.ts | 6 +- apps/sim/app/api/mcp/servers/[id]/route.ts | 2 +- apps/sim/app/api/mcp/servers/route.ts | 2 +- .../api/mcp/workflow-servers/[id]/route.ts | 2 +- .../[id]/tools/[toolId]/route.ts | 2 +- .../mcp/workflow-servers/[id]/tools/route.ts | 2 +- .../sim/app/api/mcp/workflow-servers/route.ts | 2 +- .../[id]/invitations/route.test.ts | 2 +- .../organizations/[id]/invitations/route.ts | 2 +- .../[id]/members/[memberId]/route.ts | 2 +- .../api/organizations/[id]/members/route.ts | 2 +- apps/sim/app/api/organizations/[id]/route.ts | 2 +- .../[id]/transfer-ownership/route.ts | 2 +- .../organizations/[id]/whitelabel/route.ts | 2 +- apps/sim/app/api/organizations/route.test.ts | 2 +- apps/sim/app/api/organizations/route.ts | 2 +- apps/sim/app/api/schedules/[id]/route.test.ts | 12 +- apps/sim/app/api/schedules/[id]/route.ts | 4 +- apps/sim/app/api/schedules/route.test.ts | 8 +- apps/sim/app/api/schedules/route.ts | 4 +- apps/sim/app/api/skills/route.ts | 2 +- .../app/api/table/[tableId]/restore/route.ts | 2 +- apps/sim/app/api/templates/[id]/route.ts | 15 +- apps/sim/app/api/templates/route.ts | 4 +- apps/sim/app/api/tools/custom/route.test.ts | 4 +- apps/sim/app/api/tools/custom/route.ts | 4 +- .../app/api/users/me/api-keys/[id]/route.ts | 2 +- apps/sim/app/api/users/me/api-keys/route.ts | 2 +- .../app/api/v1/admin/access-control/route.ts | 2 +- apps/sim/app/api/v1/admin/auth.ts | 17 +- .../v1/admin/workflows/[id]/deploy/route.ts | 2 +- .../app/api/v1/admin/workflows/[id]/route.ts | 2 +- .../versions/[versionId]/activate/route.ts | 2 +- .../v1/admin/workflows/[id]/versions/route.ts | 2 +- apps/sim/app/api/v1/files/[fileId]/route.ts | 2 +- apps/sim/app/api/v1/files/route.ts | 2 +- .../[id]/documents/[documentId]/route.ts | 2 +- .../api/v1/knowledge/[id]/documents/route.ts | 2 +- apps/sim/app/api/v1/knowledge/[id]/route.ts | 2 +- apps/sim/app/api/v1/knowledge/route.ts | 2 +- .../api/v1/tables/[tableId]/columns/route.ts | 2 +- apps/sim/app/api/v1/tables/[tableId]/route.ts | 2 +- apps/sim/app/api/v1/tables/route.ts | 2 +- apps/sim/app/api/v1/workflows/[id]/route.ts | 2 +- apps/sim/app/api/webhooks/[id]/route.ts | 4 +- apps/sim/app/api/webhooks/route.ts | 4 +- .../api/workflows/[id]/autolayout/route.ts | 2 +- .../workflows/[id]/chat/status/route.test.ts | 6 +- .../api/workflows/[id]/chat/status/route.ts | 2 +- .../app/api/workflows/[id]/duplicate/route.ts | 2 +- .../[id]/execute/route.async.test.ts | 3 +- .../app/api/workflows/[id]/execute/route.ts | 7 +- .../[executionId]/cancel/route.test.ts | 6 +- .../executions/[executionId]/cancel/route.ts | 2 +- .../executions/[executionId]/stream/route.ts | 2 +- .../workflows/[id]/form/status/route.test.ts | 6 +- .../api/workflows/[id]/form/status/route.ts | 2 +- .../app/api/workflows/[id]/restore/route.ts | 2 +- apps/sim/app/api/workflows/[id]/route.test.ts | 5 +- apps/sim/app/api/workflows/[id]/route.ts | 3 +- .../sim/app/api/workflows/[id]/state/route.ts | 2 +- .../workflows/[id]/variables/route.test.ts | 22 +- .../app/api/workflows/[id]/variables/route.ts | 4 +- apps/sim/app/api/workflows/middleware.ts | 3 +- apps/sim/app/api/workflows/route.test.ts | 2 +- apps/sim/app/api/workflows/route.ts | 2 +- .../workspaces/[id]/api-keys/[keyId]/route.ts | 2 +- .../app/api/workspaces/[id]/api-keys/route.ts | 2 +- .../api/workspaces/[id]/byok-keys/route.ts | 2 +- .../workspaces/[id]/data-retention/route.ts | 2 +- .../api/workspaces/[id]/duplicate/route.ts | 2 +- .../api/workspaces/[id]/environment/route.ts | 2 +- .../[id]/files/[fileId]/content/route.ts | 2 +- .../[id]/files/[fileId]/restore/route.ts | 2 +- .../workspaces/[id]/files/[fileId]/route.ts | 2 +- .../app/api/workspaces/[id]/files/route.ts | 2 +- .../notifications/[notificationId]/route.ts | 2 +- .../[notificationId]/test/route.ts | 6 +- .../workspaces/[id]/notifications/route.ts | 2 +- .../[groupId]/members/bulk/route.ts | 2 +- .../[groupId]/members/route.ts | 2 +- .../[id]/permission-groups/[groupId]/route.ts | 2 +- .../[id]/permission-groups/route.ts | 2 +- .../api/workspaces/[id]/permissions/route.ts | 2 +- apps/sim/app/api/workspaces/[id]/route.ts | 2 +- .../api/workspaces/invitations/route.test.ts | 2 +- .../app/api/workspaces/invitations/route.ts | 2 +- .../app/api/workspaces/members/[id]/route.ts | 2 +- apps/sim/app/api/workspaces/route.ts | 2 +- .../deploy/hooks/use-change-detection.ts | 2 +- .../workspace-notification-delivery.ts | 8 +- apps/sim/blocks/types.ts | 110 +---- apps/sim/ee/audit-logs/constants.ts | 2 +- apps/sim/hooks/use-collaborative-workflow.ts | 20 +- apps/sim/hooks/use-undo-redo.ts | 8 +- apps/sim/lib/api-key/auth.ts | 2 +- apps/sim/lib/api-key/crypto.ts | 74 +--- apps/sim/lib/auth/auth.ts | 2 +- apps/sim/lib/auth/internal.ts | 2 +- apps/sim/lib/copilot/auth/permissions.test.ts | 2 +- apps/sim/lib/copilot/auth/permissions.ts | 2 +- apps/sim/lib/copilot/chat/lifecycle.ts | 6 +- .../sim/lib/copilot/chat/persisted-message.ts | 5 +- apps/sim/lib/copilot/chat/post.ts | 7 +- apps/sim/lib/copilot/chat/process-contents.ts | 6 +- apps/sim/lib/copilot/request/http.ts | 2 +- .../lib/copilot/request/lifecycle/finalize.ts | 2 +- apps/sim/lib/copilot/request/lifecycle/run.ts | 3 +- apps/sim/lib/copilot/request/otel.ts | 5 +- apps/sim/lib/copilot/request/tools/tables.ts | 5 +- apps/sim/lib/copilot/tools/handlers/access.ts | 3 +- .../tools/handlers/deployment/deploy.ts | 2 +- .../tools/handlers/deployment/manage.test.ts | 2 +- .../tools/handlers/deployment/manage.ts | 2 +- apps/sim/lib/copilot/tools/handlers/jobs.ts | 2 +- .../tools/handlers/materialize-file.ts | 2 +- apps/sim/lib/copilot/tools/handlers/oauth.ts | 3 +- .../tools/handlers/workflow/mutations.ts | 2 +- .../server/workflow/edit-workflow/index.ts | 2 +- .../server/workflow/get-workflow-logs.ts | 2 +- apps/sim/lib/core/config/env.ts | 1 - apps/sim/lib/core/security/deployment.ts | 13 +- apps/sim/lib/core/security/encryption.ts | 69 +--- apps/sim/lib/execution/preprocessing.test.ts | 2 +- apps/sim/lib/execution/preprocessing.ts | 2 +- .../preprocessing.webhook-correlation.test.ts | 2 +- apps/sim/lib/knowledge/chunks/service.ts | 8 +- apps/sim/lib/knowledge/documents/service.ts | 4 +- apps/sim/lib/logs/events.ts | 2 +- .../lib/logs/execution/snapshot/service.ts | 4 +- apps/sim/lib/messaging/email/unsubscribe.ts | 15 +- apps/sim/lib/webhooks/processor.test.ts | 2 +- apps/sim/lib/webhooks/providers/ashby.ts | 6 +- apps/sim/lib/webhooks/providers/attio.ts | 6 +- apps/sim/lib/webhooks/providers/calcom.ts | 6 +- apps/sim/lib/webhooks/providers/circleback.ts | 6 +- apps/sim/lib/webhooks/providers/fireflies.ts | 6 +- apps/sim/lib/webhooks/providers/github.ts | 2 +- apps/sim/lib/webhooks/providers/gong.ts | 4 +- apps/sim/lib/webhooks/providers/greenhouse.ts | 6 +- apps/sim/lib/webhooks/providers/intercom.ts | 2 +- apps/sim/lib/webhooks/providers/jira.ts | 6 +- apps/sim/lib/webhooks/providers/linear.ts | 6 +- .../lib/webhooks/providers/microsoft-teams.ts | 7 +- apps/sim/lib/webhooks/providers/notion.ts | 6 +- apps/sim/lib/webhooks/providers/resend.ts | 9 +- apps/sim/lib/webhooks/providers/salesforce.ts | 4 +- apps/sim/lib/webhooks/providers/slack.ts | 9 +- .../lib/webhooks/providers/twilio-voice.ts | 2 +- apps/sim/lib/webhooks/providers/typeform.ts | 6 +- apps/sim/lib/webhooks/providers/utils.ts | 2 +- apps/sim/lib/webhooks/providers/vercel.ts | 2 +- apps/sim/lib/webhooks/providers/whatsapp.ts | 9 +- apps/sim/lib/webhooks/providers/zoom.ts | 11 +- apps/sim/lib/workflows/active-context.ts | 58 --- .../workflows/executor/execution-core.test.ts | 2 +- .../lib/workflows/executor/execution-core.ts | 2 +- .../workflows/orchestration/chat-deploy.ts | 2 +- .../sim/lib/workflows/orchestration/deploy.ts | 2 +- .../orchestration/folder-lifecycle.ts | 2 +- .../orchestration/workflow-lifecycle.ts | 2 +- .../workflows/persistence/duplicate.test.ts | 3 +- .../lib/workflows/persistence/duplicate.ts | 6 +- apps/sim/lib/workflows/persistence/utils.ts | 380 ++---------------- apps/sim/lib/workflows/utils.test.ts | 214 +++------- apps/sim/lib/workflows/utils.ts | 95 +---- apps/sim/package.json | 11 +- apps/sim/socket/handlers/index.ts | 17 - apps/sim/socket/rooms/index.ts | 3 - apps/sim/stores/undo-redo/store.ts | 2 +- apps/sim/stores/undo-redo/types.ts | 2 +- apps/sim/stores/undo-redo/utils.ts | 2 +- apps/sim/stores/workflows/utils.ts | 2 +- apps/sim/stores/workflows/workflow/types.ts | 211 ++-------- apps/sim/tools/http/webhook_request.ts | 4 +- apps/sim/vitest.setup.ts | 2 + bun.lock | 156 ++++++- docker/app.Dockerfile | 30 +- docker/realtime.Dockerfile | 73 +--- package.json | 11 +- packages/audit/package.json | 39 ++ packages/audit/src/index.ts | 3 + .../audit => packages/audit/src}/log.test.ts | 25 +- .../lib/audit => packages/audit/src}/log.ts | 24 +- .../lib/audit => packages/audit/src}/types.ts | 0 packages/audit/tsconfig.json | 5 + packages/audit/vitest.config.ts | 9 + packages/auth/package.json | 33 ++ packages/auth/src/verify.ts | 36 ++ packages/auth/tsconfig.json | 5 + packages/db/.env.example | 7 +- packages/realtime-protocol/package.json | 36 ++ .../realtime-protocol/src}/constants.ts | 0 .../realtime-protocol/src}/schemas.ts | 2 +- packages/realtime-protocol/tsconfig.json | 5 + packages/security/package.json | 50 +++ packages/security/src/compare.test.ts | 40 ++ packages/security/src/compare.ts | 13 + packages/security/src/encryption.test.ts | 74 ++++ packages/security/src/encryption.ts | 68 ++++ packages/security/src/hash.test.ts | 20 + packages/security/src/hash.ts | 10 + packages/security/src/hmac.test.ts | 55 +++ packages/security/src/hmac.ts | 24 ++ packages/security/src/tokens.test.ts | 26 ++ packages/security/src/tokens.ts | 12 + packages/security/tsconfig.json | 5 + packages/security/vitest.config.ts | 9 + packages/testing/src/mocks/audit.mock.ts | 6 +- packages/testing/src/mocks/index.ts | 2 + .../testing/src/mocks/workflow-authz.mock.ts | 39 ++ .../testing/src/mocks/workflows-utils.mock.ts | 7 +- packages/workflow-authz/package.json | 33 ++ packages/workflow-authz/src/index.ts | 143 +++++++ packages/workflow-authz/tsconfig.json | 5 + packages/workflow-persistence/package.json | 57 +++ packages/workflow-persistence/src/index.ts | 19 + packages/workflow-persistence/src/load.ts | 182 +++++++++ packages/workflow-persistence/src/save.ts | 108 +++++ .../workflow-persistence/src}/subblocks.ts | 2 +- .../src/subflow-helpers.ts | 94 +++++ packages/workflow-persistence/src/types.ts | 23 ++ packages/workflow-persistence/tsconfig.json | 5 + packages/workflow-types/package.json | 37 ++ packages/workflow-types/src/blocks.ts | 88 ++++ packages/workflow-types/src/workflow.ts | 165 ++++++++ packages/workflow-types/tsconfig.json | 5 + scripts/check-monorepo-boundaries.ts | 77 ++++ scripts/check-realtime-prune-graph.ts | 67 +++ 326 files changed, 2927 insertions(+), 1687 deletions(-) create mode 100644 apps/realtime/.env.example create mode 100644 apps/realtime/package.json create mode 100644 apps/realtime/src/auth.ts rename apps/{sim/socket => realtime/src}/config/socket.ts (96%) rename apps/{sim/socket => realtime/src}/database/operations.ts (97%) create mode 100644 apps/realtime/src/env.ts rename apps/{sim/socket => realtime/src}/handlers/connection.ts (80%) create mode 100644 apps/realtime/src/handlers/index.ts rename apps/{sim/socket => realtime/src}/handlers/operations.ts (98%) rename apps/{sim/socket => realtime/src}/handlers/presence.ts (93%) rename apps/{sim/socket => realtime/src}/handlers/subblocks.ts (97%) rename apps/{sim/socket => realtime/src}/handlers/variables.ts (97%) rename apps/{sim/socket => realtime/src}/handlers/workflow.test.ts (96%) rename apps/{sim/socket => realtime/src}/handlers/workflow.ts (96%) rename apps/{sim/socket => realtime/src}/index.test.ts (85%) rename apps/{sim/socket => realtime/src}/index.ts (92%) rename apps/{sim/socket => realtime/src}/middleware/auth.ts (94%) rename apps/{sim/socket => realtime/src}/middleware/permissions.test.ts (99%) rename apps/{sim/socket => realtime/src}/middleware/permissions.ts (97%) create mode 100644 apps/realtime/src/rooms/index.ts rename apps/{sim/socket => realtime/src}/rooms/memory-manager.ts (99%) rename apps/{sim/socket => realtime/src}/rooms/redis-manager.ts (99%) rename apps/{sim/socket => realtime/src}/rooms/types.ts (100%) rename apps/{sim/socket => realtime/src}/routes/http.ts (97%) rename apps/{sim/socket => realtime/src}/tests/socket-server.test.ts (100%) create mode 100644 apps/realtime/tsconfig.json create mode 100644 apps/realtime/vitest.config.ts create mode 100644 apps/realtime/vitest.setup.ts delete mode 100644 apps/sim/lib/workflows/active-context.ts delete mode 100644 apps/sim/socket/handlers/index.ts delete mode 100644 apps/sim/socket/rooms/index.ts create mode 100644 packages/audit/package.json create mode 100644 packages/audit/src/index.ts rename {apps/sim/lib/audit => packages/audit/src}/log.test.ts (93%) rename {apps/sim/lib/audit => packages/audit/src}/log.ts (75%) rename {apps/sim/lib/audit => packages/audit/src}/types.ts (100%) create mode 100644 packages/audit/tsconfig.json create mode 100644 packages/audit/vitest.config.ts create mode 100644 packages/auth/package.json create mode 100644 packages/auth/src/verify.ts create mode 100644 packages/auth/tsconfig.json create mode 100644 packages/realtime-protocol/package.json rename {apps/sim/socket => packages/realtime-protocol/src}/constants.ts (100%) rename {apps/sim/socket/validation => packages/realtime-protocol/src}/schemas.ts (99%) create mode 100644 packages/realtime-protocol/tsconfig.json create mode 100644 packages/security/package.json create mode 100644 packages/security/src/compare.test.ts create mode 100644 packages/security/src/compare.ts create mode 100644 packages/security/src/encryption.test.ts create mode 100644 packages/security/src/encryption.ts create mode 100644 packages/security/src/hash.test.ts create mode 100644 packages/security/src/hash.ts create mode 100644 packages/security/src/hmac.test.ts create mode 100644 packages/security/src/hmac.ts create mode 100644 packages/security/src/tokens.test.ts create mode 100644 packages/security/src/tokens.ts create mode 100644 packages/security/tsconfig.json create mode 100644 packages/security/vitest.config.ts create mode 100644 packages/testing/src/mocks/workflow-authz.mock.ts create mode 100644 packages/workflow-authz/package.json create mode 100644 packages/workflow-authz/src/index.ts create mode 100644 packages/workflow-authz/tsconfig.json create mode 100644 packages/workflow-persistence/package.json create mode 100644 packages/workflow-persistence/src/index.ts create mode 100644 packages/workflow-persistence/src/load.ts create mode 100644 packages/workflow-persistence/src/save.ts rename {apps/sim/lib/workflows => packages/workflow-persistence/src}/subblocks.ts (96%) create mode 100644 packages/workflow-persistence/src/subflow-helpers.ts create mode 100644 packages/workflow-persistence/src/types.ts create mode 100644 packages/workflow-persistence/tsconfig.json create mode 100644 packages/workflow-types/package.json create mode 100644 packages/workflow-types/src/blocks.ts create mode 100644 packages/workflow-types/src/workflow.ts create mode 100644 packages/workflow-types/tsconfig.json create mode 100644 scripts/check-monorepo-boundaries.ts create mode 100644 scripts/check-realtime-prune-graph.ts diff --git a/.claude/rules/sim-testing.md b/.claude/rules/sim-testing.md index 4e281694493..850d0fc7643 100644 --- a/.claude/rules/sim-testing.md +++ b/.claude/rules/sim-testing.md @@ -144,7 +144,7 @@ vi.useFakeTimers() | `@/app/api/auth/oauth/utils` | `authOAuthUtilsMock`, `authOAuthUtilsMockFns` | `vi.mock('@/app/api/auth/oauth/utils', () => authOAuthUtilsMock)` | | `@/app/api/knowledge/utils` | `knowledgeApiUtilsMock`, `knowledgeApiUtilsMockFns` | `vi.mock('@/app/api/knowledge/utils', () => knowledgeApiUtilsMock)` | | `@/app/api/workflows/utils` | `workflowsApiUtilsMock`, `workflowsApiUtilsMockFns` | `vi.mock('@/app/api/workflows/utils', () => workflowsApiUtilsMock)` | -| `@/lib/audit/log` | `auditMock`, `auditMockFns` | `vi.mock('@/lib/audit/log', () => auditMock)` | +| `@sim/audit` | `auditMock`, `auditMockFns` | `vi.mock('@sim/audit', () => auditMock)` | | `@/lib/auth` | `authMock`, `authMockFns` | `vi.mock('@/lib/auth', () => authMock)` | | `@/lib/auth/hybrid` | `hybridAuthMock`, `hybridAuthMockFns` | `vi.mock('@/lib/auth/hybrid', () => hybridAuthMock)` | | `@/lib/copilot/request/http` | `copilotHttpMock`, `copilotHttpMockFns` | `vi.mock('@/lib/copilot/request/http', () => copilotHttpMock)` | diff --git a/.cursor/rules/sim-testing.mdc b/.cursor/rules/sim-testing.mdc index 7e3e4806645..c5e6803e778 100644 --- a/.cursor/rules/sim-testing.mdc +++ b/.cursor/rules/sim-testing.mdc @@ -144,7 +144,7 @@ vi.useFakeTimers() | `@/app/api/auth/oauth/utils` | `authOAuthUtilsMock`, `authOAuthUtilsMockFns` | `vi.mock('@/app/api/auth/oauth/utils', () => authOAuthUtilsMock)` | | `@/app/api/knowledge/utils` | `knowledgeApiUtilsMock`, `knowledgeApiUtilsMockFns` | `vi.mock('@/app/api/knowledge/utils', () => knowledgeApiUtilsMock)` | | `@/app/api/workflows/utils` | `workflowsApiUtilsMock`, `workflowsApiUtilsMockFns` | `vi.mock('@/app/api/workflows/utils', () => workflowsApiUtilsMock)` | -| `@/lib/audit/log` | `auditMock`, `auditMockFns` | `vi.mock('@/lib/audit/log', () => auditMock)` | +| `@sim/audit` | `auditMock`, `auditMockFns` | `vi.mock('@sim/audit', () => auditMock)` | | `@/lib/auth` | `authMock`, `authMockFns` | `vi.mock('@/lib/auth', () => authMock)` | | `@/lib/auth/hybrid` | `hybridAuthMock`, `hybridAuthMockFns` | `vi.mock('@/lib/auth/hybrid', () => hybridAuthMock)` | | `@/lib/copilot/request/http` | `copilotHttpMock`, `copilotHttpMockFns` | `vi.mock('@/lib/copilot/request/http', () => copilotHttpMock)` | diff --git a/.devcontainer/post-create.sh b/.devcontainer/post-create.sh index f77473f3a86..c7ceff427e6 100755 --- a/.devcontainer/post-create.sh +++ b/.devcontainer/post-create.sh @@ -71,7 +71,7 @@ fi # Set up environment variables if .env doesn't exist for the sim app if [ ! -f "apps/sim/.env" ]; then - echo "📄 Creating .env file from template..." + echo "📄 Creating apps/sim/.env from template..." if [ -f "apps/sim/.env.example" ]; then cp apps/sim/.env.example apps/sim/.env else @@ -79,6 +79,18 @@ if [ ! -f "apps/sim/.env" ]; then fi fi +# Set up env for the realtime server (must match the shared values in apps/sim/.env) +if [ ! -f "apps/realtime/.env" ] && [ -f "apps/realtime/.env.example" ]; then + echo "📄 Creating apps/realtime/.env from template..." + cp apps/realtime/.env.example apps/realtime/.env +fi + +# Set up packages/db/.env for drizzle-kit and migration scripts +if [ ! -f "packages/db/.env" ] && [ -f "packages/db/.env.example" ]; then + echo "📄 Creating packages/db/.env from template..." + cp packages/db/.env.example packages/db/.env +fi + # Generate schema and run database migrations echo "🗃️ Running database schema generation and migrations..." echo "Generating schema..." diff --git a/.github/workflows/test-build.yml b/.github/workflows/test-build.yml index c08ab495a2e..8bcd240011c 100644 --- a/.github/workflows/test-build.yml +++ b/.github/workflows/test-build.yml @@ -103,6 +103,15 @@ jobs: - name: Lint code run: bun run lint:check + - name: Enforce monorepo boundaries + run: bun run check:boundaries + + - name: Verify realtime prune graph + run: bun run check:realtime-prune + + - name: Type-check realtime server + run: bunx turbo run type-check --filter=@sim/realtime + - name: Run tests with coverage env: NODE_OPTIONS: '--no-warnings --max-old-space-size=8192' diff --git a/AGENTS.md b/AGENTS.md index 5e21c7e009c..025ad5a3883 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,19 +20,42 @@ You are a professional software engineer. All code must follow best practices: a ### Root Structure ``` -apps/sim/ -├── app/ # Next.js app router (pages, API routes) -├── blocks/ # Block definitions and registry -├── components/ # Shared UI (emcn/, ui/) -├── executor/ # Workflow execution engine -├── hooks/ # Shared hooks (queries/, selectors/) -├── lib/ # App-wide utilities -├── providers/ # LLM provider integrations -├── stores/ # Zustand stores -├── tools/ # Tool definitions -└── triggers/ # Trigger definitions +apps/ +├── sim/ # Next.js app (UI + API routes + workflow editor) +│ ├── app/ # Next.js app router (pages, API routes) +│ ├── blocks/ # Block definitions and registry +│ ├── components/ # Shared UI (emcn/, ui/) +│ ├── executor/ # Workflow execution engine +│ ├── hooks/ # Shared hooks (queries/, selectors/) +│ ├── lib/ # App-wide utilities +│ ├── providers/ # LLM provider integrations +│ ├── stores/ # Zustand stores +│ ├── tools/ # Tool definitions +│ └── triggers/ # Trigger definitions +└── realtime/ # Bun Socket.IO server (collaborative canvas) + └── src/ # auth, config, database, handlers, middleware, + # rooms, routes, internal/webhook-cleanup.ts + +packages/ +├── audit/ # @sim/audit — recordAudit + AuditAction + AuditResourceType +├── auth/ # @sim/auth — @sim/auth/verify (shared Better Auth verifier) +├── db/ # @sim/db — drizzle schema + client +├── logger/ # @sim/logger +├── realtime-protocol/ # @sim/realtime-protocol — socket operation constants + zod schemas +├── security/ # @sim/security — safeCompare +├── tsconfig/ # shared tsconfig presets +├── utils/ # @sim/utils +├── workflow-authz/ # @sim/workflow-authz — authorizeWorkflowByWorkspacePermission +├── workflow-persistence/ # @sim/workflow-persistence — raw load/save + subflow helpers +└── workflow-types/ # @sim/workflow-types — pure BlockState/Loop/Parallel/... types ``` +### Package boundaries +- `apps/* → packages/*` only. Packages never import from `apps/*`. +- Each package has explicit subpath `exports` maps; no barrels that accidentally pull in heavy halves. +- `apps/realtime` intentionally avoids Next.js, React, the block/tool registry, provider SDKs, and the executor. CI enforces this via `scripts/check-monorepo-boundaries.ts` and `scripts/check-realtime-prune-graph.ts`. +- Auth is shared across services via the Better Auth "Shared Database Session" pattern: both apps read the same `BETTER_AUTH_SECRET` and point at the same DB via `@sim/db`. + ### Naming Conventions - Components: PascalCase (`WorkflowList`) - Hooks: `use` prefix (`useWorkflowOperations`) diff --git a/apps/realtime/.env.example b/apps/realtime/.env.example new file mode 100644 index 00000000000..26ef119c01c --- /dev/null +++ b/apps/realtime/.env.example @@ -0,0 +1,30 @@ +# Environment variables required by the @sim/realtime (Socket.IO) server. +# These MUST match the corresponding values in apps/sim/.env for auth to work. +# See apps/realtime/src/env.ts for the full zod schema. + +# Core +NODE_ENV=development +PORT=3002 + +# Database — must point at the same Postgres as the main app +DATABASE_URL=postgresql://postgres:postgres@localhost:5432/simstudio + +# Auth — shared with apps/sim (Better Auth "Shared Database Session" pattern) +BETTER_AUTH_URL=http://localhost:3000 +BETTER_AUTH_SECRET=your_better_auth_secret_min_32_chars + +# Internal RPC — shared with apps/sim +INTERNAL_API_SECRET=your_internal_api_secret_min_32_chars + +# Public app URL — used for CORS allow-list and base URL resolution +NEXT_PUBLIC_APP_URL=http://localhost:3000 + +# Optional: Redis for cross-pod room management +# Leave unset for single-pod / in-memory rooms +# REDIS_URL=redis://localhost:6379 + +# Optional: extra Socket.IO CORS allow-list (comma-separated) +# ALLOWED_ORIGINS=https://embed.example.com,https://admin.example.com + +# Optional: disable auth entirely for trusted private networks +# DISABLE_AUTH=true diff --git a/apps/realtime/package.json b/apps/realtime/package.json new file mode 100644 index 00000000000..8451a633a35 --- /dev/null +++ b/apps/realtime/package.json @@ -0,0 +1,48 @@ +{ + "name": "@sim/realtime", + "version": "0.1.0", + "private": true, + "license": "Apache-2.0", + "type": "module", + "engines": { + "bun": ">=1.2.13", + "node": ">=20.0.0" + }, + "scripts": { + "dev": "bun --watch src/index.ts", + "start": "bun src/index.ts", + "type-check": "tsc --noEmit", + "lint": "biome check --write --unsafe .", + "lint:check": "biome check .", + "format": "biome format --write .", + "format:check": "biome format .", + "test": "vitest run", + "test:watch": "vitest" + }, + "dependencies": { + "@sim/audit": "workspace:*", + "@sim/auth": "workspace:*", + "@sim/db": "workspace:*", + "@sim/logger": "workspace:*", + "@sim/realtime-protocol": "workspace:*", + "@sim/security": "workspace:*", + "@sim/utils": "workspace:*", + "@sim/workflow-authz": "workspace:*", + "@sim/workflow-persistence": "workspace:*", + "@sim/workflow-types": "workspace:*", + "@socket.io/redis-adapter": "8.3.0", + "drizzle-orm": "^0.45.2", + "postgres": "^3.4.5", + "redis": "5.10.0", + "socket.io": "^4.8.1", + "zod": "^3.24.2" + }, + "devDependencies": { + "@sim/testing": "workspace:*", + "@sim/tsconfig": "workspace:*", + "@types/node": "24.2.1", + "socket.io-client": "4.8.1", + "typescript": "^5.7.3", + "vitest": "^3.0.8" + } +} diff --git a/apps/realtime/src/auth.ts b/apps/realtime/src/auth.ts new file mode 100644 index 00000000000..40491a62af7 --- /dev/null +++ b/apps/realtime/src/auth.ts @@ -0,0 +1,17 @@ +import { createVerifyAuth } from '@sim/auth/verify' +import { env } from '@/env' + +export const ANONYMOUS_USER_ID = '00000000-0000-0000-0000-000000000000' + +export const ANONYMOUS_USER = { + id: ANONYMOUS_USER_ID, + name: 'Anonymous', + email: 'anonymous@localhost', + emailVerified: true, + image: null, +} as const + +export const auth = createVerifyAuth({ + secret: env.BETTER_AUTH_SECRET, + baseURL: env.BETTER_AUTH_URL, +}) diff --git a/apps/sim/socket/config/socket.ts b/apps/realtime/src/config/socket.ts similarity index 96% rename from apps/sim/socket/config/socket.ts rename to apps/realtime/src/config/socket.ts index 366558536ba..3e6c50ecbe9 100644 --- a/apps/sim/socket/config/socket.ts +++ b/apps/realtime/src/config/socket.ts @@ -3,9 +3,7 @@ import { createLogger } from '@sim/logger' import { createAdapter } from '@socket.io/redis-adapter' import { createClient, type RedisClientType } from 'redis' import { Server } from 'socket.io' -import { env } from '@/lib/core/config/env' -import { isProd } from '@/lib/core/config/feature-flags' -import { getBaseUrl } from '@/lib/core/utils/urls' +import { env, getBaseUrl, isProd } from '@/env' const logger = createLogger('SocketIOConfig') diff --git a/apps/sim/socket/database/operations.ts b/apps/realtime/src/database/operations.ts similarity index 97% rename from apps/sim/socket/database/operations.ts rename to apps/realtime/src/database/operations.ts index 93c117ea1dd..9e2b4d1ddbe 100644 --- a/apps/sim/socket/database/operations.ts +++ b/apps/realtime/src/database/operations.ts @@ -1,15 +1,7 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import * as schema from '@sim/db' -import { webhook, workflow, workflowBlocks, workflowEdges, workflowSubflows } from '@sim/db' +import { workflow, workflowBlocks, workflowEdges, workflowSubflows } from '@sim/db' import { createLogger } from '@sim/logger' -import { and, eq, inArray, isNull, or, sql } from 'drizzle-orm' -import { drizzle } from 'drizzle-orm/postgres-js' -import postgres from 'postgres' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' -import { env } from '@/lib/core/config/env' -import { cleanupExternalWebhook } from '@/lib/webhooks/provider-subscriptions' -import { getActiveWorkflowContext } from '@/lib/workflows/active-context' -import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils' -import { mergeSubBlockValues } from '@/lib/workflows/subblocks' import { BLOCK_OPERATIONS, BLOCKS_OPERATIONS, @@ -19,7 +11,14 @@ import { SUBFLOW_OPERATIONS, VARIABLE_OPERATIONS, WORKFLOW_OPERATIONS, -} from '@/socket/constants' +} from '@sim/realtime-protocol/constants' +import { getActiveWorkflowContext } from '@sim/workflow-authz' +import { loadWorkflowFromNormalizedTablesRaw } from '@sim/workflow-persistence/load' +import { mergeSubBlockValues } from '@sim/workflow-persistence/subblocks' +import { and, eq, inArray, isNull, or, sql } from 'drizzle-orm' +import { drizzle } from 'drizzle-orm/postgres-js' +import postgres from 'postgres' +import { env } from '@/env' const logger = createLogger('SocketDatabase') @@ -182,7 +181,7 @@ export async function getWorkflowState(workflowId: string) { throw new Error(`Workflow ${workflowId} not found`) } - const normalizedData = await loadWorkflowFromNormalizedTables(workflowId) + const normalizedData = await loadWorkflowFromNormalizedTablesRaw(workflowId) if (normalizedData) { const finalState = { @@ -915,30 +914,10 @@ async function handleBlocksOperationTx( } } - // Clean up external webhooks - const webhooksToCleanup = await tx - .select({ - webhook: webhook, - workflow: { - id: workflow.id, - userId: workflow.userId, - workspaceId: workflow.workspaceId, - }, - }) - .from(webhook) - .innerJoin(workflow, eq(webhook.workflowId, workflow.id)) - .where(and(eq(webhook.workflowId, workflowId), inArray(webhook.blockId, blockIdsArray))) - - if (webhooksToCleanup.length > 0) { - const requestId = `socket-batch-${workflowId}-${Date.now()}` - for (const { webhook: wh, workflow: wf } of webhooksToCleanup) { - try { - await cleanupExternalWebhook(wh, wf, requestId) - } catch (error) { - logger.error(`Failed to cleanup webhook ${wh.id}:`, error) - } - } - } + // Webhook rows are only created at deploy time (saveTriggerWebhooksForDeploy in + // lib/webhooks/deploy.ts) with deploymentVersionId set; their external-subscription + // lifecycle is managed by deploy.ts, lifecycle.ts, and the /api/webhooks/[id] route. + // Removing a trigger block from the draft canvas does not touch any webhook rows. // Delete edges connected to any of the blocks await tx diff --git a/apps/realtime/src/env.ts b/apps/realtime/src/env.ts new file mode 100644 index 00000000000..f08ff5ddd29 --- /dev/null +++ b/apps/realtime/src/env.ts @@ -0,0 +1,44 @@ +import { z } from 'zod' + +const EnvSchema = z.object({ + NODE_ENV: z.enum(['development', 'test', 'production']).default('development'), + DATABASE_URL: z.string().url(), + REDIS_URL: z.string().url().optional(), + BETTER_AUTH_URL: z.string().url(), + BETTER_AUTH_SECRET: z.string().min(32), + INTERNAL_API_SECRET: z.string().min(32), + NEXT_PUBLIC_APP_URL: z.string().url(), + ALLOWED_ORIGINS: z.string().optional(), + PORT: z.coerce.number().int().positive().default(3002), + DISABLE_AUTH: z + .string() + .optional() + .transform((value) => value === 'true' || value === '1'), +}) + +function parseEnv() { + const parsed = EnvSchema.safeParse(process.env) + if (!parsed.success) { + const formatted = parsed.error.format() + throw new Error(`Invalid realtime server environment: ${JSON.stringify(formatted, null, 2)}`) + } + return parsed.data +} + +export const env = parseEnv() + +export const isProd = env.NODE_ENV === 'production' +export const isDev = env.NODE_ENV === 'development' +export const isTest = env.NODE_ENV === 'test' + +let appHostname = '' +try { + appHostname = new URL(env.NEXT_PUBLIC_APP_URL).hostname +} catch {} +export const isHosted = appHostname === 'sim.ai' || appHostname.endsWith('.sim.ai') + +export const isAuthDisabled = env.DISABLE_AUTH === true && !isHosted + +export function getBaseUrl(): string { + return env.NEXT_PUBLIC_APP_URL +} diff --git a/apps/sim/socket/handlers/connection.ts b/apps/realtime/src/handlers/connection.ts similarity index 80% rename from apps/sim/socket/handlers/connection.ts rename to apps/realtime/src/handlers/connection.ts index ee7a9a77434..90eddb82464 100644 --- a/apps/sim/socket/handlers/connection.ts +++ b/apps/realtime/src/handlers/connection.ts @@ -1,8 +1,8 @@ import { createLogger } from '@sim/logger' -import { cleanupPendingSubblocksForSocket } from '@/socket/handlers/subblocks' -import { cleanupPendingVariablesForSocket } from '@/socket/handlers/variables' -import type { AuthenticatedSocket } from '@/socket/middleware/auth' -import type { IRoomManager } from '@/socket/rooms' +import { cleanupPendingSubblocksForSocket } from '@/handlers/subblocks' +import { cleanupPendingVariablesForSocket } from '@/handlers/variables' +import type { AuthenticatedSocket } from '@/middleware/auth' +import type { IRoomManager } from '@/rooms' const logger = createLogger('ConnectionHandlers') diff --git a/apps/realtime/src/handlers/index.ts b/apps/realtime/src/handlers/index.ts new file mode 100644 index 00000000000..6ded2e54741 --- /dev/null +++ b/apps/realtime/src/handlers/index.ts @@ -0,0 +1,17 @@ +import { setupConnectionHandlers } from '@/handlers/connection' +import { setupOperationsHandlers } from '@/handlers/operations' +import { setupPresenceHandlers } from '@/handlers/presence' +import { setupSubblocksHandlers } from '@/handlers/subblocks' +import { setupVariablesHandlers } from '@/handlers/variables' +import { setupWorkflowHandlers } from '@/handlers/workflow' +import type { AuthenticatedSocket } from '@/middleware/auth' +import type { IRoomManager } from '@/rooms' + +export function setupAllHandlers(socket: AuthenticatedSocket, roomManager: IRoomManager) { + setupWorkflowHandlers(socket, roomManager) + setupOperationsHandlers(socket, roomManager) + setupSubblocksHandlers(socket, roomManager) + setupVariablesHandlers(socket, roomManager) + setupPresenceHandlers(socket, roomManager) + setupConnectionHandlers(socket, roomManager) +} diff --git a/apps/sim/socket/handlers/operations.ts b/apps/realtime/src/handlers/operations.ts similarity index 98% rename from apps/sim/socket/handlers/operations.ts rename to apps/realtime/src/handlers/operations.ts index c47c380e896..6635d157b3a 100644 --- a/apps/sim/socket/handlers/operations.ts +++ b/apps/realtime/src/handlers/operations.ts @@ -1,6 +1,4 @@ import { createLogger } from '@sim/logger' -import { generateId } from '@sim/utils/id' -import { ZodError } from 'zod' import { BLOCK_OPERATIONS, BLOCKS_OPERATIONS, @@ -9,12 +7,14 @@ import { VARIABLE_OPERATIONS, type VariableOperation, WORKFLOW_OPERATIONS, -} from '@/socket/constants' -import { persistWorkflowOperation } from '@/socket/database/operations' -import type { AuthenticatedSocket } from '@/socket/middleware/auth' -import { checkRolePermission } from '@/socket/middleware/permissions' -import type { IRoomManager, UserSession } from '@/socket/rooms' -import { WorkflowOperationSchema } from '@/socket/validation/schemas' +} from '@sim/realtime-protocol/constants' +import { WorkflowOperationSchema } from '@sim/realtime-protocol/schemas' +import { generateId } from '@sim/utils/id' +import { ZodError } from 'zod' +import { persistWorkflowOperation } from '@/database/operations' +import type { AuthenticatedSocket } from '@/middleware/auth' +import { checkRolePermission } from '@/middleware/permissions' +import type { IRoomManager, UserSession } from '@/rooms' const logger = createLogger('OperationsHandlers') diff --git a/apps/sim/socket/handlers/presence.ts b/apps/realtime/src/handlers/presence.ts similarity index 93% rename from apps/sim/socket/handlers/presence.ts rename to apps/realtime/src/handlers/presence.ts index 208183d2c5c..13aadc22f34 100644 --- a/apps/sim/socket/handlers/presence.ts +++ b/apps/realtime/src/handlers/presence.ts @@ -1,6 +1,6 @@ import { createLogger } from '@sim/logger' -import type { AuthenticatedSocket } from '@/socket/middleware/auth' -import type { IRoomManager } from '@/socket/rooms' +import type { AuthenticatedSocket } from '@/middleware/auth' +import type { IRoomManager } from '@/rooms' const logger = createLogger('PresenceHandlers') diff --git a/apps/sim/socket/handlers/subblocks.ts b/apps/realtime/src/handlers/subblocks.ts similarity index 97% rename from apps/sim/socket/handlers/subblocks.ts rename to apps/realtime/src/handlers/subblocks.ts index 997f8416c7b..e71792ca685 100644 --- a/apps/sim/socket/handlers/subblocks.ts +++ b/apps/realtime/src/handlers/subblocks.ts @@ -1,11 +1,11 @@ import { db } from '@sim/db' import { workflow, workflowBlocks } from '@sim/db/schema' import { createLogger } from '@sim/logger' +import { SUBBLOCK_OPERATIONS } from '@sim/realtime-protocol/constants' import { and, eq } from 'drizzle-orm' -import { SUBBLOCK_OPERATIONS } from '@/socket/constants' -import type { AuthenticatedSocket } from '@/socket/middleware/auth' -import { checkRolePermission } from '@/socket/middleware/permissions' -import type { IRoomManager } from '@/socket/rooms' +import type { AuthenticatedSocket } from '@/middleware/auth' +import { checkRolePermission } from '@/middleware/permissions' +import type { IRoomManager } from '@/rooms' const logger = createLogger('SubblocksHandlers') diff --git a/apps/sim/socket/handlers/variables.ts b/apps/realtime/src/handlers/variables.ts similarity index 97% rename from apps/sim/socket/handlers/variables.ts rename to apps/realtime/src/handlers/variables.ts index bf5114c8390..b67570674aa 100644 --- a/apps/sim/socket/handlers/variables.ts +++ b/apps/realtime/src/handlers/variables.ts @@ -1,11 +1,11 @@ import { db } from '@sim/db' import { workflow } from '@sim/db/schema' import { createLogger } from '@sim/logger' +import { VARIABLE_OPERATIONS } from '@sim/realtime-protocol/constants' import { eq } from 'drizzle-orm' -import { VARIABLE_OPERATIONS } from '@/socket/constants' -import type { AuthenticatedSocket } from '@/socket/middleware/auth' -import { checkRolePermission } from '@/socket/middleware/permissions' -import type { IRoomManager } from '@/socket/rooms' +import type { AuthenticatedSocket } from '@/middleware/auth' +import { checkRolePermission } from '@/middleware/permissions' +import type { IRoomManager } from '@/rooms' const logger = createLogger('VariablesHandlers') diff --git a/apps/sim/socket/handlers/workflow.test.ts b/apps/realtime/src/handlers/workflow.test.ts similarity index 96% rename from apps/sim/socket/handlers/workflow.test.ts rename to apps/realtime/src/handlers/workflow.test.ts index ac65399faf2..9dd82db87c4 100644 --- a/apps/sim/socket/handlers/workflow.test.ts +++ b/apps/realtime/src/handlers/workflow.test.ts @@ -2,7 +2,7 @@ * @vitest-environment node */ import { beforeEach, describe, expect, it, vi } from 'vitest' -import type { IRoomManager } from '@/socket/rooms' +import type { IRoomManager } from '@/rooms' const { mockGetWorkflowState, mockVerifyWorkflowAccess } = vi.hoisted(() => ({ mockGetWorkflowState: vi.fn(), @@ -14,15 +14,15 @@ vi.mock('@sim/db', () => ({ user: { image: 'image' }, })) -vi.mock('@/socket/database/operations', () => ({ +vi.mock('@/database/operations', () => ({ getWorkflowState: mockGetWorkflowState, })) -vi.mock('@/socket/middleware/permissions', () => ({ +vi.mock('@/middleware/permissions', () => ({ verifyWorkflowAccess: mockVerifyWorkflowAccess, })) -import { setupWorkflowHandlers } from '@/socket/handlers/workflow' +import { setupWorkflowHandlers } from '@/handlers/workflow' interface JoinWorkflowPayload { workflowId: string diff --git a/apps/sim/socket/handlers/workflow.ts b/apps/realtime/src/handlers/workflow.ts similarity index 96% rename from apps/sim/socket/handlers/workflow.ts rename to apps/realtime/src/handlers/workflow.ts index 8796f2a3190..da977fcdb5e 100644 --- a/apps/sim/socket/handlers/workflow.ts +++ b/apps/realtime/src/handlers/workflow.ts @@ -1,10 +1,10 @@ import { db, user } from '@sim/db' import { createLogger } from '@sim/logger' import { eq } from 'drizzle-orm' -import { getWorkflowState } from '@/socket/database/operations' -import type { AuthenticatedSocket } from '@/socket/middleware/auth' -import { verifyWorkflowAccess } from '@/socket/middleware/permissions' -import type { IRoomManager, UserPresence } from '@/socket/rooms' +import { getWorkflowState } from '@/database/operations' +import type { AuthenticatedSocket } from '@/middleware/auth' +import { verifyWorkflowAccess } from '@/middleware/permissions' +import type { IRoomManager, UserPresence } from '@/rooms' const logger = createLogger('WorkflowHandlers') diff --git a/apps/sim/socket/index.test.ts b/apps/realtime/src/index.test.ts similarity index 85% rename from apps/sim/socket/index.test.ts rename to apps/realtime/src/index.test.ts index e50f4e97c22..c00592e0d6d 100644 --- a/apps/sim/socket/index.test.ts +++ b/apps/realtime/src/index.test.ts @@ -4,21 +4,28 @@ * @vitest-environment node */ import { createServer, request as httpRequest } from 'http' -import { createEnvMock, createMockLogger } from '@sim/testing' +import { createMockLogger } from '@sim/testing' import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' -import { createSocketIOServer } from '@/socket/config/socket' -import { MemoryRoomManager } from '@/socket/rooms' -import { createHttpHandler } from '@/socket/routes/http' +import { createSocketIOServer } from '@/config/socket' +import { MemoryRoomManager } from '@/rooms' +import { createHttpHandler } from '@/routes/http' -vi.mock('@/lib/auth', () => ({ +vi.mock('@/auth', () => ({ auth: { api: { verifyOneTimeToken: vi.fn(), }, }, + ANONYMOUS_USER_ID: '00000000-0000-0000-0000-000000000000', + ANONYMOUS_USER: { + id: '00000000-0000-0000-0000-000000000000', + name: 'Anonymous', + email: 'anonymous@localhost', + emailVerified: true, + image: null, + }, })) -// Mock redis package to prevent actual Redis connections vi.mock('redis', () => ({ createClient: vi.fn(() => ({ on: vi.fn(), @@ -28,15 +35,27 @@ vi.mock('redis', () => ({ })), })) -vi.mock('@/lib/core/config/env', () => - createEnvMock({ +vi.mock('@/env', () => ({ + env: { DATABASE_URL: 'postgres://localhost/test', NODE_ENV: 'test', REDIS_URL: undefined, - }) -) + BETTER_AUTH_URL: 'http://localhost:3000', + BETTER_AUTH_SECRET: 'test-better-auth-secret-at-least-32-chars', + INTERNAL_API_SECRET: 'test-internal-api-secret-at-least-32-chars', + NEXT_PUBLIC_APP_URL: 'http://localhost:3000', + PORT: 3002, + DISABLE_AUTH: false, + }, + isProd: false, + isDev: false, + isTest: true, + isHosted: false, + isAuthDisabled: false, + getBaseUrl: () => 'http://localhost:3000', +})) -vi.mock('@/socket/middleware/auth', () => ({ +vi.mock('@/middleware/auth', () => ({ authenticateSocket: vi.fn((socket, next) => { socket.userId = 'test-user-id' socket.userName = 'Test User' @@ -45,7 +64,7 @@ vi.mock('@/socket/middleware/auth', () => ({ }), })) -vi.mock('@/socket/middleware/permissions', () => ({ +vi.mock('@/middleware/permissions', () => ({ verifyWorkflowAccess: vi.fn().mockResolvedValue({ hasAccess: true, role: 'admin', @@ -55,7 +74,7 @@ vi.mock('@/socket/middleware/permissions', () => ({ }), })) -vi.mock('@/socket/database/operations', () => ({ +vi.mock('@/database/operations', () => ({ getWorkflowState: vi.fn().mockResolvedValue({ id: 'test-workflow', name: 'Test Workflow', @@ -275,13 +294,13 @@ describe('Socket Server Index Integration', () => { describe('Module Integration', () => { it.concurrent('should properly import all extracted modules', async () => { - const { createSocketIOServer } = await import('@/socket/config/socket') - const { createHttpHandler } = await import('@/socket/routes/http') - const { MemoryRoomManager, RedisRoomManager } = await import('@/socket/rooms') - const { authenticateSocket } = await import('@/socket/middleware/auth') - const { verifyWorkflowAccess } = await import('@/socket/middleware/permissions') - const { getWorkflowState } = await import('@/socket/database/operations') - const { WorkflowOperationSchema } = await import('@/socket/validation/schemas') + const { createSocketIOServer } = await import('@/config/socket') + const { createHttpHandler } = await import('@/routes/http') + const { MemoryRoomManager, RedisRoomManager } = await import('@/rooms') + const { authenticateSocket } = await import('@/middleware/auth') + const { verifyWorkflowAccess } = await import('@/middleware/permissions') + const { getWorkflowState } = await import('@/database/operations') + const { WorkflowOperationSchema } = await import('@sim/realtime-protocol/schemas') expect(createSocketIOServer).toBeTypeOf('function') expect(createHttpHandler).toBeTypeOf('function') @@ -332,7 +351,7 @@ describe('Socket Server Index Integration', () => { describe('Validation and Utils', () => { it.concurrent('should validate workflow operations', async () => { - const { WorkflowOperationSchema } = await import('@/socket/validation/schemas') + const { WorkflowOperationSchema } = await import('@sim/realtime-protocol/schemas') const validOperation = { operation: 'batch-add-blocks', @@ -358,7 +377,7 @@ describe('Socket Server Index Integration', () => { }) it.concurrent('should validate batch-add-blocks with edges', async () => { - const { WorkflowOperationSchema } = await import('@/socket/validation/schemas') + const { WorkflowOperationSchema } = await import('@sim/realtime-protocol/schemas') const validOperationWithEdge = { operation: 'batch-add-blocks', @@ -393,7 +412,7 @@ describe('Socket Server Index Integration', () => { }) it.concurrent('should validate edge operations', async () => { - const { WorkflowOperationSchema } = await import('@/socket/validation/schemas') + const { WorkflowOperationSchema } = await import('@sim/realtime-protocol/schemas') const validEdgeOperation = { operation: 'add', @@ -410,7 +429,7 @@ describe('Socket Server Index Integration', () => { }) it('should validate subflow operations', async () => { - const { WorkflowOperationSchema } = await import('@/socket/validation/schemas') + const { WorkflowOperationSchema } = await import('@sim/realtime-protocol/schemas') const validSubflowOperation = { operation: 'update', diff --git a/apps/sim/socket/index.ts b/apps/realtime/src/index.ts similarity index 92% rename from apps/sim/socket/index.ts rename to apps/realtime/src/index.ts index adca5a9c373..dde8e67e442 100644 --- a/apps/sim/socket/index.ts +++ b/apps/realtime/src/index.ts @@ -1,12 +1,12 @@ import { createServer } from 'http' import { createLogger } from '@sim/logger' import type { Server as SocketIOServer } from 'socket.io' -import { env } from '@/lib/core/config/env' -import { createSocketIOServer, shutdownSocketIOAdapter } from '@/socket/config/socket' -import { setupAllHandlers } from '@/socket/handlers' -import { type AuthenticatedSocket, authenticateSocket } from '@/socket/middleware/auth' -import { type IRoomManager, MemoryRoomManager, RedisRoomManager } from '@/socket/rooms' -import { createHttpHandler } from '@/socket/routes/http' +import { createSocketIOServer, shutdownSocketIOAdapter } from '@/config/socket' +import { env } from '@/env' +import { setupAllHandlers } from '@/handlers' +import { type AuthenticatedSocket, authenticateSocket } from '@/middleware/auth' +import { type IRoomManager, MemoryRoomManager, RedisRoomManager } from '@/rooms' +import { createHttpHandler } from '@/routes/http' const logger = createLogger('CollaborativeSocketServer') @@ -29,7 +29,7 @@ async function createRoomManager(io: SocketIOServer): Promise { async function main() { const httpServer = createServer() - const PORT = Number(env.PORT || env.SOCKET_PORT || 3002) + const PORT = env.PORT logger.info('Starting Socket.IO server...', { port: PORT, diff --git a/apps/sim/socket/middleware/auth.ts b/apps/realtime/src/middleware/auth.ts similarity index 94% rename from apps/sim/socket/middleware/auth.ts rename to apps/realtime/src/middleware/auth.ts index a9daddcfc3f..91cdb71b4b6 100644 --- a/apps/sim/socket/middleware/auth.ts +++ b/apps/realtime/src/middleware/auth.ts @@ -1,9 +1,8 @@ import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' import type { Socket } from 'socket.io' -import { auth } from '@/lib/auth' -import { ANONYMOUS_USER, ANONYMOUS_USER_ID } from '@/lib/auth/constants' -import { isAuthDisabled } from '@/lib/core/config/feature-flags' +import { ANONYMOUS_USER, ANONYMOUS_USER_ID, auth } from '@/auth' +import { isAuthDisabled } from '@/env' const logger = createLogger('SocketAuth') diff --git a/apps/sim/socket/middleware/permissions.test.ts b/apps/realtime/src/middleware/permissions.test.ts similarity index 99% rename from apps/sim/socket/middleware/permissions.test.ts rename to apps/realtime/src/middleware/permissions.test.ts index 784d4ea7ffe..2d8cd12999c 100644 --- a/apps/sim/socket/middleware/permissions.test.ts +++ b/apps/realtime/src/middleware/permissions.test.ts @@ -14,7 +14,7 @@ import { SOCKET_OPERATIONS, } from '@sim/testing' import { describe, expect, it } from 'vitest' -import { checkRolePermission } from '@/socket/middleware/permissions' +import { checkRolePermission } from '@/middleware/permissions' describe('checkRolePermission', () => { describe('admin role', () => { diff --git a/apps/sim/socket/middleware/permissions.ts b/apps/realtime/src/middleware/permissions.ts similarity index 97% rename from apps/sim/socket/middleware/permissions.ts rename to apps/realtime/src/middleware/permissions.ts index 244cf1b07b5..dcc893b1478 100644 --- a/apps/sim/socket/middleware/permissions.ts +++ b/apps/realtime/src/middleware/permissions.ts @@ -1,8 +1,6 @@ import { db } from '@sim/db' import { workflow } from '@sim/db/schema' import { createLogger } from '@sim/logger' -import { and, eq, isNull } from 'drizzle-orm' -import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils' import { BLOCK_OPERATIONS, BLOCKS_OPERATIONS, @@ -12,7 +10,9 @@ import { SUBFLOW_OPERATIONS, VARIABLE_OPERATIONS, WORKFLOW_OPERATIONS, -} from '@/socket/constants' +} from '@sim/realtime-protocol/constants' +import { authorizeWorkflowByWorkspacePermission } from '@sim/workflow-authz' +import { and, eq, isNull } from 'drizzle-orm' const logger = createLogger('SocketPermissions') diff --git a/apps/realtime/src/rooms/index.ts b/apps/realtime/src/rooms/index.ts new file mode 100644 index 00000000000..8067fc1b215 --- /dev/null +++ b/apps/realtime/src/rooms/index.ts @@ -0,0 +1,3 @@ +export { MemoryRoomManager } from '@/rooms/memory-manager' +export { RedisRoomManager } from '@/rooms/redis-manager' +export type { IRoomManager, UserPresence, UserSession, WorkflowRoom } from '@/rooms/types' diff --git a/apps/sim/socket/rooms/memory-manager.ts b/apps/realtime/src/rooms/memory-manager.ts similarity index 99% rename from apps/sim/socket/rooms/memory-manager.ts rename to apps/realtime/src/rooms/memory-manager.ts index 1e14c9c7df1..a032e785bb5 100644 --- a/apps/sim/socket/rooms/memory-manager.ts +++ b/apps/realtime/src/rooms/memory-manager.ts @@ -1,6 +1,6 @@ import { createLogger } from '@sim/logger' import type { Server } from 'socket.io' -import type { IRoomManager, UserPresence, UserSession, WorkflowRoom } from '@/socket/rooms/types' +import type { IRoomManager, UserPresence, UserSession, WorkflowRoom } from '@/rooms/types' const logger = createLogger('MemoryRoomManager') diff --git a/apps/sim/socket/rooms/redis-manager.ts b/apps/realtime/src/rooms/redis-manager.ts similarity index 99% rename from apps/sim/socket/rooms/redis-manager.ts rename to apps/realtime/src/rooms/redis-manager.ts index adfe6720485..0e6b3eadf2b 100644 --- a/apps/sim/socket/rooms/redis-manager.ts +++ b/apps/realtime/src/rooms/redis-manager.ts @@ -1,7 +1,7 @@ import { createLogger } from '@sim/logger' import { createClient, type RedisClientType } from 'redis' import type { Server } from 'socket.io' -import type { IRoomManager, UserPresence, UserSession } from '@/socket/rooms/types' +import type { IRoomManager, UserPresence, UserSession } from '@/rooms/types' const logger = createLogger('RedisRoomManager') diff --git a/apps/sim/socket/rooms/types.ts b/apps/realtime/src/rooms/types.ts similarity index 100% rename from apps/sim/socket/rooms/types.ts rename to apps/realtime/src/rooms/types.ts diff --git a/apps/sim/socket/routes/http.ts b/apps/realtime/src/routes/http.ts similarity index 97% rename from apps/sim/socket/routes/http.ts rename to apps/realtime/src/routes/http.ts index 5c555e92843..0f8ed73cc52 100644 --- a/apps/sim/socket/routes/http.ts +++ b/apps/realtime/src/routes/http.ts @@ -1,7 +1,7 @@ import type { IncomingMessage, ServerResponse } from 'http' -import { env } from '@/lib/core/config/env' -import { safeCompare } from '@/lib/core/security/encryption' -import type { IRoomManager } from '@/socket/rooms' +import { safeCompare } from '@sim/security/compare' +import { env } from '@/env' +import type { IRoomManager } from '@/rooms' interface Logger { info: (message: string, ...args: unknown[]) => void diff --git a/apps/sim/socket/tests/socket-server.test.ts b/apps/realtime/src/tests/socket-server.test.ts similarity index 100% rename from apps/sim/socket/tests/socket-server.test.ts rename to apps/realtime/src/tests/socket-server.test.ts diff --git a/apps/realtime/tsconfig.json b/apps/realtime/tsconfig.json new file mode 100644 index 00000000000..cce62771d4d --- /dev/null +++ b/apps/realtime/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "@sim/tsconfig/base.json", + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + } + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/apps/realtime/vitest.config.ts b/apps/realtime/vitest.config.ts new file mode 100644 index 00000000000..a458a21d8dd --- /dev/null +++ b/apps/realtime/vitest.config.ts @@ -0,0 +1,27 @@ +import path from 'node:path' +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + include: ['**/*.test.{ts,tsx}'], + exclude: ['**/node_modules/**', '**/dist/**'], + setupFiles: ['./vitest.setup.ts'], + pool: 'threads', + testTimeout: 10000, + }, + resolve: { + alias: [ + { + find: '@sim/db', + replacement: path.resolve(__dirname, '../../packages/db'), + }, + { + find: '@sim/logger', + replacement: path.resolve(__dirname, '../../packages/logger/src'), + }, + { find: '@', replacement: path.resolve(__dirname, 'src') }, + ], + }, +}) diff --git a/apps/realtime/vitest.setup.ts b/apps/realtime/vitest.setup.ts new file mode 100644 index 00000000000..132ea305c23 --- /dev/null +++ b/apps/realtime/vitest.setup.ts @@ -0,0 +1,6 @@ +process.env.DATABASE_URL ??= 'postgres://localhost/test' +process.env.NODE_ENV ??= 'test' +process.env.BETTER_AUTH_URL ??= 'http://localhost:3000' +process.env.BETTER_AUTH_SECRET ??= 'test-better-auth-secret-at-least-32-chars' +process.env.INTERNAL_API_SECRET ??= 'test-internal-api-secret-at-least-32-chars' +process.env.NEXT_PUBLIC_APP_URL ??= 'http://localhost:3000' diff --git a/apps/sim/AGENTS.md b/apps/sim/AGENTS.md index 078de88233b..ac75315fffa 100644 --- a/apps/sim/AGENTS.md +++ b/apps/sim/AGENTS.md @@ -26,6 +26,13 @@ apps/sim/ └── triggers/ # Trigger definitions ``` +The Socket.IO collaborative-canvas server lives in a separate workspace at +`apps/realtime/`. It shares DB + auth with `apps/sim` via the `@sim/*` +packages. Do not add imports from `@/lib/webhooks/providers/*`, `@/executor/*`, +`@/blocks/*`, or `@/tools/*` to any package consumed by `apps/realtime` — +those heavyweight registries stay in this app. `apps/realtime` calls back +into this app only over internal HTTP with `INTERNAL_API_SECRET`. + ### Feature Organization Features live under `app/workspace/[workspaceId]/`: diff --git a/apps/sim/app/api/auth/forget-password/route.ts b/apps/sim/app/api/auth/forget-password/route.ts index ef20b11b299..2bf7be8782a 100644 --- a/apps/sim/app/api/auth/forget-password/route.ts +++ b/apps/sim/app/api/auth/forget-password/route.ts @@ -1,10 +1,10 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { user } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { auth } from '@/lib/auth' import { isSameOrigin } from '@/lib/core/utils/validation' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' diff --git a/apps/sim/app/api/auth/oauth/credentials/route.ts b/apps/sim/app/api/auth/oauth/credentials/route.ts index 185caf3e8bf..db2db24fc79 100644 --- a/apps/sim/app/api/auth/oauth/credentials/route.ts +++ b/apps/sim/app/api/auth/oauth/credentials/route.ts @@ -1,6 +1,7 @@ import { db } from '@sim/db' import { account, credential, credentialMember } from '@sim/db/schema' import { createLogger } from '@sim/logger' +import { authorizeWorkflowByWorkspacePermission } from '@sim/workflow-authz' import { and, eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' @@ -12,7 +13,6 @@ import { getCanonicalScopesForProvider, getServiceAccountProviderForProviderId, } from '@/lib/oauth/utils' -import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils' import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils' export const dynamic = 'force-dynamic' diff --git a/apps/sim/app/api/auth/oauth/disconnect/route.test.ts b/apps/sim/app/api/auth/oauth/disconnect/route.test.ts index 13a3b4bc7e9..3681ac5cc36 100644 --- a/apps/sim/app/api/auth/oauth/disconnect/route.test.ts +++ b/apps/sim/app/api/auth/oauth/disconnect/route.test.ts @@ -23,7 +23,7 @@ vi.mock('@/lib/webhooks/utils.server', () => ({ syncAllWebhooksForCredentialSet: mockSyncAllWebhooksForCredentialSet, })) -vi.mock('@/lib/audit/log', () => auditMock) +vi.mock('@sim/audit', () => auditMock) import { POST } from '@/app/api/auth/oauth/disconnect/route' diff --git a/apps/sim/app/api/auth/oauth/disconnect/route.ts b/apps/sim/app/api/auth/oauth/disconnect/route.ts index 7a5372e652e..51767bd482e 100644 --- a/apps/sim/app/api/auth/oauth/disconnect/route.ts +++ b/apps/sim/app/api/auth/oauth/disconnect/route.ts @@ -1,10 +1,10 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { account, credentialSet, credentialSetMember } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { and, eq, like, or } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { getSession } from '@/lib/auth' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' diff --git a/apps/sim/app/api/auth/oauth2/callback/shopify/route.ts b/apps/sim/app/api/auth/oauth2/callback/shopify/route.ts index fcf9e389ee3..c12c52ea4ba 100644 --- a/apps/sim/app/api/auth/oauth2/callback/shopify/route.ts +++ b/apps/sim/app/api/auth/oauth2/callback/shopify/route.ts @@ -1,5 +1,6 @@ -import crypto from 'crypto' import { createLogger } from '@sim/logger' +import { safeCompare } from '@sim/security/compare' +import { hmacSha256Hex } from '@sim/security/hmac' import { type NextRequest, NextResponse } from 'next/server' import { getSession } from '@/lib/auth' import { env } from '@/lib/core/config/env' @@ -34,13 +35,9 @@ function validateHmac(searchParams: URLSearchParams, clientSecret: string): bool .map((key) => `${key}=${params[key]}`) .join('&') - const generatedHmac = crypto.createHmac('sha256', clientSecret).update(message).digest('hex') + const generatedHmac = hmacSha256Hex(message, clientSecret) - try { - return crypto.timingSafeEqual(Buffer.from(hmac, 'hex'), Buffer.from(generatedHmac, 'hex')) - } catch { - return false - } + return safeCompare(hmac, generatedHmac) } export const GET = withRouteHandler(async (request: NextRequest) => { diff --git a/apps/sim/app/api/billing/credits/route.ts b/apps/sim/app/api/billing/credits/route.ts index 5afe089ab27..7f7e5390221 100644 --- a/apps/sim/app/api/billing/credits/route.ts +++ b/apps/sim/app/api/billing/credits/route.ts @@ -1,7 +1,7 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { getSession } from '@/lib/auth' import { getCreditBalance } from '@/lib/billing/credits/balance' import { purchaseCredits } from '@/lib/billing/credits/purchase' diff --git a/apps/sim/app/api/chat/manage/[id]/route.test.ts b/apps/sim/app/api/chat/manage/[id]/route.test.ts index 81c04439e37..32f3a6a8311 100644 --- a/apps/sim/app/api/chat/manage/[id]/route.test.ts +++ b/apps/sim/app/api/chat/manage/[id]/route.test.ts @@ -33,7 +33,7 @@ const mockPerformChatUndeploy = workflowsOrchestrationMockFns.mockPerformChatUnd const mockNotifySocketDeploymentChanged = workflowsOrchestrationMockFns.mockNotifySocketDeploymentChanged -vi.mock('@/lib/audit/log', () => auditMock) +vi.mock('@sim/audit', () => auditMock) vi.mock('@/lib/core/config/feature-flags', () => ({ isDev: true, isHosted: false, diff --git a/apps/sim/app/api/chat/manage/[id]/route.ts b/apps/sim/app/api/chat/manage/[id]/route.ts index 3564aa00a79..4f937d75258 100644 --- a/apps/sim/app/api/chat/manage/[id]/route.ts +++ b/apps/sim/app/api/chat/manage/[id]/route.ts @@ -1,10 +1,10 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { chat } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { and, eq, isNull } from 'drizzle-orm' import type { NextRequest } from 'next/server' import { z } from 'zod' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { getSession } from '@/lib/auth' import { isDev } from '@/lib/core/config/feature-flags' import { encryptSecret } from '@/lib/core/security/encryption' diff --git a/apps/sim/app/api/chat/utils.test.ts b/apps/sim/app/api/chat/utils.test.ts index f4cdb54fb84..31401d6b5ec 100644 --- a/apps/sim/app/api/chat/utils.test.ts +++ b/apps/sim/app/api/chat/utils.test.ts @@ -40,7 +40,7 @@ vi.mock('@/serializer', () => ({ Serializer: vi.fn(), })) -vi.mock('@/lib/workflows/subblocks', () => ({ +vi.mock('@sim/workflow-persistence/subblocks', () => ({ mergeSubblockStateWithValues: mockMergeSubblockStateWithValues, mergeSubBlockValues: mockMergeSubBlockValues, })) diff --git a/apps/sim/app/api/chat/utils.ts b/apps/sim/app/api/chat/utils.ts index 8d1147c1c83..3909dd599fe 100644 --- a/apps/sim/app/api/chat/utils.ts +++ b/apps/sim/app/api/chat/utils.ts @@ -1,6 +1,7 @@ import { db } from '@sim/db' import { chat, workflow } from '@sim/db/schema' import { createLogger } from '@sim/logger' +import { authorizeWorkflowByWorkspacePermission } from '@sim/workflow-authz' import { and, eq, isNull } from 'drizzle-orm' import type { NextRequest, NextResponse } from 'next/server' import { @@ -9,7 +10,6 @@ import { validateAuthToken, } from '@/lib/core/security/deployment' import { decryptSecret } from '@/lib/core/security/encryption' -import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils' const logger = createLogger('ChatAuthUtils') diff --git a/apps/sim/app/api/copilot/chat/queries.ts b/apps/sim/app/api/copilot/chat/queries.ts index 8252c493413..bc475a8bc09 100644 --- a/apps/sim/app/api/copilot/chat/queries.ts +++ b/apps/sim/app/api/copilot/chat/queries.ts @@ -2,6 +2,7 @@ import { db } from '@sim/db' import { copilotChats } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' +import { authorizeWorkflowByWorkspacePermission } from '@sim/workflow-authz' import { and, desc, eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { getLatestRunForStream } from '@/lib/copilot/async-runs/repository' @@ -17,7 +18,6 @@ import { import { readFilePreviewSessions } from '@/lib/copilot/request/session' import { readEvents } from '@/lib/copilot/request/session/buffer' import { toStreamBatchEvent } from '@/lib/copilot/request/session/types' -import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils' import { assertActiveWorkspaceAccess } from '@/lib/workspaces/permissions/utils' const logger = createLogger('CopilotChatAPI') diff --git a/apps/sim/app/api/copilot/chat/stream/route.ts b/apps/sim/app/api/copilot/chat/stream/route.ts index f443fc397b8..45a4c3c9875 100644 --- a/apps/sim/app/api/copilot/chat/stream/route.ts +++ b/apps/sim/app/api/copilot/chat/stream/route.ts @@ -1,5 +1,6 @@ import { context as otelContext, trace } from '@opentelemetry/api' import { createLogger } from '@sim/logger' +import { sleep } from '@sim/utils/helpers' import { type NextRequest, NextResponse } from 'next/server' import { getLatestRunForStream } from '@/lib/copilot/async-runs/repository' import { @@ -407,7 +408,7 @@ async function handleResumeRequestBody({ break } - await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS)) + await sleep(POLL_INTERVAL_MS) } if (!controllerClosed && Date.now() - startTime >= MAX_STREAM_MS) { emitTerminalIfMissing(MothershipStreamV1CompletionStatus.error, { diff --git a/apps/sim/app/api/copilot/chats/route.ts b/apps/sim/app/api/copilot/chats/route.ts index 512cd129bdc..07b6974ed45 100644 --- a/apps/sim/app/api/copilot/chats/route.ts +++ b/apps/sim/app/api/copilot/chats/route.ts @@ -1,6 +1,7 @@ import { db } from '@sim/db' import { copilotChats, permissions, workflow, workspace } from '@sim/db/schema' import { createLogger } from '@sim/logger' +import { authorizeWorkflowByWorkspacePermission } from '@sim/workflow-authz' import { and, desc, eq, isNull, or, sql } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' @@ -13,7 +14,6 @@ import { } from '@/lib/copilot/request/http' import { taskPubSub } from '@/lib/copilot/tasks' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' -import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils' import { assertActiveWorkspaceAccess } from '@/lib/workspaces/permissions/utils' const logger = createLogger('CopilotChatsListAPI') diff --git a/apps/sim/app/api/copilot/checkpoints/revert/route.test.ts b/apps/sim/app/api/copilot/checkpoints/revert/route.test.ts index 05520d59841..0730fe748fb 100644 --- a/apps/sim/app/api/copilot/checkpoints/revert/route.test.ts +++ b/apps/sim/app/api/copilot/checkpoints/revert/route.test.ts @@ -3,7 +3,7 @@ * * @vitest-environment node */ -import { authMockFns, workflowsUtilsMock, workflowsUtilsMockFns } from '@sim/testing' +import { authMockFns, workflowAuthzMockFns, workflowsUtilsMock } from '@sim/testing' import { NextRequest } from 'next/server' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' @@ -63,7 +63,7 @@ describe('Copilot Checkpoints Revert API Route', () => { authMockFns.mockGetSession.mockResolvedValue(null) - workflowsUtilsMockFns.mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValue({ + workflowAuthzMockFns.mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValue({ allowed: true, status: 200, }) @@ -251,7 +251,7 @@ describe('Copilot Checkpoints Revert API Route', () => { thenResults.push(mockCheckpoint) // Checkpoint found thenResults.push(mockWorkflow) // Workflow found but different user - workflowsUtilsMockFns.mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValueOnce({ + workflowAuthzMockFns.mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValueOnce({ allowed: false, status: 403, }) diff --git a/apps/sim/app/api/copilot/checkpoints/revert/route.ts b/apps/sim/app/api/copilot/checkpoints/revert/route.ts index 01661bf4e3b..b5c050d13d7 100644 --- a/apps/sim/app/api/copilot/checkpoints/revert/route.ts +++ b/apps/sim/app/api/copilot/checkpoints/revert/route.ts @@ -1,6 +1,7 @@ import { db } from '@sim/db' import { workflowCheckpoints, workflow as workflowTable } from '@sim/db/schema' import { createLogger } from '@sim/logger' +import { authorizeWorkflowByWorkspacePermission } from '@sim/workflow-authz' import { and, eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' @@ -14,7 +15,6 @@ import { } from '@/lib/copilot/request/http' import { getInternalApiBaseUrl } from '@/lib/core/utils/urls' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' -import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils' import { isUuidV4 } from '@/executor/constants' const logger = createLogger('CheckpointRevertAPI') diff --git a/apps/sim/app/api/copilot/checkpoints/route.test.ts b/apps/sim/app/api/copilot/checkpoints/route.test.ts index e73b6ed0ca9..e3da1f258f4 100644 --- a/apps/sim/app/api/copilot/checkpoints/route.test.ts +++ b/apps/sim/app/api/copilot/checkpoints/route.test.ts @@ -3,7 +3,7 @@ * * @vitest-environment node */ -import { authMockFns, workflowsUtilsMock, workflowsUtilsMockFns } from '@sim/testing' +import { authMockFns, workflowAuthzMockFns, workflowsUtilsMock } from '@sim/testing' import { NextRequest } from 'next/server' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' @@ -79,7 +79,7 @@ describe('Copilot Checkpoints API Route', () => { userId: 'user-123', workflowId: 'workflow-123', }) - workflowsUtilsMockFns.mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValue({ + workflowAuthzMockFns.mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValue({ allowed: true, }) }) diff --git a/apps/sim/app/api/copilot/checkpoints/route.ts b/apps/sim/app/api/copilot/checkpoints/route.ts index b80fad00a08..0e00dbf1c3a 100644 --- a/apps/sim/app/api/copilot/checkpoints/route.ts +++ b/apps/sim/app/api/copilot/checkpoints/route.ts @@ -1,6 +1,7 @@ import { db } from '@sim/db' import { workflowCheckpoints } from '@sim/db/schema' import { createLogger } from '@sim/logger' +import { authorizeWorkflowByWorkspacePermission } from '@sim/workflow-authz' import { and, desc, eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' @@ -13,7 +14,6 @@ import { createUnauthorizedResponse, } from '@/lib/copilot/request/http' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' -import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils' const logger = createLogger('WorkflowCheckpointsAPI') diff --git a/apps/sim/app/api/credential-sets/[id]/invite/[invitationId]/route.ts b/apps/sim/app/api/credential-sets/[id]/invite/[invitationId]/route.ts index de1a54ae185..287a78b0d23 100644 --- a/apps/sim/app/api/credential-sets/[id]/invite/[invitationId]/route.ts +++ b/apps/sim/app/api/credential-sets/[id]/invite/[invitationId]/route.ts @@ -1,10 +1,10 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { credentialSet, credentialSetInvitation, member, organization, user } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { and, eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { getEmailSubject, renderPollingGroupInvitationEmail } from '@/components/emails' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { getSession } from '@/lib/auth' import { hasCredentialSetsAccess } from '@/lib/billing' import { getBaseUrl } from '@/lib/core/utils/urls' diff --git a/apps/sim/app/api/credential-sets/[id]/invite/route.ts b/apps/sim/app/api/credential-sets/[id]/invite/route.ts index a522cfcf41c..d0f4c0ca00c 100644 --- a/apps/sim/app/api/credential-sets/[id]/invite/route.ts +++ b/apps/sim/app/api/credential-sets/[id]/invite/route.ts @@ -1,3 +1,4 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { credentialSet, credentialSetInvitation, member, organization, user } from '@sim/db/schema' import { createLogger } from '@sim/logger' @@ -6,7 +7,6 @@ import { and, eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { getEmailSubject, renderPollingGroupInvitationEmail } from '@/components/emails' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { getSession } from '@/lib/auth' import { hasCredentialSetsAccess } from '@/lib/billing' import { getBaseUrl } from '@/lib/core/utils/urls' diff --git a/apps/sim/app/api/credential-sets/[id]/members/route.ts b/apps/sim/app/api/credential-sets/[id]/members/route.ts index ca3c1894903..64d72281259 100644 --- a/apps/sim/app/api/credential-sets/[id]/members/route.ts +++ b/apps/sim/app/api/credential-sets/[id]/members/route.ts @@ -1,10 +1,10 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { account, credentialSet, credentialSetMember, member, user } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { generateId } from '@sim/utils/id' import { and, eq, inArray } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { getSession } from '@/lib/auth' import { hasCredentialSetsAccess } from '@/lib/billing' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' diff --git a/apps/sim/app/api/credential-sets/[id]/route.ts b/apps/sim/app/api/credential-sets/[id]/route.ts index 47e9c44d2d1..8a7fcb51464 100644 --- a/apps/sim/app/api/credential-sets/[id]/route.ts +++ b/apps/sim/app/api/credential-sets/[id]/route.ts @@ -1,10 +1,10 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { credentialSet, credentialSetMember, member } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { and, eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { getSession } from '@/lib/auth' import { hasCredentialSetsAccess } from '@/lib/billing' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' diff --git a/apps/sim/app/api/credential-sets/invite/[token]/route.ts b/apps/sim/app/api/credential-sets/invite/[token]/route.ts index 8c248db611c..2d8e1b77a63 100644 --- a/apps/sim/app/api/credential-sets/invite/[token]/route.ts +++ b/apps/sim/app/api/credential-sets/invite/[token]/route.ts @@ -1,3 +1,4 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { credentialSet, @@ -9,7 +10,6 @@ import { createLogger } from '@sim/logger' import { generateId } from '@sim/utils/id' import { and, eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { getSession } from '@/lib/auth' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { syncAllWebhooksForCredentialSet } from '@/lib/webhooks/utils.server' diff --git a/apps/sim/app/api/credential-sets/memberships/route.ts b/apps/sim/app/api/credential-sets/memberships/route.ts index e198790b62d..1e3846bd0d7 100644 --- a/apps/sim/app/api/credential-sets/memberships/route.ts +++ b/apps/sim/app/api/credential-sets/memberships/route.ts @@ -1,10 +1,10 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { credentialSet, credentialSetMember, organization } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { generateId } from '@sim/utils/id' import { and, eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { getSession } from '@/lib/auth' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { syncAllWebhooksForCredentialSet } from '@/lib/webhooks/utils.server' diff --git a/apps/sim/app/api/credential-sets/route.ts b/apps/sim/app/api/credential-sets/route.ts index 55c06f8686b..cc5ba887999 100644 --- a/apps/sim/app/api/credential-sets/route.ts +++ b/apps/sim/app/api/credential-sets/route.ts @@ -1,3 +1,4 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { credentialSet, credentialSetMember, member, organization, user } from '@sim/db/schema' import { createLogger } from '@sim/logger' @@ -5,7 +6,6 @@ import { generateId } from '@sim/utils/id' import { and, count, desc, eq } from 'drizzle-orm' import { NextResponse } from 'next/server' import { z } from 'zod' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { getSession } from '@/lib/auth' import { hasCredentialSetsAccess } from '@/lib/billing' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' diff --git a/apps/sim/app/api/credentials/[id]/route.ts b/apps/sim/app/api/credentials/[id]/route.ts index 6b265e4636c..a85a32a72c4 100644 --- a/apps/sim/app/api/credentials/[id]/route.ts +++ b/apps/sim/app/api/credentials/[id]/route.ts @@ -1,3 +1,4 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { credential, credentialMember, environment, workspaceEnvironment } from '@sim/db/schema' import { createLogger } from '@sim/logger' @@ -5,7 +6,6 @@ import { generateId } from '@sim/utils/id' import { and, eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { getSession } from '@/lib/auth' import { encryptSecret } from '@/lib/core/security/encryption' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' diff --git a/apps/sim/app/api/credentials/route.ts b/apps/sim/app/api/credentials/route.ts index 465ae5340ea..ebb429b438d 100644 --- a/apps/sim/app/api/credentials/route.ts +++ b/apps/sim/app/api/credentials/route.ts @@ -1,3 +1,4 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { account, credential, credentialMember, workspace } from '@sim/db/schema' import { createLogger } from '@sim/logger' @@ -5,7 +6,6 @@ import { generateId } from '@sim/utils/id' import { and, eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { getSession } from '@/lib/auth' import { encryptSecret } from '@/lib/core/security/encryption' import { generateRequestId } from '@/lib/core/utils/request' diff --git a/apps/sim/app/api/environment/route.ts b/apps/sim/app/api/environment/route.ts index 9c0aca941ea..7d74c421262 100644 --- a/apps/sim/app/api/environment/route.ts +++ b/apps/sim/app/api/environment/route.ts @@ -1,3 +1,4 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { environment } from '@sim/db/schema' import { createLogger } from '@sim/logger' @@ -5,7 +6,6 @@ import { generateId } from '@sim/utils/id' import { eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { getSession } from '@/lib/auth' import { decryptSecret, encryptSecret } from '@/lib/core/security/encryption' import { generateRequestId } from '@/lib/core/utils/request' diff --git a/apps/sim/app/api/files/serve/[...path]/route.ts b/apps/sim/app/api/files/serve/[...path]/route.ts index a9126e5bb29..1125e1d3285 100644 --- a/apps/sim/app/api/files/serve/[...path]/route.ts +++ b/apps/sim/app/api/files/serve/[...path]/route.ts @@ -1,6 +1,6 @@ -import { createHash } from 'crypto' import { readFile } from 'fs/promises' import { createLogger } from '@sim/logger' +import { sha256Hex } from '@sim/security/hash' import type { NextRequest } from 'next/server' import { NextResponse } from 'next/server' import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' @@ -80,11 +80,7 @@ async function compileDocumentIfNeeded( } const code = buffer.toString('utf-8') - const cacheKey = createHash('sha256') - .update(ext) - .update(code) - .update(workspaceId ?? '') - .digest('hex') + const cacheKey = sha256Hex(`${ext}${code}${workspaceId ?? ''}`) const cached = compiledDocCache.get(cacheKey) if (cached) { return { buffer: cached, contentType: format.contentType } diff --git a/apps/sim/app/api/files/upload/route.ts b/apps/sim/app/api/files/upload/route.ts index 9ea1c53d871..705ea2d8b17 100644 --- a/apps/sim/app/api/files/upload/route.ts +++ b/apps/sim/app/api/files/upload/route.ts @@ -2,7 +2,7 @@ import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { sanitizeFileName } from '@/executor/constants' import '@/lib/uploads/core/setup.server' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { getSession } from '@/lib/auth' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { captureServerEvent } from '@/lib/posthog/server' diff --git a/apps/sim/app/api/folders/[id]/duplicate/route.ts b/apps/sim/app/api/folders/[id]/duplicate/route.ts index 7cdc446b8d5..9b7811a822f 100644 --- a/apps/sim/app/api/folders/[id]/duplicate/route.ts +++ b/apps/sim/app/api/folders/[id]/duplicate/route.ts @@ -1,3 +1,4 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { workflow, workflowFolder } from '@sim/db/schema' import { createLogger } from '@sim/logger' @@ -5,7 +6,6 @@ import { generateId } from '@sim/utils/id' import { and, eq, isNull, min } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { getSession } from '@/lib/auth' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' diff --git a/apps/sim/app/api/folders/[id]/route.test.ts b/apps/sim/app/api/folders/[id]/route.test.ts index 04f697e74a5..477ada12fce 100644 --- a/apps/sim/app/api/folders/[id]/route.test.ts +++ b/apps/sim/app/api/folders/[id]/route.test.ts @@ -37,7 +37,7 @@ const mockPerformDeleteFolder = workflowsOrchestrationMockFns.mockPerformDeleteF const mockGetUserEntityPermissions = permissionsMockFns.mockGetUserEntityPermissions -vi.mock('@/lib/audit/log', () => auditMock) +vi.mock('@sim/audit', () => auditMock) vi.mock('@sim/logger', () => ({ createLogger: vi.fn().mockReturnValue(mockLogger), runWithRequestContext: (_ctx: unknown, fn: () => T): T => fn(), diff --git a/apps/sim/app/api/folders/route.test.ts b/apps/sim/app/api/folders/route.test.ts index d3a3a173fa6..e5040507b92 100644 --- a/apps/sim/app/api/folders/route.test.ts +++ b/apps/sim/app/api/folders/route.test.ts @@ -30,7 +30,7 @@ const { mockLogger } = vi.hoisted(() => { const mockGetUserEntityPermissions = permissionsMockFns.mockGetUserEntityPermissions -vi.mock('@/lib/audit/log', () => auditMock) +vi.mock('@sim/audit', () => auditMock) vi.mock('drizzle-orm', () => ({ ...drizzleOrmMock, min: vi.fn((field) => ({ type: 'min', field })), diff --git a/apps/sim/app/api/folders/route.ts b/apps/sim/app/api/folders/route.ts index 69e8c42921c..14117e2b171 100644 --- a/apps/sim/app/api/folders/route.ts +++ b/apps/sim/app/api/folders/route.ts @@ -1,3 +1,4 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { workflow, workflowFolder } from '@sim/db/schema' import { createLogger } from '@sim/logger' @@ -5,7 +6,6 @@ import { generateId } from '@sim/utils/id' import { and, asc, eq, isNotNull, isNull, min } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { getSession } from '@/lib/auth' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { captureServerEvent } from '@/lib/posthog/server' diff --git a/apps/sim/app/api/form/manage/[id]/route.ts b/apps/sim/app/api/form/manage/[id]/route.ts index a8df0decc93..501f3edbf1a 100644 --- a/apps/sim/app/api/form/manage/[id]/route.ts +++ b/apps/sim/app/api/form/manage/[id]/route.ts @@ -1,10 +1,10 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { form } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { and, eq, isNull } from 'drizzle-orm' import type { NextRequest } from 'next/server' import { z } from 'zod' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { getSession } from '@/lib/auth' import { encryptSecret } from '@/lib/core/security/encryption' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' diff --git a/apps/sim/app/api/form/route.ts b/apps/sim/app/api/form/route.ts index 7b1ded808be..5336d324502 100644 --- a/apps/sim/app/api/form/route.ts +++ b/apps/sim/app/api/form/route.ts @@ -1,3 +1,4 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { form } from '@sim/db/schema' import { createLogger } from '@sim/logger' @@ -5,7 +6,6 @@ import { generateId } from '@sim/utils/id' import { and, eq, isNull } from 'drizzle-orm' import type { NextRequest } from 'next/server' import { z } from 'zod' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { getSession } from '@/lib/auth' import { isDev } from '@/lib/core/config/feature-flags' import { encryptSecret } from '@/lib/core/security/encryption' diff --git a/apps/sim/app/api/form/utils.ts b/apps/sim/app/api/form/utils.ts index 9f4bafd05ad..55bbe65e17f 100644 --- a/apps/sim/app/api/form/utils.ts +++ b/apps/sim/app/api/form/utils.ts @@ -1,6 +1,7 @@ import { db } from '@sim/db' import { form, workflow } from '@sim/db/schema' import { createLogger } from '@sim/logger' +import { authorizeWorkflowByWorkspacePermission } from '@sim/workflow-authz' import { and, eq, isNull } from 'drizzle-orm' import type { NextRequest, NextResponse } from 'next/server' import { @@ -9,7 +10,6 @@ import { validateAuthToken, } from '@/lib/core/security/deployment' import { decryptSecret } from '@/lib/core/security/encryption' -import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils' const logger = createLogger('FormAuthUtils') diff --git a/apps/sim/app/api/guardrails/validate/route.ts b/apps/sim/app/api/guardrails/validate/route.ts index 157ed3f6907..efd47375f00 100644 --- a/apps/sim/app/api/guardrails/validate/route.ts +++ b/apps/sim/app/api/guardrails/validate/route.ts @@ -1,4 +1,5 @@ import { createLogger } from '@sim/logger' +import { authorizeWorkflowByWorkspacePermission } from '@sim/workflow-authz' import { type NextRequest, NextResponse } from 'next/server' import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' @@ -7,7 +8,6 @@ import { validateHallucination } from '@/lib/guardrails/validate_hallucination' import { validateJson } from '@/lib/guardrails/validate_json' import { validatePII } from '@/lib/guardrails/validate_pii' import { validateRegex } from '@/lib/guardrails/validate_regex' -import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils' import { assertPermissionsAllowed, ProviderNotAllowedError, diff --git a/apps/sim/app/api/invitations/[id]/accept/route.ts b/apps/sim/app/api/invitations/[id]/accept/route.ts index 928bcf002b0..f3be2b8e1b7 100644 --- a/apps/sim/app/api/invitations/[id]/accept/route.ts +++ b/apps/sim/app/api/invitations/[id]/accept/route.ts @@ -1,7 +1,7 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { getSession } from '@/lib/auth' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { acceptInvitation } from '@/lib/invitations/core' diff --git a/apps/sim/app/api/invitations/[id]/reject/route.ts b/apps/sim/app/api/invitations/[id]/reject/route.ts index bc4be85a7be..7f9c311b4ca 100644 --- a/apps/sim/app/api/invitations/[id]/reject/route.ts +++ b/apps/sim/app/api/invitations/[id]/reject/route.ts @@ -1,7 +1,7 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { getSession } from '@/lib/auth' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { rejectInvitation } from '@/lib/invitations/core' diff --git a/apps/sim/app/api/invitations/[id]/resend/route.ts b/apps/sim/app/api/invitations/[id]/resend/route.ts index 28d0dc11937..1841f93118a 100644 --- a/apps/sim/app/api/invitations/[id]/resend/route.ts +++ b/apps/sim/app/api/invitations/[id]/resend/route.ts @@ -1,9 +1,9 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { user } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { getSession } from '@/lib/auth' import { getOrganizationSubscription } from '@/lib/billing/core/billing' import { isOrganizationOwnerOrAdmin } from '@/lib/billing/core/organization' diff --git a/apps/sim/app/api/invitations/[id]/route.ts b/apps/sim/app/api/invitations/[id]/route.ts index b6b15249baf..8e08cfc89dc 100644 --- a/apps/sim/app/api/invitations/[id]/route.ts +++ b/apps/sim/app/api/invitations/[id]/route.ts @@ -1,10 +1,10 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { invitation, invitationWorkspaceGrant } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { and, eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { getSession } from '@/lib/auth' import { isOrganizationOwnerOrAdmin } from '@/lib/billing/core/organization' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' diff --git a/apps/sim/app/api/jobs/[jobId]/route.test.ts b/apps/sim/app/api/jobs/[jobId]/route.test.ts index 3caf1992098..dac212544a0 100644 --- a/apps/sim/app/api/jobs/[jobId]/route.test.ts +++ b/apps/sim/app/api/jobs/[jobId]/route.test.ts @@ -5,9 +5,9 @@ import { hybridAuthMockFns, workflowsUtilsMock, workflowsUtilsMockFns } from '@s import type { NextRequest } from 'next/server' import { beforeEach, describe, expect, it, vi } from 'vitest' -const { mockGetJobQueue, mockVerifyWorkflowAccess, mockGetJob } = vi.hoisted(() => ({ +const { mockGetJobQueue, mockAuthorizeWorkflow, mockGetJob } = vi.hoisted(() => ({ mockGetJobQueue: vi.fn(), - mockVerifyWorkflowAccess: vi.fn(), + mockAuthorizeWorkflow: vi.fn(), mockGetJob: vi.fn(), })) @@ -15,8 +15,8 @@ vi.mock('@/lib/core/async-jobs', () => ({ getJobQueue: mockGetJobQueue, })) -vi.mock('@/socket/middleware/permissions', () => ({ - verifyWorkflowAccess: mockVerifyWorkflowAccess, +vi.mock('@sim/workflow-authz', () => ({ + authorizeWorkflowByWorkspacePermission: mockAuthorizeWorkflow, })) vi.mock('@/lib/workflows/utils', () => workflowsUtilsMock) @@ -42,7 +42,7 @@ describe('GET /api/jobs/[jobId]', () => { workspaceId: undefined, }) - mockVerifyWorkflowAccess.mockResolvedValue({ hasAccess: true }) + mockAuthorizeWorkflow.mockResolvedValue({ allowed: true, status: 200 }) workflowsUtilsMockFns.mockGetWorkflowById.mockResolvedValue({ id: 'workflow-1', workspaceId: 'workspace-1', diff --git a/apps/sim/app/api/jobs/[jobId]/route.ts b/apps/sim/app/api/jobs/[jobId]/route.ts index 3275d56ae80..927c33e24dc 100644 --- a/apps/sim/app/api/jobs/[jobId]/route.ts +++ b/apps/sim/app/api/jobs/[jobId]/route.ts @@ -33,12 +33,13 @@ export const GET = withRouteHandler( const metadataToCheck = job.metadata if (metadataToCheck?.workflowId) { - const { verifyWorkflowAccess } = await import('@/socket/middleware/permissions') - const accessCheck = await verifyWorkflowAccess( - authenticatedUserId, - metadataToCheck.workflowId as string - ) - if (!accessCheck.hasAccess) { + const { authorizeWorkflowByWorkspacePermission } = await import('@sim/workflow-authz') + const accessCheck = await authorizeWorkflowByWorkspacePermission({ + userId: authenticatedUserId, + workflowId: metadataToCheck.workflowId as string, + action: 'read', + }) + if (!accessCheck.allowed) { logger.warn(`[${requestId}] Access denied to workflow ${metadataToCheck.workflowId}`) return createErrorResponse('Access denied', 403) } diff --git a/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/documents/route.test.ts b/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/documents/route.test.ts index 5034584ee5c..57593fc9739 100644 --- a/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/documents/route.test.ts +++ b/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/documents/route.test.ts @@ -30,7 +30,7 @@ const mockCheckWriteAccess = knowledgeApiUtilsMockFns.mockCheckKnowledgeBaseWrit vi.mock('@sim/db', () => ({ db: mockDbChain })) vi.mock('@/app/api/knowledge/utils', () => knowledgeApiUtilsMock) -vi.mock('@/lib/audit/log', () => auditMock) +vi.mock('@sim/audit', () => auditMock) import { GET, PATCH } from '@/app/api/knowledge/[id]/connectors/[connectorId]/documents/route' diff --git a/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/documents/route.ts b/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/documents/route.ts index 210826cfe76..395da6b2812 100644 --- a/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/documents/route.ts +++ b/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/documents/route.ts @@ -1,10 +1,10 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { document, knowledgeConnector } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { and, eq, inArray, isNull } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' diff --git a/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/route.test.ts b/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/route.test.ts index bfc04f50802..9f78afd961a 100644 --- a/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/route.test.ts +++ b/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/route.test.ts @@ -50,7 +50,7 @@ vi.mock('@/lib/knowledge/tags/service', () => ({ vi.mock('@/lib/knowledge/documents/service', () => ({ deleteDocumentStorageFiles: vi.fn().mockResolvedValue(undefined), })) -vi.mock('@/lib/audit/log', () => auditMock) +vi.mock('@sim/audit', () => auditMock) import { DELETE, GET, PATCH } from '@/app/api/knowledge/[id]/connectors/[connectorId]/route' diff --git a/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/route.ts b/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/route.ts index 3bc7bb41b46..77ca9942fbd 100644 --- a/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/route.ts +++ b/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/route.ts @@ -1,3 +1,4 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { document, @@ -11,7 +12,6 @@ import { and, desc, eq, inArray, isNull, sql } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { decryptApiKey } from '@/lib/api-key/crypto' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { hasLiveSyncAccess } from '@/lib/billing/core/subscription' import { generateRequestId } from '@/lib/core/utils/request' diff --git a/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/sync/route.test.ts b/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/sync/route.test.ts index cb5145a1beb..b61ae4c7f27 100644 --- a/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/sync/route.test.ts +++ b/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/sync/route.test.ts @@ -34,7 +34,7 @@ vi.mock('@/app/api/knowledge/utils', () => knowledgeApiUtilsMock) vi.mock('@/lib/knowledge/connectors/sync-engine', () => ({ dispatchSync: mockDispatchSync, })) -vi.mock('@/lib/audit/log', () => auditMock) +vi.mock('@sim/audit', () => auditMock) import { POST } from '@/app/api/knowledge/[id]/connectors/[connectorId]/sync/route' diff --git a/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/sync/route.ts b/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/sync/route.ts index 57ea35e6161..63244e17516 100644 --- a/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/sync/route.ts +++ b/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/sync/route.ts @@ -1,9 +1,9 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { knowledgeConnector } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { and, eq, isNull } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' diff --git a/apps/sim/app/api/knowledge/[id]/connectors/route.ts b/apps/sim/app/api/knowledge/[id]/connectors/route.ts index e4e64724f24..6a6cb4c93b9 100644 --- a/apps/sim/app/api/knowledge/[id]/connectors/route.ts +++ b/apps/sim/app/api/knowledge/[id]/connectors/route.ts @@ -1,3 +1,4 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { knowledgeBase, knowledgeBaseTagDefinitions, knowledgeConnector } from '@sim/db/schema' import { createLogger } from '@sim/logger' @@ -6,7 +7,6 @@ import { and, desc, eq, isNull, sql } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { encryptApiKey } from '@/lib/api-key/crypto' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { hasLiveSyncAccess } from '@/lib/billing/core/subscription' import { generateRequestId } from '@/lib/core/utils/request' diff --git a/apps/sim/app/api/knowledge/[id]/documents/[documentId]/chunks/route.ts b/apps/sim/app/api/knowledge/[id]/documents/[documentId]/chunks/route.ts index a3bc917402b..6935272ad6b 100644 --- a/apps/sim/app/api/knowledge/[id]/documents/[documentId]/chunks/route.ts +++ b/apps/sim/app/api/knowledge/[id]/documents/[documentId]/chunks/route.ts @@ -1,11 +1,11 @@ import { createLogger } from '@sim/logger' +import { authorizeWorkflowByWorkspacePermission } from '@sim/workflow-authz' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { batchChunkOperation, createChunk, queryChunks } from '@/lib/knowledge/chunks/service' -import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils' import { checkDocumentAccess, checkDocumentWriteAccess } from '@/app/api/knowledge/utils' import { calculateCost } from '@/providers/utils' diff --git a/apps/sim/app/api/knowledge/[id]/documents/[documentId]/route.test.ts b/apps/sim/app/api/knowledge/[id]/documents/[documentId]/route.test.ts index 2562d3ff8ee..26e76cbb3fe 100644 --- a/apps/sim/app/api/knowledge/[id]/documents/[documentId]/route.test.ts +++ b/apps/sim/app/api/knowledge/[id]/documents/[documentId]/route.test.ts @@ -34,7 +34,7 @@ vi.mock('@/lib/knowledge/documents/service', () => ({ processDocumentAsync: vi.fn(), })) -vi.mock('@/lib/audit/log', () => auditMock) +vi.mock('@sim/audit', () => auditMock) import { deleteDocument, diff --git a/apps/sim/app/api/knowledge/[id]/documents/[documentId]/route.ts b/apps/sim/app/api/knowledge/[id]/documents/[documentId]/route.ts index bf70faecefd..f49a23a83a9 100644 --- a/apps/sim/app/api/knowledge/[id]/documents/[documentId]/route.ts +++ b/apps/sim/app/api/knowledge/[id]/documents/[documentId]/route.ts @@ -1,7 +1,7 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' diff --git a/apps/sim/app/api/knowledge/[id]/documents/route.test.ts b/apps/sim/app/api/knowledge/[id]/documents/route.test.ts index 355f570d804..d66f2cdd402 100644 --- a/apps/sim/app/api/knowledge/[id]/documents/route.test.ts +++ b/apps/sim/app/api/knowledge/[id]/documents/route.test.ts @@ -42,7 +42,7 @@ vi.mock('@/lib/knowledge/documents/service', () => ({ retryDocumentProcessing: vi.fn(), })) -vi.mock('@/lib/audit/log', () => auditMock) +vi.mock('@sim/audit', () => auditMock) import { createDocumentRecords, diff --git a/apps/sim/app/api/knowledge/[id]/documents/route.ts b/apps/sim/app/api/knowledge/[id]/documents/route.ts index 5e67472d537..40ed102fba7 100644 --- a/apps/sim/app/api/knowledge/[id]/documents/route.ts +++ b/apps/sim/app/api/knowledge/[id]/documents/route.ts @@ -1,8 +1,9 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { createLogger } from '@sim/logger' import { generateId } from '@sim/utils/id' +import { authorizeWorkflowByWorkspacePermission } from '@sim/workflow-authz' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { getSession } from '@/lib/auth' import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' @@ -18,7 +19,6 @@ import { } from '@/lib/knowledge/documents/service' import type { DocumentSortField, SortOrder } from '@/lib/knowledge/documents/types' import { captureServerEvent } from '@/lib/posthog/server' -import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils' import { checkKnowledgeBaseAccess, checkKnowledgeBaseWriteAccess } from '@/app/api/knowledge/utils' const logger = createLogger('DocumentsAPI') diff --git a/apps/sim/app/api/knowledge/[id]/documents/upsert/route.ts b/apps/sim/app/api/knowledge/[id]/documents/upsert/route.ts index ebcae6ab053..8dcc1385b61 100644 --- a/apps/sim/app/api/knowledge/[id]/documents/upsert/route.ts +++ b/apps/sim/app/api/knowledge/[id]/documents/upsert/route.ts @@ -1,11 +1,12 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { document } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { generateId } from '@sim/utils/id' +import { authorizeWorkflowByWorkspacePermission } from '@sim/workflow-authz' import { and, eq, isNull } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { @@ -14,7 +15,6 @@ import { getProcessingConfig, processDocumentsWithQueue, } from '@/lib/knowledge/documents/service' -import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils' import { checkKnowledgeBaseWriteAccess } from '@/app/api/knowledge/utils' const logger = createLogger('DocumentUpsertAPI') diff --git a/apps/sim/app/api/knowledge/[id]/restore/route.ts b/apps/sim/app/api/knowledge/[id]/restore/route.ts index d8b0e89e7a2..ece42f9f5dc 100644 --- a/apps/sim/app/api/knowledge/[id]/restore/route.ts +++ b/apps/sim/app/api/knowledge/[id]/restore/route.ts @@ -1,9 +1,9 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { knowledgeBase } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' diff --git a/apps/sim/app/api/knowledge/[id]/route.test.ts b/apps/sim/app/api/knowledge/[id]/route.test.ts index 3dd0603c9f9..ff58a3149e1 100644 --- a/apps/sim/app/api/knowledge/[id]/route.test.ts +++ b/apps/sim/app/api/knowledge/[id]/route.test.ts @@ -22,7 +22,7 @@ vi.mock('@sim/db', () => ({ db: mockDbChain, })) -vi.mock('@/lib/audit/log', () => auditMock) +vi.mock('@sim/audit', () => auditMock) vi.mock('@/lib/knowledge/service', async (importOriginal) => { const actual = await importOriginal() diff --git a/apps/sim/app/api/knowledge/[id]/route.ts b/apps/sim/app/api/knowledge/[id]/route.ts index 18951456d7f..6f97a2515c4 100644 --- a/apps/sim/app/api/knowledge/[id]/route.ts +++ b/apps/sim/app/api/knowledge/[id]/route.ts @@ -1,7 +1,7 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { PlatformEvents } from '@/lib/core/telemetry' import { generateRequestId } from '@/lib/core/utils/request' diff --git a/apps/sim/app/api/knowledge/route.test.ts b/apps/sim/app/api/knowledge/route.test.ts index 64c3638e4f6..bc0ab08d755 100644 --- a/apps/sim/app/api/knowledge/route.test.ts +++ b/apps/sim/app/api/knowledge/route.test.ts @@ -31,7 +31,7 @@ vi.mock('@sim/db', () => ({ db: mockDbChain, })) -vi.mock('@/lib/audit/log', () => auditMock) +vi.mock('@sim/audit', () => auditMock) vi.mock('@/lib/workspaces/permissions/utils', () => permissionsMock) diff --git a/apps/sim/app/api/knowledge/route.ts b/apps/sim/app/api/knowledge/route.ts index e11c7838d48..7f8b0c1309b 100644 --- a/apps/sim/app/api/knowledge/route.ts +++ b/apps/sim/app/api/knowledge/route.ts @@ -1,7 +1,7 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { getSession } from '@/lib/auth' import { PlatformEvents } from '@/lib/core/telemetry' import { generateRequestId } from '@/lib/core/utils/request' diff --git a/apps/sim/app/api/knowledge/search/route.test.ts b/apps/sim/app/api/knowledge/search/route.test.ts index e9efa572701..52c1fc47ccf 100644 --- a/apps/sim/app/api/knowledge/search/route.test.ts +++ b/apps/sim/app/api/knowledge/search/route.test.ts @@ -11,8 +11,8 @@ import { hybridAuthMockFns, knowledgeApiUtilsMock, knowledgeApiUtilsMockFns, + workflowAuthzMockFns, workflowsUtilsMock, - workflowsUtilsMockFns, } from '@sim/testing' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' @@ -169,7 +169,7 @@ describe('Knowledge Search API Route', () => { userId: 'user-123', authType: 'session', }) - workflowsUtilsMockFns.mockAuthorizeWorkflowByWorkspacePermission.mockClear().mockResolvedValue({ + workflowAuthzMockFns.mockAuthorizeWorkflowByWorkspacePermission.mockClear().mockResolvedValue({ allowed: true, status: 200, }) @@ -324,13 +324,11 @@ describe('Knowledge Search API Route', () => { expect(response.status).toBe(200) expect(data.success).toBe(true) - expect(workflowsUtilsMockFns.mockAuthorizeWorkflowByWorkspacePermission).toHaveBeenCalledWith( - { - workflowId: 'workflow-123', - userId: 'user-123', - action: 'read', - } - ) + expect(workflowAuthzMockFns.mockAuthorizeWorkflowByWorkspacePermission).toHaveBeenCalledWith({ + workflowId: 'workflow-123', + userId: 'user-123', + action: 'read', + }) }) it.concurrent('should return unauthorized for unauthenticated request', async () => { @@ -353,7 +351,7 @@ describe('Knowledge Search API Route', () => { workflowId: 'nonexistent-workflow', } - workflowsUtilsMockFns.mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValueOnce({ + workflowAuthzMockFns.mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValueOnce({ allowed: false, status: 404, message: 'Workflow not found', diff --git a/apps/sim/app/api/knowledge/search/route.ts b/apps/sim/app/api/knowledge/search/route.ts index 2bb7739948c..6c9db51ccc2 100644 --- a/apps/sim/app/api/knowledge/search/route.ts +++ b/apps/sim/app/api/knowledge/search/route.ts @@ -1,4 +1,5 @@ import { createLogger } from '@sim/logger' +import { authorizeWorkflowByWorkspacePermission } from '@sim/workflow-authz' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' @@ -10,7 +11,6 @@ import { getDocumentTagDefinitions } from '@/lib/knowledge/tags/service' import { buildUndefinedTagsError, validateTagValue } from '@/lib/knowledge/tags/utils' import type { StructuredFilter } from '@/lib/knowledge/types' import { estimateTokenCount } from '@/lib/tokenization/estimators' -import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils' import { generateSearchEmbedding, getDocumentNamesByIds, diff --git a/apps/sim/app/api/mcp/copilot/route.ts b/apps/sim/app/api/mcp/copilot/route.ts index 917470cf991..6ae73c4126d 100644 --- a/apps/sim/app/api/mcp/copilot/route.ts +++ b/apps/sim/app/api/mcp/copilot/route.ts @@ -15,6 +15,7 @@ import { userStats } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' import { generateId } from '@sim/utils/id' +import { authorizeWorkflowByWorkspacePermission } from '@sim/workflow-authz' import { eq, sql } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { validateOAuthAccessToken } from '@/lib/auth/oauth-token' @@ -31,10 +32,7 @@ import { env } from '@/lib/core/config/env' import { RateLimiter } from '@/lib/core/rate-limiter' import { getBaseUrl } from '@/lib/core/utils/urls' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' -import { - authorizeWorkflowByWorkspacePermission, - resolveWorkflowIdForUser, -} from '@/lib/workflows/utils' +import { resolveWorkflowIdForUser } from '@/lib/workflows/utils' const logger = createLogger('CopilotMcpAPI') const mcpRateLimiter = new RateLimiter() diff --git a/apps/sim/app/api/mcp/servers/[id]/route.ts b/apps/sim/app/api/mcp/servers/[id]/route.ts index bcffb6b8232..13005bb6433 100644 --- a/apps/sim/app/api/mcp/servers/[id]/route.ts +++ b/apps/sim/app/api/mcp/servers/[id]/route.ts @@ -1,10 +1,10 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { mcpServers } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' import { and, eq, isNull } from 'drizzle-orm' import type { NextRequest } from 'next/server' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { McpDnsResolutionError, diff --git a/apps/sim/app/api/mcp/servers/route.ts b/apps/sim/app/api/mcp/servers/route.ts index 4b9c8d93d47..bab33a9b9cb 100644 --- a/apps/sim/app/api/mcp/servers/route.ts +++ b/apps/sim/app/api/mcp/servers/route.ts @@ -1,3 +1,4 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { mcpServers } from '@sim/db/schema' import { createLogger } from '@sim/logger' @@ -5,7 +6,6 @@ import { toError } from '@sim/utils/errors' import { generateId } from '@sim/utils/id' import { and, eq, isNull } from 'drizzle-orm' import type { NextRequest } from 'next/server' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { McpDnsResolutionError, diff --git a/apps/sim/app/api/mcp/workflow-servers/[id]/route.ts b/apps/sim/app/api/mcp/workflow-servers/[id]/route.ts index 21c76cf46fd..f90a962cc22 100644 --- a/apps/sim/app/api/mcp/workflow-servers/[id]/route.ts +++ b/apps/sim/app/api/mcp/workflow-servers/[id]/route.ts @@ -1,10 +1,10 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { workflowMcpServer, workflowMcpTool } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' import { and, eq, isNull } from 'drizzle-orm' import type { NextRequest } from 'next/server' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware' import { mcpPubSub } from '@/lib/mcp/pubsub' diff --git a/apps/sim/app/api/mcp/workflow-servers/[id]/tools/[toolId]/route.ts b/apps/sim/app/api/mcp/workflow-servers/[id]/tools/[toolId]/route.ts index 76fa504a887..be511aeb868 100644 --- a/apps/sim/app/api/mcp/workflow-servers/[id]/tools/[toolId]/route.ts +++ b/apps/sim/app/api/mcp/workflow-servers/[id]/tools/[toolId]/route.ts @@ -1,10 +1,10 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { workflowMcpServer, workflowMcpTool } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' import { and, eq, isNull } from 'drizzle-orm' import type { NextRequest } from 'next/server' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware' import { mcpPubSub } from '@/lib/mcp/pubsub' diff --git a/apps/sim/app/api/mcp/workflow-servers/[id]/tools/route.ts b/apps/sim/app/api/mcp/workflow-servers/[id]/tools/route.ts index 08b71262c68..611a1b80c5c 100644 --- a/apps/sim/app/api/mcp/workflow-servers/[id]/tools/route.ts +++ b/apps/sim/app/api/mcp/workflow-servers/[id]/tools/route.ts @@ -1,3 +1,4 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { workflow, workflowMcpServer, workflowMcpTool } from '@sim/db/schema' import { createLogger } from '@sim/logger' @@ -5,7 +6,6 @@ import { toError } from '@sim/utils/errors' import { generateId } from '@sim/utils/id' import { and, eq, isNull } from 'drizzle-orm' import type { NextRequest } from 'next/server' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware' import { mcpPubSub } from '@/lib/mcp/pubsub' diff --git a/apps/sim/app/api/mcp/workflow-servers/route.ts b/apps/sim/app/api/mcp/workflow-servers/route.ts index 43105b9297c..f5c9c838557 100644 --- a/apps/sim/app/api/mcp/workflow-servers/route.ts +++ b/apps/sim/app/api/mcp/workflow-servers/route.ts @@ -1,3 +1,4 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { workflow, workflowMcpServer, workflowMcpTool } from '@sim/db/schema' import { createLogger } from '@sim/logger' @@ -5,7 +6,6 @@ import { toError } from '@sim/utils/errors' import { generateId } from '@sim/utils/id' import { and, eq, inArray, isNull, sql } from 'drizzle-orm' import type { NextRequest } from 'next/server' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware' import { mcpPubSub } from '@/lib/mcp/pubsub' diff --git a/apps/sim/app/api/organizations/[id]/invitations/route.test.ts b/apps/sim/app/api/organizations/[id]/invitations/route.test.ts index cd508506fb7..fb81ec48e0e 100644 --- a/apps/sim/app/api/organizations/[id]/invitations/route.test.ts +++ b/apps/sim/app/api/organizations/[id]/invitations/route.test.ts @@ -89,7 +89,7 @@ vi.mock('drizzle-orm', () => ({ vi.mock('@sim/logger', () => loggerMock) -vi.mock('@/lib/audit/log', () => auditMock) +vi.mock('@sim/audit', () => auditMock) vi.mock('@/lib/auth', () => ({ getSession: mockGetSession, diff --git a/apps/sim/app/api/organizations/[id]/invitations/route.ts b/apps/sim/app/api/organizations/[id]/invitations/route.ts index 68281a96b4e..dec09e2e1f8 100644 --- a/apps/sim/app/api/organizations/[id]/invitations/route.ts +++ b/apps/sim/app/api/organizations/[id]/invitations/route.ts @@ -1,9 +1,9 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { invitation, member, organization, user, workspace } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { and, eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { getSession } from '@/lib/auth' import { validateBulkInvitations, diff --git a/apps/sim/app/api/organizations/[id]/members/[memberId]/route.ts b/apps/sim/app/api/organizations/[id]/members/[memberId]/route.ts index 16b2ecf5535..971b1e57c79 100644 --- a/apps/sim/app/api/organizations/[id]/members/[memberId]/route.ts +++ b/apps/sim/app/api/organizations/[id]/members/[memberId]/route.ts @@ -1,10 +1,10 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { member, user, userStats } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { and, eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { getSession } from '@/lib/auth' import { setActiveOrganizationForCurrentSession } from '@/lib/auth/active-organization' import { getUserUsageData } from '@/lib/billing/core/usage' diff --git a/apps/sim/app/api/organizations/[id]/members/route.ts b/apps/sim/app/api/organizations/[id]/members/route.ts index b810226fd53..36a4d1d4b65 100644 --- a/apps/sim/app/api/organizations/[id]/members/route.ts +++ b/apps/sim/app/api/organizations/[id]/members/route.ts @@ -1,3 +1,4 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { invitation, @@ -9,7 +10,6 @@ import { import { createLogger } from '@sim/logger' import { and, eq, inArray } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { getSession } from '@/lib/auth' import { ENTITLED_SUBSCRIPTION_STATUSES } from '@/lib/billing/subscriptions/utils' import { validateSeatAvailability } from '@/lib/billing/validation/seat-management' diff --git a/apps/sim/app/api/organizations/[id]/route.ts b/apps/sim/app/api/organizations/[id]/route.ts index 671be8c67b9..70326038cca 100644 --- a/apps/sim/app/api/organizations/[id]/route.ts +++ b/apps/sim/app/api/organizations/[id]/route.ts @@ -1,10 +1,10 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { member, organization } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { and, eq, ne } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { getSession } from '@/lib/auth' import { getOrganizationSeatAnalytics, diff --git a/apps/sim/app/api/organizations/[id]/transfer-ownership/route.ts b/apps/sim/app/api/organizations/[id]/transfer-ownership/route.ts index 4203f0c5e88..6eac1e6d644 100644 --- a/apps/sim/app/api/organizations/[id]/transfer-ownership/route.ts +++ b/apps/sim/app/api/organizations/[id]/transfer-ownership/route.ts @@ -1,10 +1,10 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { member, user } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { and, eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { getSession } from '@/lib/auth' import { setActiveOrganizationForCurrentSession } from '@/lib/auth/active-organization' import { diff --git a/apps/sim/app/api/organizations/[id]/whitelabel/route.ts b/apps/sim/app/api/organizations/[id]/whitelabel/route.ts index fc54802db7f..ef1dab40e7c 100644 --- a/apps/sim/app/api/organizations/[id]/whitelabel/route.ts +++ b/apps/sim/app/api/organizations/[id]/whitelabel/route.ts @@ -1,10 +1,10 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { member, organization } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { and, eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { getSession } from '@/lib/auth' import { isOrganizationOnEnterprisePlan } from '@/lib/billing/core/subscription' import { HEX_COLOR_REGEX } from '@/lib/branding' diff --git a/apps/sim/app/api/organizations/route.test.ts b/apps/sim/app/api/organizations/route.test.ts index c52185a0278..a3a17208d84 100644 --- a/apps/sim/app/api/organizations/route.test.ts +++ b/apps/sim/app/api/organizations/route.test.ts @@ -71,7 +71,7 @@ vi.mock('drizzle-orm', () => ({ vi.mock('@sim/logger', () => loggerMock) -vi.mock('@/lib/audit/log', () => auditMock) +vi.mock('@sim/audit', () => auditMock) vi.mock('@/lib/auth', () => ({ getSession: mockGetSession, diff --git a/apps/sim/app/api/organizations/route.ts b/apps/sim/app/api/organizations/route.ts index 13b8e041e98..c79e30c017c 100644 --- a/apps/sim/app/api/organizations/route.ts +++ b/apps/sim/app/api/organizations/route.ts @@ -1,9 +1,9 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { member, organization, subscription as subscriptionTable } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { and, eq, inArray, or } from 'drizzle-orm' import { NextResponse } from 'next/server' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { getSession } from '@/lib/auth' import { setActiveOrganizationForCurrentSession } from '@/lib/auth/active-organization' import { diff --git a/apps/sim/app/api/schedules/[id]/route.test.ts b/apps/sim/app/api/schedules/[id]/route.test.ts index 01932c9fe3f..df3407461f1 100644 --- a/apps/sim/app/api/schedules/[id]/route.test.ts +++ b/apps/sim/app/api/schedules/[id]/route.test.ts @@ -7,8 +7,8 @@ import { auditMock, authMockFns, databaseMock, + workflowAuthzMockFns, workflowsUtilsMock, - workflowsUtilsMockFns, } from '@sim/testing' import { NextRequest } from 'next/server' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' @@ -21,7 +21,7 @@ vi.mock('drizzle-orm', () => ({ isNull: vi.fn(), })) -vi.mock('@/lib/audit/log', () => auditMock) +vi.mock('@sim/audit', () => auditMock) import { PUT } from './route' @@ -61,7 +61,7 @@ describe('Schedule PUT API (Reactivate)', () => { beforeEach(() => { vi.clearAllMocks() authMockFns.mockGetSession.mockResolvedValue({ user: { id: 'user-1' } }) - workflowsUtilsMockFns.mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValue({ + workflowAuthzMockFns.mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValue({ allowed: true, status: 200, workflow: { id: 'wf-1', workspaceId: 'ws-1' }, @@ -125,7 +125,7 @@ describe('Schedule PUT API (Reactivate)', () => { }) it('returns 404 when workflow does not exist for schedule', async () => { - workflowsUtilsMockFns.mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValue({ + workflowAuthzMockFns.mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValue({ allowed: false, status: 404, workflow: null, @@ -144,7 +144,7 @@ describe('Schedule PUT API (Reactivate)', () => { describe('Authorization', () => { it('returns 403 when user is not workflow owner', async () => { - workflowsUtilsMockFns.mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValue({ + workflowAuthzMockFns.mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValue({ allowed: false, status: 403, workflow: { id: 'wf-1', workspaceId: null }, @@ -165,7 +165,7 @@ describe('Schedule PUT API (Reactivate)', () => { }) it('returns 403 for workspace member with only read permission', async () => { - workflowsUtilsMockFns.mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValue({ + workflowAuthzMockFns.mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValue({ allowed: false, status: 403, workflow: { id: 'wf-1', workspaceId: 'ws-1' }, diff --git a/apps/sim/app/api/schedules/[id]/route.ts b/apps/sim/app/api/schedules/[id]/route.ts index 73155a15892..e8e3a486e60 100644 --- a/apps/sim/app/api/schedules/[id]/route.ts +++ b/apps/sim/app/api/schedules/[id]/route.ts @@ -1,16 +1,16 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { workflowSchedule } from '@sim/db/schema' import { createLogger } from '@sim/logger' +import { authorizeWorkflowByWorkspacePermission } from '@sim/workflow-authz' import { and, eq, isNull } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { getSession } from '@/lib/auth' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { captureServerEvent } from '@/lib/posthog/server' import { validateCronExpression } from '@/lib/workflows/schedules/utils' -import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils' import { verifyWorkspaceMembership } from '@/app/api/workflows/utils' const logger = createLogger('ScheduleAPI') diff --git a/apps/sim/app/api/schedules/route.test.ts b/apps/sim/app/api/schedules/route.test.ts index 7a0b1828db2..88929342615 100644 --- a/apps/sim/app/api/schedules/route.test.ts +++ b/apps/sim/app/api/schedules/route.test.ts @@ -3,7 +3,7 @@ * * @vitest-environment node */ -import { authMockFns, databaseMock, workflowsUtilsMock, workflowsUtilsMockFns } from '@sim/testing' +import { authMockFns, databaseMock, workflowAuthzMockFns, workflowsUtilsMock } from '@sim/testing' import { NextRequest } from 'next/server' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' @@ -44,7 +44,7 @@ describe('Schedule GET API', () => { beforeEach(() => { vi.clearAllMocks() authMockFns.mockGetSession.mockResolvedValue({ user: { id: 'user-1' } }) - workflowsUtilsMockFns.mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValue({ + workflowAuthzMockFns.mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValue({ allowed: true, status: 200, workflow: { id: 'wf-1', workspaceId: 'ws-1' }, @@ -103,7 +103,7 @@ describe('Schedule GET API', () => { }) it('returns 404 for non-existent workflow', async () => { - workflowsUtilsMockFns.mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValue({ + workflowAuthzMockFns.mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValue({ allowed: false, status: 404, message: 'Workflow not found', @@ -118,7 +118,7 @@ describe('Schedule GET API', () => { }) it('denies access for unauthorized user', async () => { - workflowsUtilsMockFns.mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValue({ + workflowAuthzMockFns.mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValue({ allowed: false, status: 403, message: 'Unauthorized: Access denied to read this workflow', diff --git a/apps/sim/app/api/schedules/route.ts b/apps/sim/app/api/schedules/route.ts index 2deecfebdfb..6b0b17a8450 100644 --- a/apps/sim/app/api/schedules/route.ts +++ b/apps/sim/app/api/schedules/route.ts @@ -1,16 +1,16 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { workflow, workflowDeploymentVersion, workflowSchedule } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { generateId } from '@sim/utils/id' +import { authorizeWorkflowByWorkspacePermission } from '@sim/workflow-authz' import { and, eq, isNull, or } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { getSession } from '@/lib/auth' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { captureServerEvent } from '@/lib/posthog/server' import { validateCronExpression } from '@/lib/workflows/schedules/utils' -import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils' import { verifyWorkspaceMembership } from '@/app/api/workflows/utils' const logger = createLogger('ScheduledAPI') diff --git a/apps/sim/app/api/skills/route.ts b/apps/sim/app/api/skills/route.ts index 069d5dcbfb8..6c91c9d1d7b 100644 --- a/apps/sim/app/api/skills/route.ts +++ b/apps/sim/app/api/skills/route.ts @@ -1,7 +1,7 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' diff --git a/apps/sim/app/api/table/[tableId]/restore/route.ts b/apps/sim/app/api/table/[tableId]/restore/route.ts index e8139035cf2..6e5ee48c0a1 100644 --- a/apps/sim/app/api/table/[tableId]/restore/route.ts +++ b/apps/sim/app/api/table/[tableId]/restore/route.ts @@ -1,6 +1,6 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' diff --git a/apps/sim/app/api/templates/[id]/route.ts b/apps/sim/app/api/templates/[id]/route.ts index d810fcf2f57..55b1a25445f 100644 --- a/apps/sim/app/api/templates/[id]/route.ts +++ b/apps/sim/app/api/templates/[id]/route.ts @@ -1,10 +1,10 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { templateCreators, templates, workflow } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { eq, sql } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { getSession } from '@/lib/auth' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' @@ -203,13 +203,14 @@ export const PUT = withRouteHandler( if (status !== undefined) updateData.status = status if (updateState && template.workflowId) { - const { verifyWorkflowAccess } = await import('@/socket/middleware/permissions') - const { hasAccess: hasWorkflowAccess } = await verifyWorkflowAccess( - session.user.id, - template.workflowId - ) + const { authorizeWorkflowByWorkspacePermission } = await import('@sim/workflow-authz') + const authorization = await authorizeWorkflowByWorkspacePermission({ + userId: session.user.id, + workflowId: template.workflowId, + action: 'read', + }) - if (!hasWorkflowAccess) { + if (!authorization.allowed) { logger.warn(`[${requestId}] User denied workflow access for state sync on template ${id}`) return NextResponse.json({ error: 'Access denied to workflow' }, { status: 403 }) } diff --git a/apps/sim/app/api/templates/route.ts b/apps/sim/app/api/templates/route.ts index ffca21baf97..96bb31b15ad 100644 --- a/apps/sim/app/api/templates/route.ts +++ b/apps/sim/app/api/templates/route.ts @@ -1,3 +1,4 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { templateCreators, @@ -8,10 +9,10 @@ import { } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { generateId } from '@sim/utils/id' +import { authorizeWorkflowByWorkspacePermission } from '@sim/workflow-authz' import { and, desc, eq, ilike, or, sql } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { getSession } from '@/lib/auth' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' @@ -20,7 +21,6 @@ import { extractRequiredCredentials, sanitizeCredentials, } from '@/lib/workflows/credentials/credential-extractor' -import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils' const logger = createLogger('TemplatesAPI') diff --git a/apps/sim/app/api/tools/custom/route.test.ts b/apps/sim/app/api/tools/custom/route.test.ts index 4414e922777..67e1a186e2e 100644 --- a/apps/sim/app/api/tools/custom/route.test.ts +++ b/apps/sim/app/api/tools/custom/route.test.ts @@ -9,8 +9,8 @@ import { hybridAuthMockFns, permissionsMock, permissionsMockFns, + workflowAuthzMockFns, workflowsUtilsMock, - workflowsUtilsMockFns, } from '@sim/testing' import { NextRequest } from 'next/server' import { beforeEach, describe, expect, it, vi } from 'vitest' @@ -233,7 +233,7 @@ describe('Custom Tools API Routes', () => { }) mockGetUserEntityPermissions.mockResolvedValue('admin') mockUpsertCustomTools.mockResolvedValue(sampleTools) - workflowsUtilsMockFns.mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValue({ + workflowAuthzMockFns.mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValue({ allowed: true, status: 200, workflow: { workspaceId: 'workspace-123' }, diff --git a/apps/sim/app/api/tools/custom/route.ts b/apps/sim/app/api/tools/custom/route.ts index 5ef4bf04295..9145c585524 100644 --- a/apps/sim/app/api/tools/custom/route.ts +++ b/apps/sim/app/api/tools/custom/route.ts @@ -1,16 +1,16 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { customTools } from '@sim/db/schema' import { createLogger } from '@sim/logger' +import { authorizeWorkflowByWorkspacePermission } from '@sim/workflow-authz' import { and, desc, eq, isNull, or } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { captureServerEvent } from '@/lib/posthog/server' import { upsertCustomTools } from '@/lib/workflows/custom-tools/operations' -import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils' import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils' const logger = createLogger('CustomToolsAPI') diff --git a/apps/sim/app/api/users/me/api-keys/[id]/route.ts b/apps/sim/app/api/users/me/api-keys/[id]/route.ts index e604fe5835b..147cc9d21a3 100644 --- a/apps/sim/app/api/users/me/api-keys/[id]/route.ts +++ b/apps/sim/app/api/users/me/api-keys/[id]/route.ts @@ -1,9 +1,9 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { apiKey } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { and, eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { getSession } from '@/lib/auth' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' diff --git a/apps/sim/app/api/users/me/api-keys/route.ts b/apps/sim/app/api/users/me/api-keys/route.ts index 098f9fe3029..b66fc85dc20 100644 --- a/apps/sim/app/api/users/me/api-keys/route.ts +++ b/apps/sim/app/api/users/me/api-keys/route.ts @@ -1,3 +1,4 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { apiKey } from '@sim/db/schema' import { createLogger } from '@sim/logger' @@ -6,7 +7,6 @@ import { and, eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { createApiKey, getApiKeyDisplayFormat } from '@/lib/api-key/auth' import { hashApiKey } from '@/lib/api-key/crypto' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { getSession } from '@/lib/auth' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' diff --git a/apps/sim/app/api/v1/admin/access-control/route.ts b/apps/sim/app/api/v1/admin/access-control/route.ts index d1cd6f9253a..3ac24168fae 100644 --- a/apps/sim/app/api/v1/admin/access-control/route.ts +++ b/apps/sim/app/api/v1/admin/access-control/route.ts @@ -22,11 +22,11 @@ * Response: { success: true, deletedCount: number, membersRemoved: number } */ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { permissionGroup, permissionGroupMember, user, workspace } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { count, eq, inArray, sql } from 'drizzle-orm' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { withAdminAuth } from '@/app/api/v1/admin/middleware' import { diff --git a/apps/sim/app/api/v1/admin/auth.ts b/apps/sim/app/api/v1/admin/auth.ts index 5e04bcc1d90..813d3f8c5b0 100644 --- a/apps/sim/app/api/v1/admin/auth.ts +++ b/apps/sim/app/api/v1/admin/auth.ts @@ -8,8 +8,8 @@ * curl -H "x-admin-key: your_admin_key" https://your-instance/api/v1/admin/... */ -import { createHash, timingSafeEqual } from 'crypto' import { createLogger } from '@sim/logger' +import { safeCompare } from '@sim/security/compare' import type { NextRequest } from 'next/server' import { env } from '@/lib/core/config/env' @@ -54,7 +54,7 @@ export function authenticateAdminRequest(request: NextRequest): AdminAuthResult } } - if (!constantTimeCompare(providedKey, adminKey)) { + if (!safeCompare(providedKey, adminKey)) { logger.warn('Invalid admin API key attempted', { keyPrefix: providedKey.slice(0, 8) }) return { authenticated: false, @@ -64,16 +64,3 @@ export function authenticateAdminRequest(request: NextRequest): AdminAuthResult return { authenticated: true } } - -/** - * Constant-time string comparison. - * - * @param a - First string to compare - * @param b - Second string to compare - * @returns True if strings are equal, false otherwise - */ -function constantTimeCompare(a: string, b: string): boolean { - const aHash = createHash('sha256').update(a).digest() - const bHash = createHash('sha256').update(b).digest() - return timingSafeEqual(aHash, bHash) -} diff --git a/apps/sim/app/api/v1/admin/workflows/[id]/deploy/route.ts b/apps/sim/app/api/v1/admin/workflows/[id]/deploy/route.ts index 86873b8e54b..5b10a1c3817 100644 --- a/apps/sim/app/api/v1/admin/workflows/[id]/deploy/route.ts +++ b/apps/sim/app/api/v1/admin/workflows/[id]/deploy/route.ts @@ -1,7 +1,7 @@ import { createLogger } from '@sim/logger' +import { getActiveWorkflowRecord } from '@sim/workflow-authz' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' -import { getActiveWorkflowRecord } from '@/lib/workflows/active-context' import { performFullDeploy, performFullUndeploy } from '@/lib/workflows/orchestration' import { withAdminAuthParams } from '@/app/api/v1/admin/middleware' import { diff --git a/apps/sim/app/api/v1/admin/workflows/[id]/route.ts b/apps/sim/app/api/v1/admin/workflows/[id]/route.ts index 6416ee62433..77e5e246da9 100644 --- a/apps/sim/app/api/v1/admin/workflows/[id]/route.ts +++ b/apps/sim/app/api/v1/admin/workflows/[id]/route.ts @@ -15,10 +15,10 @@ import { db } from '@sim/db' import { workflowBlocks, workflowEdges } from '@sim/db/schema' import { createLogger } from '@sim/logger' +import { getActiveWorkflowRecord } from '@sim/workflow-authz' import { count, eq } from 'drizzle-orm' import { NextResponse } from 'next/server' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' -import { getActiveWorkflowRecord } from '@/lib/workflows/active-context' import { performDeleteWorkflow } from '@/lib/workflows/orchestration' import { withAdminAuthParams } from '@/app/api/v1/admin/middleware' import { diff --git a/apps/sim/app/api/v1/admin/workflows/[id]/versions/[versionId]/activate/route.ts b/apps/sim/app/api/v1/admin/workflows/[id]/versions/[versionId]/activate/route.ts index 3b1f972d001..9e27097db99 100644 --- a/apps/sim/app/api/v1/admin/workflows/[id]/versions/[versionId]/activate/route.ts +++ b/apps/sim/app/api/v1/admin/workflows/[id]/versions/[versionId]/activate/route.ts @@ -1,7 +1,7 @@ import { createLogger } from '@sim/logger' +import { getActiveWorkflowRecord } from '@sim/workflow-authz' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' -import { getActiveWorkflowRecord } from '@/lib/workflows/active-context' import { performActivateVersion } from '@/lib/workflows/orchestration' import { withAdminAuthParams } from '@/app/api/v1/admin/middleware' import { diff --git a/apps/sim/app/api/v1/admin/workflows/[id]/versions/route.ts b/apps/sim/app/api/v1/admin/workflows/[id]/versions/route.ts index 5633eef91cb..6fdfc3fc0c6 100644 --- a/apps/sim/app/api/v1/admin/workflows/[id]/versions/route.ts +++ b/apps/sim/app/api/v1/admin/workflows/[id]/versions/route.ts @@ -1,6 +1,6 @@ import { createLogger } from '@sim/logger' +import { getActiveWorkflowRecord } from '@sim/workflow-authz' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' -import { getActiveWorkflowRecord } from '@/lib/workflows/active-context' import { listWorkflowVersions } from '@/lib/workflows/persistence/utils' import { withAdminAuthParams } from '@/app/api/v1/admin/middleware' import { diff --git a/apps/sim/app/api/v1/files/[fileId]/route.ts b/apps/sim/app/api/v1/files/[fileId]/route.ts index 13dea76f5a7..ef909a7da15 100644 --- a/apps/sim/app/api/v1/files/[fileId]/route.ts +++ b/apps/sim/app/api/v1/files/[fileId]/route.ts @@ -1,7 +1,7 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { diff --git a/apps/sim/app/api/v1/files/route.ts b/apps/sim/app/api/v1/files/route.ts index 3beb70d273e..e16f8b6c885 100644 --- a/apps/sim/app/api/v1/files/route.ts +++ b/apps/sim/app/api/v1/files/route.ts @@ -1,7 +1,7 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { diff --git a/apps/sim/app/api/v1/knowledge/[id]/documents/[documentId]/route.ts b/apps/sim/app/api/v1/knowledge/[id]/documents/[documentId]/route.ts index 19b0a80cb2a..21c6baf4e21 100644 --- a/apps/sim/app/api/v1/knowledge/[id]/documents/[documentId]/route.ts +++ b/apps/sim/app/api/v1/knowledge/[id]/documents/[documentId]/route.ts @@ -1,9 +1,9 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { document, knowledgeConnector } from '@sim/db/schema' import { and, eq, isNull } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { deleteDocument } from '@/lib/knowledge/documents/service' import { diff --git a/apps/sim/app/api/v1/knowledge/[id]/documents/route.ts b/apps/sim/app/api/v1/knowledge/[id]/documents/route.ts index 4cf12b2db2d..32107b050c2 100644 --- a/apps/sim/app/api/v1/knowledge/[id]/documents/route.ts +++ b/apps/sim/app/api/v1/knowledge/[id]/documents/route.ts @@ -1,6 +1,6 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { createSingleDocument, diff --git a/apps/sim/app/api/v1/knowledge/[id]/route.ts b/apps/sim/app/api/v1/knowledge/[id]/route.ts index 700ac9a8a85..e0fe7d7c13f 100644 --- a/apps/sim/app/api/v1/knowledge/[id]/route.ts +++ b/apps/sim/app/api/v1/knowledge/[id]/route.ts @@ -1,6 +1,6 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { deleteKnowledgeBase, updateKnowledgeBase } from '@/lib/knowledge/service' import { diff --git a/apps/sim/app/api/v1/knowledge/route.ts b/apps/sim/app/api/v1/knowledge/route.ts index 3f64ec3f7e1..a24ce394966 100644 --- a/apps/sim/app/api/v1/knowledge/route.ts +++ b/apps/sim/app/api/v1/knowledge/route.ts @@ -1,6 +1,6 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { createKnowledgeBase, getKnowledgeBases } from '@/lib/knowledge/service' import { diff --git a/apps/sim/app/api/v1/tables/[tableId]/columns/route.ts b/apps/sim/app/api/v1/tables/[tableId]/columns/route.ts index 09ef58e5f3e..bf20d38216a 100644 --- a/apps/sim/app/api/v1/tables/[tableId]/columns/route.ts +++ b/apps/sim/app/api/v1/tables/[tableId]/columns/route.ts @@ -1,7 +1,7 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { diff --git a/apps/sim/app/api/v1/tables/[tableId]/route.ts b/apps/sim/app/api/v1/tables/[tableId]/route.ts index 67bfd5dd8fc..dad51353a5f 100644 --- a/apps/sim/app/api/v1/tables/[tableId]/route.ts +++ b/apps/sim/app/api/v1/tables/[tableId]/route.ts @@ -1,6 +1,6 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { deleteTable, type TableSchema } from '@/lib/table' diff --git a/apps/sim/app/api/v1/tables/route.ts b/apps/sim/app/api/v1/tables/route.ts index bd564708650..43618f93102 100644 --- a/apps/sim/app/api/v1/tables/route.ts +++ b/apps/sim/app/api/v1/tables/route.ts @@ -1,7 +1,7 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { diff --git a/apps/sim/app/api/v1/workflows/[id]/route.ts b/apps/sim/app/api/v1/workflows/[id]/route.ts index b3b6437c713..44994ebfa57 100644 --- a/apps/sim/app/api/v1/workflows/[id]/route.ts +++ b/apps/sim/app/api/v1/workflows/[id]/route.ts @@ -2,10 +2,10 @@ import { db } from '@sim/db' import { workflowBlocks } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { generateId } from '@sim/utils/id' +import { getActiveWorkflowRecord } from '@sim/workflow-authz' import { eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' -import { getActiveWorkflowRecord } from '@/lib/workflows/active-context' import { extractInputFieldsFromBlocks } from '@/lib/workflows/input-format' import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils' import { createApiResponse, getUserLimits } from '@/app/api/v1/logs/meta' diff --git a/apps/sim/app/api/webhooks/[id]/route.ts b/apps/sim/app/api/webhooks/[id]/route.ts index f24a213b378..5c2a5cd51ad 100644 --- a/apps/sim/app/api/webhooks/[id]/route.ts +++ b/apps/sim/app/api/webhooks/[id]/route.ts @@ -1,9 +1,10 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { webhook, workflow } from '@sim/db/schema' import { createLogger } from '@sim/logger' +import { authorizeWorkflowByWorkspacePermission } from '@sim/workflow-authz' import { and, eq, isNull } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { validateInteger } from '@/lib/core/security/input-validation' import { PlatformEvents } from '@/lib/core/telemetry' @@ -11,7 +12,6 @@ import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { captureServerEvent } from '@/lib/posthog/server' import { cleanupExternalWebhook } from '@/lib/webhooks/provider-subscriptions' -import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils' const logger = createLogger('WebhookAPI') diff --git a/apps/sim/app/api/webhooks/route.ts b/apps/sim/app/api/webhooks/route.ts index 762f0a1c7fc..e1121b1caf0 100644 --- a/apps/sim/app/api/webhooks/route.ts +++ b/apps/sim/app/api/webhooks/route.ts @@ -1,10 +1,11 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { permissions, webhook, workflow, workflowDeploymentVersion } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { generateId, generateShortId } from '@sim/utils/id' +import { authorizeWorkflowByWorkspacePermission } from '@sim/workflow-authz' import { and, desc, eq, inArray, isNull, or } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { getSession } from '@/lib/auth' import { PlatformEvents } from '@/lib/core/telemetry' import { generateRequestId } from '@/lib/core/utils/request' @@ -20,7 +21,6 @@ import { import { getProviderHandler } from '@/lib/webhooks/providers' import { mergeNonUserFields } from '@/lib/webhooks/utils' import { syncWebhooksForCredentialSet } from '@/lib/webhooks/utils.server' -import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils' import { extractCredentialSetId, isCredentialSetValue } from '@/executor/constants' const logger = createLogger('WebhooksAPI') diff --git a/apps/sim/app/api/workflows/[id]/autolayout/route.ts b/apps/sim/app/api/workflows/[id]/autolayout/route.ts index 6893a3192f4..9b659744d25 100644 --- a/apps/sim/app/api/workflows/[id]/autolayout/route.ts +++ b/apps/sim/app/api/workflows/[id]/autolayout/route.ts @@ -1,4 +1,5 @@ import { createLogger } from '@sim/logger' +import { authorizeWorkflowByWorkspacePermission } from '@sim/workflow-authz' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' @@ -14,7 +15,6 @@ import { loadWorkflowFromNormalizedTables, type NormalizedWorkflowData, } from '@/lib/workflows/persistence/utils' -import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils' export const dynamic = 'force-dynamic' diff --git a/apps/sim/app/api/workflows/[id]/chat/status/route.test.ts b/apps/sim/app/api/workflows/[id]/chat/status/route.test.ts index fef312d7acf..188a5a47580 100644 --- a/apps/sim/app/api/workflows/[id]/chat/status/route.test.ts +++ b/apps/sim/app/api/workflows/[id]/chat/status/route.test.ts @@ -8,8 +8,8 @@ import { dbChainMockFns, hybridAuthMockFns, resetDbChainMock, + workflowAuthzMockFns, workflowsUtilsMock, - workflowsUtilsMockFns, } from '@sim/testing' import { NextRequest } from 'next/server' import { beforeEach, describe, expect, it, vi } from 'vitest' @@ -46,7 +46,7 @@ describe('Workflow Chat Status Route', () => { userId: 'user-1', authType: 'session', }) - workflowsUtilsMockFns.mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValueOnce({ + workflowAuthzMockFns.mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValueOnce({ allowed: false, status: 403, message: 'Access denied', @@ -66,7 +66,7 @@ describe('Workflow Chat Status Route', () => { userId: 'user-1', authType: 'session', }) - workflowsUtilsMockFns.mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValueOnce({ + workflowAuthzMockFns.mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValueOnce({ allowed: true, status: 200, workflow: { id: 'wf-1', workspaceId: 'ws-1' }, diff --git a/apps/sim/app/api/workflows/[id]/chat/status/route.ts b/apps/sim/app/api/workflows/[id]/chat/status/route.ts index 334f87ef727..5b11700b56b 100644 --- a/apps/sim/app/api/workflows/[id]/chat/status/route.ts +++ b/apps/sim/app/api/workflows/[id]/chat/status/route.ts @@ -1,12 +1,12 @@ import { db } from '@sim/db' import { chat } from '@sim/db/schema' import { createLogger } from '@sim/logger' +import { authorizeWorkflowByWorkspacePermission } from '@sim/workflow-authz' import { and, eq, isNull } from 'drizzle-orm' import type { NextRequest } from 'next/server' import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' -import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils' import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/utils' const logger = createLogger('ChatStatusAPI') diff --git a/apps/sim/app/api/workflows/[id]/duplicate/route.ts b/apps/sim/app/api/workflows/[id]/duplicate/route.ts index 48e7ce50454..a1fdc2de0d4 100644 --- a/apps/sim/app/api/workflows/[id]/duplicate/route.ts +++ b/apps/sim/app/api/workflows/[id]/duplicate/route.ts @@ -1,7 +1,7 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { PlatformEvents } from '@/lib/core/telemetry' import { generateRequestId } from '@/lib/core/utils/request' diff --git a/apps/sim/app/api/workflows/[id]/execute/route.async.test.ts b/apps/sim/app/api/workflows/[id]/execute/route.async.test.ts index 6fd35a75438..5b8debdc366 100644 --- a/apps/sim/app/api/workflows/[id]/execute/route.async.test.ts +++ b/apps/sim/app/api/workflows/[id]/execute/route.async.test.ts @@ -9,6 +9,7 @@ import { hybridAuthMockFns, loggingSessionMock, requestUtilsMockFns, + workflowAuthzMockFns, workflowsUtilsMock, workflowsUtilsMockFns, } from '@sim/testing' @@ -22,7 +23,7 @@ const mockCheckHybridAuth = hybridAuthMockFns.mockCheckHybridAuth const mockPreprocessExecution = executionPreprocessingMockFns.mockPreprocessExecution const mockAuthorizeWorkflowByWorkspacePermission = - workflowsUtilsMockFns.mockAuthorizeWorkflowByWorkspacePermission + workflowAuthzMockFns.mockAuthorizeWorkflowByWorkspacePermission vi.mock('@/lib/workflows/utils', () => workflowsUtilsMock) diff --git a/apps/sim/app/api/workflows/[id]/execute/route.ts b/apps/sim/app/api/workflows/[id]/execute/route.ts index 9df86fbf771..b6e1aeab7b7 100644 --- a/apps/sim/app/api/workflows/[id]/execute/route.ts +++ b/apps/sim/app/api/workflows/[id]/execute/route.ts @@ -3,6 +3,7 @@ import { workflow as workflowTable } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' import { generateId, isValidUuid } from '@sim/utils/id' +import { authorizeWorkflowByWorkspacePermission } from '@sim/workflow-authz' import { eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' @@ -45,11 +46,7 @@ import { loadWorkflowFromNormalizedTables, } from '@/lib/workflows/persistence/utils' import { createStreamingResponse } from '@/lib/workflows/streaming/streaming' -import { - authorizeWorkflowByWorkspacePermission, - createHttpResponseFromBlock, - workflowHasResponseBlock, -} from '@/lib/workflows/utils' +import { createHttpResponseFromBlock, workflowHasResponseBlock } from '@/lib/workflows/utils' import { executeWorkflowJob, type WorkflowExecutionPayload } from '@/background/workflow-execution' import { PublicApiNotAllowedError, diff --git a/apps/sim/app/api/workflows/[id]/executions/[executionId]/cancel/route.test.ts b/apps/sim/app/api/workflows/[id]/executions/[executionId]/cancel/route.test.ts index c7e514a082f..6a9808f1b0e 100644 --- a/apps/sim/app/api/workflows/[id]/executions/[executionId]/cancel/route.test.ts +++ b/apps/sim/app/api/workflows/[id]/executions/[executionId]/cancel/route.test.ts @@ -5,8 +5,8 @@ import { databaseMock, hybridAuthMockFns, + workflowAuthzMockFns, workflowsUtilsMock, - workflowsUtilsMockFns, } from '@sim/testing' import { NextRequest } from 'next/server' import { beforeEach, describe, expect, it, vi } from 'vitest' @@ -68,7 +68,7 @@ describe('POST /api/workflows/[id]/executions/[executionId]/cancel', () => { beforeEach(() => { vi.clearAllMocks() hybridAuthMockFns.mockCheckHybridAuth.mockResolvedValue({ success: true, userId: 'user-1' }) - workflowsUtilsMockFns.mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValue({ + workflowAuthzMockFns.mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValue({ allowed: true, }) mockAbortManualExecution.mockReturnValue(false) @@ -193,7 +193,7 @@ describe('POST /api/workflows/[id]/executions/[executionId]/cancel', () => { it('returns 403 when workflow access is denied', async () => { mockMarkExecutionCancelled.mockResolvedValue({ durablyRecorded: true, reason: 'recorded' }) - workflowsUtilsMockFns.mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValue({ + workflowAuthzMockFns.mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValue({ allowed: false, message: 'Access denied', status: 403, diff --git a/apps/sim/app/api/workflows/[id]/executions/[executionId]/cancel/route.ts b/apps/sim/app/api/workflows/[id]/executions/[executionId]/cancel/route.ts index 1e7496fe87e..1c23e6c6a09 100644 --- a/apps/sim/app/api/workflows/[id]/executions/[executionId]/cancel/route.ts +++ b/apps/sim/app/api/workflows/[id]/executions/[executionId]/cancel/route.ts @@ -1,6 +1,7 @@ import { db } from '@sim/db' import { workflowExecutionLogs } from '@sim/db/schema' import { createLogger } from '@sim/logger' +import { authorizeWorkflowByWorkspacePermission } from '@sim/workflow-authz' import { and, eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { checkHybridAuth } from '@/lib/auth/hybrid' @@ -10,7 +11,6 @@ import { createExecutionEventWriter, setExecutionMeta } from '@/lib/execution/ev import { abortManualExecution } from '@/lib/execution/manual-cancellation' import { captureServerEvent } from '@/lib/posthog/server' import { PauseResumeManager } from '@/lib/workflows/executor/human-in-the-loop-manager' -import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils' const logger = createLogger('CancelExecutionAPI') diff --git a/apps/sim/app/api/workflows/[id]/executions/[executionId]/stream/route.ts b/apps/sim/app/api/workflows/[id]/executions/[executionId]/stream/route.ts index 8cf17e17a48..c89246cdece 100644 --- a/apps/sim/app/api/workflows/[id]/executions/[executionId]/stream/route.ts +++ b/apps/sim/app/api/workflows/[id]/executions/[executionId]/stream/route.ts @@ -1,6 +1,7 @@ import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' import { sleep } from '@sim/utils/helpers' +import { authorizeWorkflowByWorkspacePermission } from '@sim/workflow-authz' import { type NextRequest, NextResponse } from 'next/server' import { getSession } from '@/lib/auth' import { SSE_HEADERS } from '@/lib/core/utils/sse' @@ -11,7 +12,6 @@ import { readExecutionEvents, } from '@/lib/execution/event-buffer' import { formatSSEEvent } from '@/lib/workflows/executor/execution-events' -import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils' const logger = createLogger('ExecutionStreamReconnectAPI') diff --git a/apps/sim/app/api/workflows/[id]/form/status/route.test.ts b/apps/sim/app/api/workflows/[id]/form/status/route.test.ts index 5787f6cd261..3fa8cd76157 100644 --- a/apps/sim/app/api/workflows/[id]/form/status/route.test.ts +++ b/apps/sim/app/api/workflows/[id]/form/status/route.test.ts @@ -8,8 +8,8 @@ import { dbChainMockFns, hybridAuthMockFns, resetDbChainMock, + workflowAuthzMockFns, workflowsUtilsMock, - workflowsUtilsMockFns, } from '@sim/testing' import { NextRequest } from 'next/server' import { beforeEach, describe, expect, it, vi } from 'vitest' @@ -45,7 +45,7 @@ describe('Workflow Form Status Route', () => { userId: 'user-1', authType: 'session', }) - workflowsUtilsMockFns.mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValueOnce({ + workflowAuthzMockFns.mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValueOnce({ allowed: false, status: 403, message: 'Access denied', @@ -65,7 +65,7 @@ describe('Workflow Form Status Route', () => { userId: 'user-1', authType: 'session', }) - workflowsUtilsMockFns.mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValueOnce({ + workflowAuthzMockFns.mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValueOnce({ allowed: true, status: 200, workflow: { id: 'wf-1', workspaceId: 'ws-1' }, diff --git a/apps/sim/app/api/workflows/[id]/form/status/route.ts b/apps/sim/app/api/workflows/[id]/form/status/route.ts index 00ce06ce3a6..ebe71b1ba29 100644 --- a/apps/sim/app/api/workflows/[id]/form/status/route.ts +++ b/apps/sim/app/api/workflows/[id]/form/status/route.ts @@ -1,11 +1,11 @@ import { db } from '@sim/db' import { form } from '@sim/db/schema' import { createLogger } from '@sim/logger' +import { authorizeWorkflowByWorkspacePermission } from '@sim/workflow-authz' import { and, eq } from 'drizzle-orm' import type { NextRequest } from 'next/server' import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' -import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils' import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/utils' const logger = createLogger('FormStatusAPI') diff --git a/apps/sim/app/api/workflows/[id]/restore/route.ts b/apps/sim/app/api/workflows/[id]/restore/route.ts index 54b3db3a3c4..e0205917494 100644 --- a/apps/sim/app/api/workflows/[id]/restore/route.ts +++ b/apps/sim/app/api/workflows/[id]/restore/route.ts @@ -1,6 +1,6 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' diff --git a/apps/sim/app/api/workflows/[id]/route.test.ts b/apps/sim/app/api/workflows/[id]/route.test.ts index 8201561adc1..2d3ec73334c 100644 --- a/apps/sim/app/api/workflows/[id]/route.test.ts +++ b/apps/sim/app/api/workflows/[id]/route.test.ts @@ -10,6 +10,7 @@ import { envMock, hybridAuthMockFns, telemetryMock, + workflowAuthzMockFns, workflowsOrchestrationMock, workflowsOrchestrationMockFns, workflowsPersistenceUtilsMock, @@ -24,7 +25,7 @@ const mockLoadWorkflowFromNormalizedTables = workflowsPersistenceUtilsMockFns.mockLoadWorkflowFromNormalizedTables const mockGetWorkflowById = workflowsUtilsMockFns.mockGetWorkflowById const mockAuthorizeWorkflowByWorkspacePermission = - workflowsUtilsMockFns.mockAuthorizeWorkflowByWorkspacePermission + workflowAuthzMockFns.mockAuthorizeWorkflowByWorkspacePermission const mockPerformDeleteWorkflow = workflowsOrchestrationMockFns.mockPerformDeleteWorkflow const mockDbUpdate = vi.fn() const mockDbSelect = vi.fn() @@ -52,7 +53,7 @@ vi.mock('@/lib/core/config/env', () => envMock) vi.mock('@/lib/core/telemetry', () => telemetryMock) -vi.mock('@/lib/audit/log', () => auditMock) +vi.mock('@sim/audit', () => auditMock) vi.mock('@/lib/workflows/persistence/utils', () => workflowsPersistenceUtilsMock) diff --git a/apps/sim/app/api/workflows/[id]/route.ts b/apps/sim/app/api/workflows/[id]/route.ts index c9b68ca180c..7f862131f81 100644 --- a/apps/sim/app/api/workflows/[id]/route.ts +++ b/apps/sim/app/api/workflows/[id]/route.ts @@ -1,6 +1,7 @@ import { db } from '@sim/db' import { workflow } from '@sim/db/schema' import { createLogger } from '@sim/logger' +import { authorizeWorkflowByWorkspacePermission } from '@sim/workflow-authz' import { and, eq, isNull, ne } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' @@ -10,7 +11,7 @@ import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { captureServerEvent } from '@/lib/posthog/server' import { performDeleteWorkflow } from '@/lib/workflows/orchestration' import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils' -import { authorizeWorkflowByWorkspacePermission, getWorkflowById } from '@/lib/workflows/utils' +import { getWorkflowById } from '@/lib/workflows/utils' const logger = createLogger('WorkflowByIdAPI') diff --git a/apps/sim/app/api/workflows/[id]/state/route.ts b/apps/sim/app/api/workflows/[id]/state/route.ts index ac76548f8be..d6c164da53a 100644 --- a/apps/sim/app/api/workflows/[id]/state/route.ts +++ b/apps/sim/app/api/workflows/[id]/state/route.ts @@ -2,6 +2,7 @@ import { db } from '@sim/db' import { workflow } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' +import { authorizeWorkflowByWorkspacePermission } from '@sim/workflow-authz' import { eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' @@ -16,7 +17,6 @@ import { saveWorkflowToNormalizedTables, } from '@/lib/workflows/persistence/utils' import { sanitizeAgentToolsInBlocks } from '@/lib/workflows/sanitization/validation' -import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils' import { validateEdges } from '@/stores/workflows/workflow/edge-validation' import type { BlockState, WorkflowState } from '@/stores/workflows/workflow/types' import { generateLoopBlocks, generateParallelBlocks } from '@/stores/workflows/workflow/utils' diff --git a/apps/sim/app/api/workflows/[id]/variables/route.test.ts b/apps/sim/app/api/workflows/[id]/variables/route.test.ts index 511d46c09c7..be226926086 100644 --- a/apps/sim/app/api/workflows/[id]/variables/route.test.ts +++ b/apps/sim/app/api/workflows/[id]/variables/route.test.ts @@ -7,13 +7,13 @@ import { auditMock, hybridAuthMockFns, + workflowAuthzMockFns, workflowsUtilsMock, - workflowsUtilsMockFns, } from '@sim/testing' import { NextRequest } from 'next/server' import { beforeEach, describe, expect, it, vi } from 'vitest' -vi.mock('@/lib/audit/log', () => auditMock) +vi.mock('@sim/audit', () => auditMock) vi.mock('@/lib/workflows/utils', () => workflowsUtilsMock) @@ -47,7 +47,7 @@ describe('Workflow Variables API Route', () => { userId: 'user-123', authType: 'session', }) - workflowsUtilsMockFns.mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValueOnce({ + workflowAuthzMockFns.mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValueOnce({ allowed: false, status: 404, message: 'Workflow not found', @@ -80,7 +80,7 @@ describe('Workflow Variables API Route', () => { userId: 'user-123', authType: 'session', }) - workflowsUtilsMockFns.mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValueOnce({ + workflowAuthzMockFns.mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValueOnce({ allowed: true, status: 200, workflow: mockWorkflow, @@ -112,7 +112,7 @@ describe('Workflow Variables API Route', () => { userId: 'user-123', authType: 'session', }) - workflowsUtilsMockFns.mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValueOnce({ + workflowAuthzMockFns.mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValueOnce({ allowed: true, status: 200, workflow: mockWorkflow, @@ -142,7 +142,7 @@ describe('Workflow Variables API Route', () => { userId: 'user-123', authType: 'session', }) - workflowsUtilsMockFns.mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValueOnce({ + workflowAuthzMockFns.mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValueOnce({ allowed: false, status: 403, message: 'Unauthorized: Access denied to read this workflow', @@ -175,7 +175,7 @@ describe('Workflow Variables API Route', () => { userId: 'user-123', authType: 'session', }) - workflowsUtilsMockFns.mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValueOnce({ + workflowAuthzMockFns.mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValueOnce({ allowed: true, status: 200, workflow: mockWorkflow, @@ -207,7 +207,7 @@ describe('Workflow Variables API Route', () => { userId: 'user-123', authType: 'session', }) - workflowsUtilsMockFns.mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValueOnce({ + workflowAuthzMockFns.mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValueOnce({ allowed: true, status: 200, workflow: mockWorkflow, @@ -250,7 +250,7 @@ describe('Workflow Variables API Route', () => { userId: 'user-123', authType: 'session', }) - workflowsUtilsMockFns.mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValueOnce({ + workflowAuthzMockFns.mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValueOnce({ allowed: false, status: 403, message: 'Unauthorized: Access denied to write this workflow', @@ -294,7 +294,7 @@ describe('Workflow Variables API Route', () => { userId: 'user-123', authType: 'session', }) - workflowsUtilsMockFns.mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValueOnce({ + workflowAuthzMockFns.mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValueOnce({ allowed: true, status: 200, workflow: mockWorkflow, @@ -324,7 +324,7 @@ describe('Workflow Variables API Route', () => { userId: 'user-123', authType: 'session', }) - workflowsUtilsMockFns.mockAuthorizeWorkflowByWorkspacePermission.mockRejectedValueOnce( + workflowAuthzMockFns.mockAuthorizeWorkflowByWorkspacePermission.mockRejectedValueOnce( new Error('Database connection failed') ) diff --git a/apps/sim/app/api/workflows/[id]/variables/route.ts b/apps/sim/app/api/workflows/[id]/variables/route.ts index 9a069c8ed7f..62d90a7e8a5 100644 --- a/apps/sim/app/api/workflows/[id]/variables/route.ts +++ b/apps/sim/app/api/workflows/[id]/variables/route.ts @@ -1,14 +1,14 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { workflow } from '@sim/db/schema' import { createLogger } from '@sim/logger' +import { authorizeWorkflowByWorkspacePermission } from '@sim/workflow-authz' import { eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' -import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils' import type { Variable } from '@/stores/variables/types' const logger = createLogger('WorkflowVariablesAPI') diff --git a/apps/sim/app/api/workflows/middleware.ts b/apps/sim/app/api/workflows/middleware.ts index af745564152..2a66a616c77 100644 --- a/apps/sim/app/api/workflows/middleware.ts +++ b/apps/sim/app/api/workflows/middleware.ts @@ -1,4 +1,5 @@ import { createLogger } from '@sim/logger' +import { authorizeWorkflowByWorkspacePermission } from '@sim/workflow-authz' import type { NextRequest } from 'next/server' import { type ApiKeyAuthResult, @@ -6,7 +7,7 @@ import { updateApiKeyLastUsed, } from '@/lib/api-key/service' import { type AuthResult, checkHybridAuth } from '@/lib/auth/hybrid' -import { authorizeWorkflowByWorkspacePermission, getWorkflowById } from '@/lib/workflows/utils' +import { getWorkflowById } from '@/lib/workflows/utils' const logger = createLogger('WorkflowMiddleware') diff --git a/apps/sim/app/api/workflows/route.test.ts b/apps/sim/app/api/workflows/route.test.ts index f01c4af4e06..ed10d8dc497 100644 --- a/apps/sim/app/api/workflows/route.test.ts +++ b/apps/sim/app/api/workflows/route.test.ts @@ -41,7 +41,7 @@ vi.mock('@sim/db', () => ({ }, })) -vi.mock('@/lib/audit/log', () => auditMock) +vi.mock('@sim/audit', () => auditMock) vi.mock('@/lib/workspaces/permissions/utils', () => permissionsMock) diff --git a/apps/sim/app/api/workflows/route.ts b/apps/sim/app/api/workflows/route.ts index aa211fa9eb3..d8b902388ea 100644 --- a/apps/sim/app/api/workflows/route.ts +++ b/apps/sim/app/api/workflows/route.ts @@ -1,3 +1,4 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { permissions, workflow, workflowFolder } from '@sim/db/schema' import { createLogger } from '@sim/logger' @@ -5,7 +6,6 @@ import { generateId } from '@sim/utils/id' import { and, asc, eq, inArray, isNull, min, sql } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' diff --git a/apps/sim/app/api/workspaces/[id]/api-keys/[keyId]/route.ts b/apps/sim/app/api/workspaces/[id]/api-keys/[keyId]/route.ts index 65bf80f1c4b..4677eb2e544 100644 --- a/apps/sim/app/api/workspaces/[id]/api-keys/[keyId]/route.ts +++ b/apps/sim/app/api/workspaces/[id]/api-keys/[keyId]/route.ts @@ -1,10 +1,10 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { apiKey } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { and, eq, not } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { getSession } from '@/lib/auth' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' diff --git a/apps/sim/app/api/workspaces/[id]/api-keys/route.ts b/apps/sim/app/api/workspaces/[id]/api-keys/route.ts index b9404581796..f0539f5b3a9 100644 --- a/apps/sim/app/api/workspaces/[id]/api-keys/route.ts +++ b/apps/sim/app/api/workspaces/[id]/api-keys/route.ts @@ -1,3 +1,4 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { apiKey } from '@sim/db/schema' import { createLogger } from '@sim/logger' @@ -7,7 +8,6 @@ import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { createApiKey, getApiKeyDisplayFormat } from '@/lib/api-key/auth' import { hashApiKey } from '@/lib/api-key/crypto' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { getSession } from '@/lib/auth' import { PlatformEvents } from '@/lib/core/telemetry' import { generateRequestId } from '@/lib/core/utils/request' diff --git a/apps/sim/app/api/workspaces/[id]/byok-keys/route.ts b/apps/sim/app/api/workspaces/[id]/byok-keys/route.ts index 29af19b139d..aa9728b7df0 100644 --- a/apps/sim/app/api/workspaces/[id]/byok-keys/route.ts +++ b/apps/sim/app/api/workspaces/[id]/byok-keys/route.ts @@ -1,3 +1,4 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { workspaceBYOKKeys } from '@sim/db/schema' import { createLogger } from '@sim/logger' @@ -5,7 +6,6 @@ import { generateShortId } from '@sim/utils/id' import { and, eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { getSession } from '@/lib/auth' import { decryptSecret, encryptSecret } from '@/lib/core/security/encryption' import { generateRequestId } from '@/lib/core/utils/request' diff --git a/apps/sim/app/api/workspaces/[id]/data-retention/route.ts b/apps/sim/app/api/workspaces/[id]/data-retention/route.ts index 9d8f02ed89e..f1dbc043b33 100644 --- a/apps/sim/app/api/workspaces/[id]/data-retention/route.ts +++ b/apps/sim/app/api/workspaces/[id]/data-retention/route.ts @@ -1,10 +1,10 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { workspace } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { getSession } from '@/lib/auth' import { CLEANUP_CONFIG } from '@/lib/billing/cleanup-dispatcher' import { getHighestPrioritySubscription } from '@/lib/billing/core/plan' diff --git a/apps/sim/app/api/workspaces/[id]/duplicate/route.ts b/apps/sim/app/api/workspaces/[id]/duplicate/route.ts index d0b1bbaf922..3d0f939073e 100644 --- a/apps/sim/app/api/workspaces/[id]/duplicate/route.ts +++ b/apps/sim/app/api/workspaces/[id]/duplicate/route.ts @@ -1,7 +1,7 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { getSession } from '@/lib/auth' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' diff --git a/apps/sim/app/api/workspaces/[id]/environment/route.ts b/apps/sim/app/api/workspaces/[id]/environment/route.ts index affbf182946..4cabaec2583 100644 --- a/apps/sim/app/api/workspaces/[id]/environment/route.ts +++ b/apps/sim/app/api/workspaces/[id]/environment/route.ts @@ -1,3 +1,4 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { workspaceEnvironment } from '@sim/db/schema' import { createLogger } from '@sim/logger' @@ -5,7 +6,6 @@ import { generateId } from '@sim/utils/id' import { eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { getSession } from '@/lib/auth' import { encryptSecret } from '@/lib/core/security/encryption' import { generateRequestId } from '@/lib/core/utils/request' diff --git a/apps/sim/app/api/workspaces/[id]/files/[fileId]/content/route.ts b/apps/sim/app/api/workspaces/[id]/files/[fileId]/content/route.ts index 0897b99e193..606978a9279 100644 --- a/apps/sim/app/api/workspaces/[id]/files/[fileId]/content/route.ts +++ b/apps/sim/app/api/workspaces/[id]/files/[fileId]/content/route.ts @@ -1,6 +1,6 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { getSession } from '@/lib/auth' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' diff --git a/apps/sim/app/api/workspaces/[id]/files/[fileId]/restore/route.ts b/apps/sim/app/api/workspaces/[id]/files/[fileId]/restore/route.ts index b932c53f267..525db3167c4 100644 --- a/apps/sim/app/api/workspaces/[id]/files/[fileId]/restore/route.ts +++ b/apps/sim/app/api/workspaces/[id]/files/[fileId]/restore/route.ts @@ -1,6 +1,6 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { getSession } from '@/lib/auth' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' diff --git a/apps/sim/app/api/workspaces/[id]/files/[fileId]/route.ts b/apps/sim/app/api/workspaces/[id]/files/[fileId]/route.ts index b692a15a2fc..1e9f63a61d0 100644 --- a/apps/sim/app/api/workspaces/[id]/files/[fileId]/route.ts +++ b/apps/sim/app/api/workspaces/[id]/files/[fileId]/route.ts @@ -1,6 +1,6 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { getSession } from '@/lib/auth' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' diff --git a/apps/sim/app/api/workspaces/[id]/files/route.ts b/apps/sim/app/api/workspaces/[id]/files/route.ts index ca9bb9e18f3..a006dd2e963 100644 --- a/apps/sim/app/api/workspaces/[id]/files/route.ts +++ b/apps/sim/app/api/workspaces/[id]/files/route.ts @@ -1,6 +1,6 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { createLogger } from '@sim/logger' import { type NextRequest, NextResponse } from 'next/server' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { getSession } from '@/lib/auth' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' diff --git a/apps/sim/app/api/workspaces/[id]/notifications/[notificationId]/route.ts b/apps/sim/app/api/workspaces/[id]/notifications/[notificationId]/route.ts index 53a625e4af3..201a34bf704 100644 --- a/apps/sim/app/api/workspaces/[id]/notifications/[notificationId]/route.ts +++ b/apps/sim/app/api/workspaces/[id]/notifications/[notificationId]/route.ts @@ -1,10 +1,10 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { workflow, workspaceNotificationSubscription } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { and, eq, inArray } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { getSession } from '@/lib/auth' import { encryptSecret } from '@/lib/core/security/encryption' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' diff --git a/apps/sim/app/api/workspaces/[id]/notifications/[notificationId]/test/route.ts b/apps/sim/app/api/workspaces/[id]/notifications/[notificationId]/test/route.ts index 270522f2642..d3afc81e232 100644 --- a/apps/sim/app/api/workspaces/[id]/notifications/[notificationId]/test/route.ts +++ b/apps/sim/app/api/workspaces/[id]/notifications/[notificationId]/test/route.ts @@ -1,7 +1,7 @@ -import { createHmac } from 'crypto' import { db } from '@sim/db' import { account, workspaceNotificationSubscription } from '@sim/db/schema' import { createLogger } from '@sim/logger' +import { hmacSha256Hex } from '@sim/security/hmac' import { toError } from '@sim/utils/errors' import { generateId } from '@sim/utils/id' import { and, eq } from 'drizzle-orm' @@ -36,9 +36,7 @@ interface SlackConfig { function generateSignature(secret: string, timestamp: number, body: string): string { const signatureBase = `${timestamp}.${body}` - const hmac = createHmac('sha256', secret) - hmac.update(signatureBase) - return hmac.digest('hex') + return hmacSha256Hex(signatureBase, secret) } function buildTestPayload(subscription: typeof workspaceNotificationSubscription.$inferSelect) { diff --git a/apps/sim/app/api/workspaces/[id]/notifications/route.ts b/apps/sim/app/api/workspaces/[id]/notifications/route.ts index ee8753fbdde..25bd80bba8c 100644 --- a/apps/sim/app/api/workspaces/[id]/notifications/route.ts +++ b/apps/sim/app/api/workspaces/[id]/notifications/route.ts @@ -1,3 +1,4 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { workflow, workspaceNotificationSubscription } from '@sim/db/schema' import { createLogger } from '@sim/logger' @@ -5,7 +6,6 @@ import { generateId } from '@sim/utils/id' import { and, eq, inArray } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { getSession } from '@/lib/auth' import { encryptSecret } from '@/lib/core/security/encryption' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' diff --git a/apps/sim/app/api/workspaces/[id]/permission-groups/[groupId]/members/bulk/route.ts b/apps/sim/app/api/workspaces/[id]/permission-groups/[groupId]/members/bulk/route.ts index 66a60ec2cee..a0031101bf4 100644 --- a/apps/sim/app/api/workspaces/[id]/permission-groups/[groupId]/members/bulk/route.ts +++ b/apps/sim/app/api/workspaces/[id]/permission-groups/[groupId]/members/bulk/route.ts @@ -1,3 +1,4 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { permissionGroup, permissionGroupMember, permissions } from '@sim/db/schema' import { createLogger } from '@sim/logger' @@ -6,7 +7,6 @@ import { generateId } from '@sim/utils/id' import { and, eq, inArray } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { getSession } from '@/lib/auth' import { isWorkspaceOnEnterprisePlan } from '@/lib/billing' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' diff --git a/apps/sim/app/api/workspaces/[id]/permission-groups/[groupId]/members/route.ts b/apps/sim/app/api/workspaces/[id]/permission-groups/[groupId]/members/route.ts index 41579e1c975..d7a0cdc59b6 100644 --- a/apps/sim/app/api/workspaces/[id]/permission-groups/[groupId]/members/route.ts +++ b/apps/sim/app/api/workspaces/[id]/permission-groups/[groupId]/members/route.ts @@ -1,3 +1,4 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { permissionGroup, permissionGroupMember, permissions, user } from '@sim/db/schema' import { createLogger } from '@sim/logger' @@ -6,7 +7,6 @@ import { generateId } from '@sim/utils/id' import { and, eq, inArray } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { getSession } from '@/lib/auth' import { isWorkspaceOnEnterprisePlan } from '@/lib/billing' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' diff --git a/apps/sim/app/api/workspaces/[id]/permission-groups/[groupId]/route.ts b/apps/sim/app/api/workspaces/[id]/permission-groups/[groupId]/route.ts index 0450d123c41..bc4f4643b5b 100644 --- a/apps/sim/app/api/workspaces/[id]/permission-groups/[groupId]/route.ts +++ b/apps/sim/app/api/workspaces/[id]/permission-groups/[groupId]/route.ts @@ -1,10 +1,10 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { permissionGroup, permissionGroupMember } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { and, eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { getSession } from '@/lib/auth' import { isWorkspaceOnEnterprisePlan } from '@/lib/billing' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' diff --git a/apps/sim/app/api/workspaces/[id]/permission-groups/route.ts b/apps/sim/app/api/workspaces/[id]/permission-groups/route.ts index 1181890bc1e..1680b7c1128 100644 --- a/apps/sim/app/api/workspaces/[id]/permission-groups/route.ts +++ b/apps/sim/app/api/workspaces/[id]/permission-groups/route.ts @@ -1,3 +1,4 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { permissionGroup, permissionGroupMember, user } from '@sim/db/schema' import { createLogger } from '@sim/logger' @@ -5,7 +6,6 @@ import { generateId } from '@sim/utils/id' import { and, count, desc, eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { getSession } from '@/lib/auth' import { isWorkspaceOnEnterprisePlan } from '@/lib/billing' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' diff --git a/apps/sim/app/api/workspaces/[id]/permissions/route.ts b/apps/sim/app/api/workspaces/[id]/permissions/route.ts index f228f8ef033..236bcb7187b 100644 --- a/apps/sim/app/api/workspaces/[id]/permissions/route.ts +++ b/apps/sim/app/api/workspaces/[id]/permissions/route.ts @@ -1,3 +1,4 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { permissions, user, workspace, workspaceEnvironment } from '@sim/db/schema' import { createLogger } from '@sim/logger' @@ -5,7 +6,6 @@ import { generateId } from '@sim/utils/id' import { and, eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { getSession } from '@/lib/auth' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { syncWorkspaceEnvCredentials } from '@/lib/credentials/environment' diff --git a/apps/sim/app/api/workspaces/[id]/route.ts b/apps/sim/app/api/workspaces/[id]/route.ts index e1fffb08a3c..a17a78a2a55 100644 --- a/apps/sim/app/api/workspaces/[id]/route.ts +++ b/apps/sim/app/api/workspaces/[id]/route.ts @@ -1,9 +1,9 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { workflow } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { and, eq, inArray, isNull } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { getSession } from '@/lib/auth' import { captureServerEvent } from '@/lib/posthog/server' import { archiveWorkspace } from '@/lib/workspaces/lifecycle' diff --git a/apps/sim/app/api/workspaces/invitations/route.test.ts b/apps/sim/app/api/workspaces/invitations/route.test.ts index 38d3cdd63fe..e15b4236061 100644 --- a/apps/sim/app/api/workspaces/invitations/route.test.ts +++ b/apps/sim/app/api/workspaces/invitations/route.test.ts @@ -92,7 +92,7 @@ vi.mock('@/ee/access-control/utils/permission-check', () => ({ InvitationsNotAllowedError: class InvitationsNotAllowedError extends Error {}, })) -vi.mock('@/lib/audit/log', () => auditMock) +vi.mock('@sim/audit', () => auditMock) vi.mock('@/lib/posthog/server', () => ({ captureServerEvent: vi.fn(), diff --git a/apps/sim/app/api/workspaces/invitations/route.ts b/apps/sim/app/api/workspaces/invitations/route.ts index 5f6c848257b..a994d6daa48 100644 --- a/apps/sim/app/api/workspaces/invitations/route.ts +++ b/apps/sim/app/api/workspaces/invitations/route.ts @@ -1,9 +1,9 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { permissions, type permissionTypeEnum, user, workspace } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { and, eq, isNull, sql } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { getSession } from '@/lib/auth' import { getUserOrganization } from '@/lib/billing/organizations/membership' import { validateSeatAvailability } from '@/lib/billing/validation/seat-management' diff --git a/apps/sim/app/api/workspaces/members/[id]/route.ts b/apps/sim/app/api/workspaces/members/[id]/route.ts index 067112b74be..43add66c447 100644 --- a/apps/sim/app/api/workspaces/members/[id]/route.ts +++ b/apps/sim/app/api/workspaces/members/[id]/route.ts @@ -1,10 +1,10 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { permissions, workspace } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { and, eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { getSession } from '@/lib/auth' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { revokeWorkspaceCredentialMemberships } from '@/lib/credentials/access' diff --git a/apps/sim/app/api/workspaces/route.ts b/apps/sim/app/api/workspaces/route.ts index a83f115e835..e1e874170a9 100644 --- a/apps/sim/app/api/workspaces/route.ts +++ b/apps/sim/app/api/workspaces/route.ts @@ -1,3 +1,4 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { permissions, settings, type WorkspaceMode, workflow, workspace } from '@sim/db/schema' import { createLogger } from '@sim/logger' @@ -5,7 +6,6 @@ import { generateId } from '@sim/utils/id' import { and, desc, eq, isNull, sql } from 'drizzle-orm' import { NextResponse } from 'next/server' import { z } from 'zod' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { getSession } from '@/lib/auth' import { PlatformEvents } from '@/lib/core/telemetry' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/hooks/use-change-detection.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/hooks/use-change-detection.ts index 41b394b8a77..a9a319688e1 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/hooks/use-change-detection.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/hooks/use-change-detection.ts @@ -1,6 +1,6 @@ import { useMemo } from 'react' +import { mergeSubblockStateWithValues } from '@sim/workflow-persistence/subblocks' import { hasWorkflowChanged } from '@/lib/workflows/comparison' -import { mergeSubblockStateWithValues } from '@/lib/workflows/subblocks' import { useVariablesStore } from '@/stores/variables/store' import { useSubBlockStore } from '@/stores/workflows/subblock/store' import { useWorkflowStore } from '@/stores/workflows/workflow/store' diff --git a/apps/sim/background/workspace-notification-delivery.ts b/apps/sim/background/workspace-notification-delivery.ts index a0e1eab3ac0..454e68bea90 100644 --- a/apps/sim/background/workspace-notification-delivery.ts +++ b/apps/sim/background/workspace-notification-delivery.ts @@ -1,4 +1,3 @@ -import { createHmac } from 'crypto' import { db, workflowExecutionLogs } from '@sim/db' import { account, @@ -6,9 +5,11 @@ import { workspaceNotificationSubscription, } from '@sim/db/schema' import { createLogger } from '@sim/logger' +import { hmacSha256Hex } from '@sim/security/hmac' import { toError } from '@sim/utils/errors' import { formatDuration } from '@sim/utils/formatting' import { generateId } from '@sim/utils/id' +import { getActiveWorkflowContext } from '@sim/workflow-authz' import { task } from '@trigger.dev/sdk' import { and, eq, isNull, lte, or, sql } from 'drizzle-orm' import { @@ -26,7 +27,6 @@ import { getBaseUrl } from '@/lib/core/utils/urls' import type { TraceSpan, WorkflowExecutionLog } from '@/lib/logs/types' import { sendEmail } from '@/lib/messaging/email/mailer' import type { AlertConfig } from '@/lib/notifications/alert-rules' -import { getActiveWorkflowContext } from '@/lib/workflows/active-context' import { getWorkspaceBilledAccountUserId } from '@/lib/workspaces/utils' const logger = createLogger('WorkspaceNotificationDelivery') @@ -62,9 +62,7 @@ interface NotificationPayload { function generateSignature(secret: string, timestamp: number, body: string): string { const signatureBase = `${timestamp}.${body}` - const hmac = createHmac('sha256', secret) - hmac.update(signatureBase) - return hmac.digest('hex') + return hmacSha256Hex(signatureBase, secret) } async function buildPayload( diff --git a/apps/sim/blocks/types.ts b/apps/sim/blocks/types.ts index 79242ff0037..5d02261dcaa 100644 --- a/apps/sim/blocks/types.ts +++ b/apps/sim/blocks/types.ts @@ -1,18 +1,18 @@ import type { JSX, SVGProps } from 'react' +import type { + OutputCondition, + OutputFieldDefinition, + PrimitiveValueType, + SubBlockType, +} from '@sim/workflow-types/blocks' import type { SelectorKey } from '@/hooks/selectors/types' import type { ToolResponse } from '@/tools/types' +export type { OutputCondition, OutputFieldDefinition, PrimitiveValueType, SubBlockType } +export { isHiddenFromDisplay } from '@sim/workflow-types/blocks' + export type BlockIcon = (props: SVGProps) => JSX.Element export type ParamType = 'string' | 'number' | 'boolean' | 'json' | 'array' | 'file' -export type PrimitiveValueType = - | 'string' - | 'number' - | 'boolean' - | 'json' - | 'array' - | 'file' - | 'file[]' - | 'any' export type BlockCategory = 'blocks' | 'tools' | 'triggers' @@ -117,53 +117,6 @@ export type GenerationType = | 'cron-expression' | 'odata-expression' -export type SubBlockType = - | 'short-input' // Single line input - | 'long-input' // Multi-line input - | 'dropdown' // Select menu - | 'combobox' // Searchable dropdown with text input - | 'slider' // Range input - | 'table' // Grid layout - | 'code' // Code editor - | 'switch' // Toggle button - | 'tool-input' // Tool configuration - | 'skill-input' // Skill selection for agent blocks - | 'checkbox-list' // Multiple selection - | 'grouped-checkbox-list' // Grouped, scrollable checkbox list with select all - | 'condition-input' // Conditional logic - | 'eval-input' // Evaluation input - | 'time-input' // Time input - | 'oauth-input' // OAuth credential selector - | 'webhook-config' // Webhook configuration - | 'schedule-info' // Schedule status display (next run, last ran, failure badge) - | 'file-selector' // File selector for Google Drive, etc. - | 'sheet-selector' // Sheet/tab selector for Google Sheets, Microsoft Excel - | 'project-selector' // Project selector for Jira, Discord, etc. - | 'channel-selector' // Channel selector for Slack, Discord, etc. - | 'user-selector' // User selector for Slack, etc. - | 'folder-selector' // Folder selector for Gmail, etc. - | 'knowledge-base-selector' // Knowledge base selector - | 'knowledge-tag-filters' // Multiple tag filters for knowledge bases - | 'document-selector' // Document selector for knowledge bases - | 'document-tag-entry' // Document tag entry for creating documents - | 'mcp-server-selector' // MCP server selector - | 'mcp-tool-selector' // MCP tool selector - | 'mcp-dynamic-args' // MCP dynamic arguments based on tool schema - | 'input-format' // Input structure format - | 'response-format' // Response structure format - | 'filter-builder' // Filter conditions builder - | 'sort-builder' // Sort conditions builder - | 'file-upload' // File uploader - | 'input-mapping' // Map parent variables to child workflow input schema - | 'variables-input' // Variable assignments for updating workflow variables - | 'messages-input' // Multiple message inputs with role and content for LLM message history - | 'workflow-selector' // Workflow selector for agent tools - | 'workflow-input-mapper' // Dynamic workflow input mapper based on selected workflow - | 'text' // Read-only text display - | 'router-input' // Router route definitions with descriptions - | 'table-selector' // Table selector with link to view table - | 'modal' // Launches a modal component resolved via the client-side modal registry - /** * Selector types that require display name hydration * These show IDs/keys that need to be resolved to human-readable names @@ -203,51 +156,6 @@ export type ToolOutputToValueType = T extends Record export type BlockOutput = PrimitiveValueType | { [key: string]: any } -/** - * Condition for showing an output field. - * Uses the same pattern as SubBlockConfig.condition - */ -export interface OutputCondition { - field: string - value: string | number | boolean | Array - not?: boolean - and?: { - field: string - value: - | string - | number - | boolean - | Array - | undefined - | null - not?: boolean - } -} - -export type OutputFieldDefinition = - | PrimitiveValueType - | { - type: PrimitiveValueType - description?: string - /** - * Optional condition for when this output should be shown. - * If not specified, the output is always shown. - * Uses the same condition format as subBlocks. - */ - condition?: OutputCondition - /** - * If true, this output is hidden from display in the tag dropdown and logs, - * but still available for resolution and execution. - */ - hiddenFromDisplay?: boolean - } - -export function isHiddenFromDisplay(def: unknown): boolean { - return Boolean( - def && typeof def === 'object' && 'hiddenFromDisplay' in def && def.hiddenFromDisplay - ) -} - export interface ParamConfig { type: ParamType description?: string diff --git a/apps/sim/ee/audit-logs/constants.ts b/apps/sim/ee/audit-logs/constants.ts index 445265f4f81..b1bd15fadaf 100644 --- a/apps/sim/ee/audit-logs/constants.ts +++ b/apps/sim/ee/audit-logs/constants.ts @@ -1,5 +1,5 @@ +import { AuditResourceType } from '@sim/audit' import type { ComboboxOption } from '@/components/emcn' -import { AuditResourceType } from '@/lib/audit/types' const ACRONYMS = new Set(['API', 'BYOK', 'MCP', 'OAUTH']) diff --git a/apps/sim/hooks/use-collaborative-workflow.ts b/apps/sim/hooks/use-collaborative-workflow.ts index bbf54868a27..e119cdf8e44 100644 --- a/apps/sim/hooks/use-collaborative-workflow.ts +++ b/apps/sim/hooks/use-collaborative-workflow.ts @@ -1,5 +1,15 @@ import { useCallback, useEffect, useRef } from 'react' import { createLogger } from '@sim/logger' +import { + BLOCK_OPERATIONS, + BLOCKS_OPERATIONS, + EDGES_OPERATIONS, + OPERATION_TARGETS, + SUBBLOCK_OPERATIONS, + SUBFLOW_OPERATIONS, + VARIABLE_OPERATIONS, + WORKFLOW_OPERATIONS, +} from '@sim/realtime-protocol/constants' import { generateId } from '@sim/utils/id' import { useQueryClient } from '@tanstack/react-query' import type { Edge } from 'reactflow' @@ -10,16 +20,6 @@ import { getBlock } from '@/blocks' import { normalizeName, RESERVED_BLOCK_NAMES } from '@/executor/constants' import { invalidateDeploymentQueries } from '@/hooks/queries/deployments' import { useUndoRedo } from '@/hooks/use-undo-redo' -import { - BLOCK_OPERATIONS, - BLOCKS_OPERATIONS, - EDGES_OPERATIONS, - OPERATION_TARGETS, - SUBBLOCK_OPERATIONS, - SUBFLOW_OPERATIONS, - VARIABLE_OPERATIONS, - WORKFLOW_OPERATIONS, -} from '@/socket/constants' import { useNotificationStore } from '@/stores/notifications' import { registerEmitFunctions, useOperationQueue } from '@/stores/operation-queue/store' import { usePanelEditorStore } from '@/stores/panel' diff --git a/apps/sim/hooks/use-undo-redo.ts b/apps/sim/hooks/use-undo-redo.ts index 023b2f1d8f8..2140de378a9 100644 --- a/apps/sim/hooks/use-undo-redo.ts +++ b/apps/sim/hooks/use-undo-redo.ts @@ -8,9 +8,6 @@ declare global { } } -import type { Edge } from 'reactflow' -import { useSession } from '@/lib/auth/auth-client' -import { enqueueReplaceWorkflowState } from '@/lib/workflows/operations/socket-operations' import { BLOCK_OPERATIONS, BLOCKS_OPERATIONS, @@ -18,7 +15,10 @@ import { EDGES_OPERATIONS, OPERATION_TARGETS, UNDO_REDO_OPERATIONS, -} from '@/socket/constants' +} from '@sim/realtime-protocol/constants' +import type { Edge } from 'reactflow' +import { useSession } from '@/lib/auth/auth-client' +import { enqueueReplaceWorkflowState } from '@/lib/workflows/operations/socket-operations' import { useOperationQueue } from '@/stores/operation-queue/store' import { type BatchAddBlocksOperation, diff --git a/apps/sim/lib/api-key/auth.ts b/apps/sim/lib/api-key/auth.ts index 8c359565630..99e527b18f8 100644 --- a/apps/sim/lib/api-key/auth.ts +++ b/apps/sim/lib/api-key/auth.ts @@ -1,6 +1,7 @@ import { db } from '@sim/db' import { apiKey } from '@sim/db/schema' import { createLogger } from '@sim/logger' +import { safeCompare } from '@sim/security/compare' import { generateShortId } from '@sim/utils/id' import { and, eq } from 'drizzle-orm' import { @@ -13,7 +14,6 @@ import { isLegacyApiKeyFormat, } from '@/lib/api-key/crypto' import { env } from '@/lib/core/config/env' -import { safeCompare } from '@/lib/core/security/encryption' const logger = createLogger('ApiKeyAuth') diff --git a/apps/sim/lib/api-key/crypto.ts b/apps/sim/lib/api-key/crypto.ts index 3e3118ccbb8..b8e89ecb030 100644 --- a/apps/sim/lib/api-key/crypto.ts +++ b/apps/sim/lib/api-key/crypto.ts @@ -1,13 +1,12 @@ -import { createCipheriv, createDecipheriv, createHash, randomBytes } from 'crypto' import { createLogger } from '@sim/logger' +import { decrypt, encrypt } from '@sim/security/encryption' +import { sha256Hex } from '@sim/security/hash' +import { generateSecureToken } from '@sim/security/tokens' +import { toError } from '@sim/utils/errors' import { env } from '@/lib/core/config/env' const logger = createLogger('ApiKeyCrypto') -/** - * Get the API encryption key from the environment - * @returns The API encryption key - */ function getApiEncryptionKey(): Buffer | null { const key = env.API_ENCRYPTION_KEY if (!key) { @@ -23,77 +22,38 @@ function getApiEncryptionKey(): Buffer | null { } /** - * Encrypts an API key using the dedicated API encryption key - * @param apiKey - The API key to encrypt - * @returns A promise that resolves to an object containing the encrypted API key and IV + * Encrypts an API key using the dedicated API encryption key. Falls back to + * returning the plain key when `API_ENCRYPTION_KEY` is unset, for backward + * compatibility with deployments that predate encryption-at-rest. */ export async function encryptApiKey(apiKey: string): Promise<{ encrypted: string; iv: string }> { const key = getApiEncryptionKey() - - // If no API encryption key is set, return the key as-is for backward compatibility if (!key) { return { encrypted: apiKey, iv: '' } } - - const iv = randomBytes(16) - const cipher = createCipheriv('aes-256-gcm', key, iv, { authTagLength: 16 }) - let encrypted = cipher.update(apiKey, 'utf8', 'hex') - encrypted += cipher.final('hex') - - const authTag = cipher.getAuthTag() - const ivHex = iv.toString('hex') - - // Format: iv:encrypted:authTag - return { - encrypted: `${ivHex}:${encrypted}:${authTag.toString('hex')}`, - iv: ivHex, - } + return encrypt(apiKey, key) } /** - * Decrypts an API key using the dedicated API encryption key - * @param encryptedValue - The encrypted value in format "iv:encrypted:authTag" or plain text - * @returns A promise that resolves to an object containing the decrypted API key + * Decrypts an API key previously produced by {@link encryptApiKey}. Values + * that lack the `iv:ciphertext:authTag` shape are assumed to be legacy plain + * text and returned unchanged. */ export async function decryptApiKey(encryptedValue: string): Promise<{ decrypted: string }> { const parts = encryptedValue.split(':') - - // Check if this is actually encrypted (contains colons) if (parts.length !== 3) { - // This is a plain text key, return as-is return { decrypted: encryptedValue } } const key = getApiEncryptionKey() - - // If no API encryption key is set, assume it's plain text if (!key) { return { decrypted: encryptedValue } } - const ivHex = parts[0] - const authTagHex = parts[2] - const encrypted = parts[1] - - if (!ivHex || !encrypted || !authTagHex) { - throw new Error('Invalid encrypted API key format. Expected "iv:encrypted:authTag"') - } - - const iv = Buffer.from(ivHex, 'hex') - const authTag = Buffer.from(authTagHex, 'hex') - try { - const decipher = createDecipheriv('aes-256-gcm', key, iv, { authTagLength: 16 }) - decipher.setAuthTag(authTag) - - let decrypted = decipher.update(encrypted, 'hex', 'utf8') - decrypted += decipher.final('utf8') - - return { decrypted } - } catch (error: unknown) { - logger.error('API key decryption error:', { - error: error instanceof Error ? error.message : 'Unknown error', - }) + return await decrypt(encryptedValue, key) + } catch (error) { + logger.error('API key decryption error:', { error: toError(error).message }) throw error } } @@ -103,7 +63,7 @@ export async function decryptApiKey(encryptedValue: string): Promise<{ decrypted * @returns A new API key string */ export function generateApiKey(): string { - return `sim_${randomBytes(24).toString('base64url')}` + return `sim_${generateSecureToken(24)}` } /** @@ -111,7 +71,7 @@ export function generateApiKey(): string { * @returns A new encrypted API key string */ export function generateEncryptedApiKey(): string { - return `sk-sim-${randomBytes(24).toString('base64url')}` + return `sk-sim-${generateSecureToken(24)}` } /** @@ -142,5 +102,5 @@ export function isLegacyApiKeyFormat(apiKey: string): boolean { * @returns The hex-encoded SHA-256 digest */ export function hashApiKey(plainKey: string): string { - return createHash('sha256').update(plainKey, 'utf8').digest('hex') + return sha256Hex(plainKey) } diff --git a/apps/sim/lib/auth/auth.ts b/apps/sim/lib/auth/auth.ts index f770920fc19..3bc9286c6e1 100644 --- a/apps/sim/lib/auth/auth.ts +++ b/apps/sim/lib/auth/auth.ts @@ -746,7 +746,7 @@ export const auth = betterAuth({ } }, onPasswordReset: async ({ user: resetUser }) => { - const { AuditAction, AuditResourceType, recordAudit } = await import('@/lib/audit/log') + const { AuditAction, AuditResourceType, recordAudit } = await import('@sim/audit') recordAudit({ actorId: resetUser.id, actorName: resetUser.name, diff --git a/apps/sim/lib/auth/internal.ts b/apps/sim/lib/auth/internal.ts index 2f5fd1cb50b..712235df5aa 100644 --- a/apps/sim/lib/auth/internal.ts +++ b/apps/sim/lib/auth/internal.ts @@ -1,8 +1,8 @@ import { createLogger } from '@sim/logger' +import { safeCompare } from '@sim/security/compare' import { jwtVerify, SignJWT } from 'jose' import { type NextRequest, NextResponse } from 'next/server' import { env } from '@/lib/core/config/env' -import { safeCompare } from '@/lib/core/security/encryption' import { getClientIp } from '@/lib/core/utils/request' const logger = createLogger('CronAuth') diff --git a/apps/sim/lib/copilot/auth/permissions.test.ts b/apps/sim/lib/copilot/auth/permissions.test.ts index dbb7222380b..9e0c4dcb3e8 100644 --- a/apps/sim/lib/copilot/auth/permissions.test.ts +++ b/apps/sim/lib/copilot/auth/permissions.test.ts @@ -10,7 +10,7 @@ const { mockGetActiveWorkflowContext } = vi.hoisted(() => ({ const mockGetUserEntityPermissions = permissionsMockFns.mockGetUserEntityPermissions -vi.mock('@/lib/workflows/active-context', () => ({ +vi.mock('@sim/workflow-authz', () => ({ getActiveWorkflowContext: mockGetActiveWorkflowContext, })) diff --git a/apps/sim/lib/copilot/auth/permissions.ts b/apps/sim/lib/copilot/auth/permissions.ts index 556bfdb0fb5..ab36213b8ca 100644 --- a/apps/sim/lib/copilot/auth/permissions.ts +++ b/apps/sim/lib/copilot/auth/permissions.ts @@ -1,5 +1,5 @@ import { createLogger } from '@sim/logger' -import { getActiveWorkflowContext } from '@/lib/workflows/active-context' +import { getActiveWorkflowContext } from '@sim/workflow-authz' import { getUserEntityPermissions, type PermissionType } from '@/lib/workspaces/permissions/utils' const logger = createLogger('CopilotPermissions') diff --git a/apps/sim/lib/copilot/chat/lifecycle.ts b/apps/sim/lib/copilot/chat/lifecycle.ts index c96287a6d2c..9dd3e5d7c05 100644 --- a/apps/sim/lib/copilot/chat/lifecycle.ts +++ b/apps/sim/lib/copilot/chat/lifecycle.ts @@ -1,9 +1,11 @@ import { db } from '@sim/db' import { copilotChats } from '@sim/db/schema' import { createLogger } from '@sim/logger' +import { + authorizeWorkflowByWorkspacePermission, + getActiveWorkflowRecord, +} from '@sim/workflow-authz' import { and, eq } from 'drizzle-orm' -import { getActiveWorkflowRecord } from '@/lib/workflows/active-context' -import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils' import { assertActiveWorkspaceAccess, checkWorkspaceAccess, diff --git a/apps/sim/lib/copilot/chat/persisted-message.ts b/apps/sim/lib/copilot/chat/persisted-message.ts index ba12391aee3..df27a2b140d 100644 --- a/apps/sim/lib/copilot/chat/persisted-message.ts +++ b/apps/sim/lib/copilot/chat/persisted-message.ts @@ -1,3 +1,4 @@ +import { generateId } from '@sim/utils/id' import { MothershipStreamV1CompletionStatus, MothershipStreamV1EventType, @@ -191,7 +192,7 @@ export function buildPersistedAssistantMessage( requestId?: string ): PersistedMessage { const message: PersistedMessage = { - id: crypto.randomUUID(), + id: generateId(), role: 'assistant', content: result.content, timestamp: new Date().toISOString(), @@ -488,7 +489,7 @@ function normalizeBlocks(rawBlocks: RawBlock[], messageContent: string): Persist export function normalizeMessage(raw: Record): PersistedMessage { const msg: PersistedMessage = { - id: (raw.id as string) ?? crypto.randomUUID(), + id: (raw.id as string) ?? generateId(), role: (raw.role as 'user' | 'assistant') ?? 'assistant', content: (raw.content as string) ?? '', timestamp: (raw.timestamp as string) ?? new Date().toISOString(), diff --git a/apps/sim/lib/copilot/chat/post.ts b/apps/sim/lib/copilot/chat/post.ts index ecd52209df2..1d598f1b4c1 100644 --- a/apps/sim/lib/copilot/chat/post.ts +++ b/apps/sim/lib/copilot/chat/post.ts @@ -2,6 +2,7 @@ import { type Context as OtelContext, context as otelContextApi } from '@opentel import { db } from '@sim/db' import { copilotChats } from '@sim/db/schema' import { createLogger } from '@sim/logger' +import { generateId } from '@sim/utils/id' import { eq, sql } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' @@ -615,8 +616,8 @@ export async function handleUnifiedChatPost(req: NextRequest) { // trace ID) as soon as startCopilotOtelRoot runs. Empty only in the // narrow pre-otelRoot window where errors don't correlate anyway. let requestId = '' - const executionId = crypto.randomUUID() - const runId = crypto.randomUUID() + const executionId = generateId() + const runId = generateId() try { const session = await getSession() @@ -628,7 +629,7 @@ export async function handleUnifiedChatPost(req: NextRequest) { const body = ChatMessageSchema.parse(await req.json()) const normalizedContexts = normalizeContexts(body.contexts) ?? [] - userMessageId = body.userMessageId || crypto.randomUUID() + userMessageId = body.userMessageId || generateId() otelRoot = startCopilotOtelRoot({ streamId: userMessageId, diff --git a/apps/sim/lib/copilot/chat/process-contents.ts b/apps/sim/lib/copilot/chat/process-contents.ts index f62109e9202..c82217c90f4 100644 --- a/apps/sim/lib/copilot/chat/process-contents.ts +++ b/apps/sim/lib/copilot/chat/process-contents.ts @@ -1,6 +1,10 @@ import { db } from '@sim/db' import { document, knowledgeBase, templates } from '@sim/db/schema' import { createLogger } from '@sim/logger' +import { + authorizeWorkflowByWorkspacePermission, + getActiveWorkflowRecord, +} from '@sim/workflow-authz' import { and, eq, isNull } from 'drizzle-orm' import { serializeFileMeta, @@ -11,10 +15,8 @@ import { getAllowedIntegrationsFromEnv } from '@/lib/core/config/feature-flags' import { getTableById } from '@/lib/table/service' import { canAccessTemplate } from '@/lib/templates/permissions' import { getWorkspaceFile } from '@/lib/uploads/contexts/workspace/workspace-file-manager' -import { getActiveWorkflowRecord } from '@/lib/workflows/active-context' import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils' import { sanitizeForCopilot } from '@/lib/workflows/sanitization/json-sanitizer' -import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils' import { checkKnowledgeBaseAccess } from '@/app/api/knowledge/utils' import { isHiddenFromDisplay } from '@/blocks/types' import { getUserPermissionConfig } from '@/ee/access-control/utils/permission-check' diff --git a/apps/sim/lib/copilot/request/http.ts b/apps/sim/lib/copilot/request/http.ts index 713e01eabb8..4f2ec7de374 100644 --- a/apps/sim/lib/copilot/request/http.ts +++ b/apps/sim/lib/copilot/request/http.ts @@ -1,10 +1,10 @@ +import { safeCompare } from '@sim/security/compare' import { generateId } from '@sim/utils/id' import type { NextRequest } from 'next/server' import { NextResponse } from 'next/server' import { getSession } from '@/lib/auth' import { ASYNC_TOOL_CONFIRMATION_STATUS } from '@/lib/copilot/async-runs/lifecycle' import { env } from '@/lib/core/config/env' -import { safeCompare } from '@/lib/core/security/encryption' import { generateRequestId } from '@/lib/core/utils/request' export const NotificationStatus = { diff --git a/apps/sim/lib/copilot/request/lifecycle/finalize.ts b/apps/sim/lib/copilot/request/lifecycle/finalize.ts index 319a5717f83..56c34efaf1e 100644 --- a/apps/sim/lib/copilot/request/lifecycle/finalize.ts +++ b/apps/sim/lib/copilot/request/lifecycle/finalize.ts @@ -64,7 +64,7 @@ export async function finalizeStream( span.setStatus({ code: SpanStatusCode.OK }) } } catch (error) { - span.recordException(error instanceof Error ? error : new Error(String(error))) + span.recordException(toError(error)) span.setStatus({ code: SpanStatusCode.ERROR, message: 'finalize threw' }) throw error } finally { diff --git a/apps/sim/lib/copilot/request/lifecycle/run.ts b/apps/sim/lib/copilot/request/lifecycle/run.ts index f39ddf6a9c5..bc5ab1da6d0 100644 --- a/apps/sim/lib/copilot/request/lifecycle/run.ts +++ b/apps/sim/lib/copilot/request/lifecycle/run.ts @@ -1,6 +1,7 @@ import type { Context } from '@opentelemetry/api' import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' +import { sleep } from '@sim/utils/helpers' import { generateId } from '@sim/utils/id' import { createRunSegment, updateRunStatus } from '@/lib/copilot/async-runs/repository' import { SIM_AGENT_API_URL, SIM_AGENT_VERSION } from '@/lib/copilot/constants' @@ -556,7 +557,7 @@ function isRetryableStreamError(error: unknown): boolean { function sleepWithAbort(ms: number, abortSignal?: AbortSignal): Promise { if (!abortSignal) { - return new Promise((resolve) => setTimeout(resolve, ms)) + return sleep(ms) } if (abortSignal.aborted) { return Promise.resolve() diff --git a/apps/sim/lib/copilot/request/otel.ts b/apps/sim/lib/copilot/request/otel.ts index 57d21d571f3..5ceb78c8a73 100644 --- a/apps/sim/lib/copilot/request/otel.ts +++ b/apps/sim/lib/copilot/request/otel.ts @@ -10,6 +10,7 @@ import { TraceFlags, trace, } from '@opentelemetry/api' +import { toError } from '@sim/utils/errors' import { RequestTraceV1Outcome } from '@/lib/copilot/generated/request-trace-v1' import { CopilotBranchKind, @@ -92,12 +93,12 @@ export function isActionableErrorStatus(code: number): boolean { // client disconnect, internal timeout, uncategorized AbortError — // becomes a real error that the dashboards will surface. export function markSpanForError(span: Span, error: unknown): void { - const asError = error instanceof Error ? error : new Error(String(error)) + const asError = toError(error) span.recordException(asError) if (!isExplicitUserStopError(error)) { span.setStatus({ code: SpanStatusCode.ERROR, - message: error instanceof Error ? error.message : String(error), + message: asError.message, }) } } diff --git a/apps/sim/lib/copilot/request/tools/tables.ts b/apps/sim/lib/copilot/request/tools/tables.ts index 1d1760b7f46..772c4ba03ba 100644 --- a/apps/sim/lib/copilot/request/tools/tables.ts +++ b/apps/sim/lib/copilot/request/tools/tables.ts @@ -2,6 +2,7 @@ import { db } from '@sim/db' import { userTableRows } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' +import { generateId } from '@sim/utils/id' import { parse as csvParse } from 'csv-parse/sync' import { eq } from 'drizzle-orm' import { FunctionExecute, Read as ReadTool } from '@/lib/copilot/generated/tool-catalog-v1' @@ -107,7 +108,7 @@ export async function maybeWriteOutputToTable( } const chunk = rows.slice(i, i + BATCH_CHUNK_SIZE) const values = chunk.map((rowData, j) => ({ - id: `row_${crypto.randomUUID().replace(/-/g, '')}`, + id: `row_${generateId().replace(/-/g, '')}`, tableId: outputTable, workspaceId: context.workspaceId!, data: rowData, @@ -251,7 +252,7 @@ export async function maybeWriteReadCsvToTable( } const chunk = rows.slice(i, i + BATCH_CHUNK_SIZE) const values = chunk.map((rowData, j) => ({ - id: `row_${crypto.randomUUID().replace(/-/g, '')}`, + id: `row_${generateId().replace(/-/g, '')}`, tableId: outputTable, workspaceId: context.workspaceId!, data: rowData, diff --git a/apps/sim/lib/copilot/tools/handlers/access.ts b/apps/sim/lib/copilot/tools/handlers/access.ts index b989ea34396..a435511a2f8 100644 --- a/apps/sim/lib/copilot/tools/handlers/access.ts +++ b/apps/sim/lib/copilot/tools/handlers/access.ts @@ -1,7 +1,8 @@ import { db } from '@sim/db' import { permissions, workspace } from '@sim/db/schema' +import { authorizeWorkflowByWorkspacePermission } from '@sim/workflow-authz' import { and, desc, eq, isNull } from 'drizzle-orm' -import { authorizeWorkflowByWorkspacePermission, type getWorkflowById } from '@/lib/workflows/utils' +import type { getWorkflowById } from '@/lib/workflows/utils' import { checkWorkspaceAccess, getUserEntityPermissions } from '@/lib/workspaces/permissions/utils' type WorkflowRecord = NonNullable>> diff --git a/apps/sim/lib/copilot/tools/handlers/deployment/deploy.ts b/apps/sim/lib/copilot/tools/handlers/deployment/deploy.ts index b57bf076f89..149b0084d80 100644 --- a/apps/sim/lib/copilot/tools/handlers/deployment/deploy.ts +++ b/apps/sim/lib/copilot/tools/handlers/deployment/deploy.ts @@ -1,9 +1,9 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { chat, workflowMcpServer, workflowMcpTool } from '@sim/db/schema' import { toError } from '@sim/utils/errors' import { generateId } from '@sim/utils/id' import { and, eq, isNull } from 'drizzle-orm' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import type { ExecutionContext, ToolCallResult } from '@/lib/copilot/request/types' import { getBaseUrl } from '@/lib/core/utils/urls' import { mcpPubSub } from '@/lib/mcp/pubsub' diff --git a/apps/sim/lib/copilot/tools/handlers/deployment/manage.test.ts b/apps/sim/lib/copilot/tools/handlers/deployment/manage.test.ts index 785fc7bca39..6af4a09b638 100644 --- a/apps/sim/lib/copilot/tools/handlers/deployment/manage.test.ts +++ b/apps/sim/lib/copilot/tools/handlers/deployment/manage.test.ts @@ -26,7 +26,7 @@ vi.mock('@sim/db', () => ({ workflowMcpTool: {}, })) -vi.mock('@/lib/audit/log', () => auditMock) +vi.mock('@sim/audit', () => auditMock) vi.mock('@/lib/mcp/pubsub', () => ({ mcpPubSub: { diff --git a/apps/sim/lib/copilot/tools/handlers/deployment/manage.ts b/apps/sim/lib/copilot/tools/handlers/deployment/manage.ts index cde7a541780..3953878c886 100644 --- a/apps/sim/lib/copilot/tools/handlers/deployment/manage.ts +++ b/apps/sim/lib/copilot/tools/handlers/deployment/manage.ts @@ -1,3 +1,4 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { chat, @@ -9,7 +10,6 @@ import { import { toError } from '@sim/utils/errors' import { generateId } from '@sim/utils/id' import { and, eq, inArray, isNull } from 'drizzle-orm' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import type { ExecutionContext, ToolCallResult } from '@/lib/copilot/request/types' import { mcpPubSub } from '@/lib/mcp/pubsub' import { generateParameterSchemaForWorkflow } from '@/lib/mcp/workflow-mcp-sync' diff --git a/apps/sim/lib/copilot/tools/handlers/jobs.ts b/apps/sim/lib/copilot/tools/handlers/jobs.ts index eb797a771f4..6f70ac1fa24 100644 --- a/apps/sim/lib/copilot/tools/handlers/jobs.ts +++ b/apps/sim/lib/copilot/tools/handlers/jobs.ts @@ -1,10 +1,10 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { copilotChats, workflowSchedule } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' import { generateId } from '@sim/utils/id' import { and, eq, isNull } from 'drizzle-orm' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import type { ExecutionContext, ToolCallResult } from '@/lib/copilot/request/types' import { parseCronToHumanReadable, validateCronExpression } from '@/lib/workflows/schedules/utils' diff --git a/apps/sim/lib/copilot/tools/handlers/materialize-file.ts b/apps/sim/lib/copilot/tools/handlers/materialize-file.ts index 7db12ce4b5d..f7341038b90 100644 --- a/apps/sim/lib/copilot/tools/handlers/materialize-file.ts +++ b/apps/sim/lib/copilot/tools/handlers/materialize-file.ts @@ -1,10 +1,10 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { workflow, workspaceFiles } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' import { generateId } from '@sim/utils/id' import { and, eq, isNull } from 'drizzle-orm' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import type { ExecutionContext, ToolCallResult } from '@/lib/copilot/request/types' import { findMothershipUploadRowByChatAndName } from '@/lib/copilot/tools/handlers/upload-file-reader' import { getServePathPrefix } from '@/lib/uploads' diff --git a/apps/sim/lib/copilot/tools/handlers/oauth.ts b/apps/sim/lib/copilot/tools/handlers/oauth.ts index 2fb2e0eb6f8..af410e9cf1c 100644 --- a/apps/sim/lib/copilot/tools/handlers/oauth.ts +++ b/apps/sim/lib/copilot/tools/handlers/oauth.ts @@ -1,6 +1,7 @@ import { db } from '@sim/db' import { pendingCredentialDraft, user } from '@sim/db/schema' import { toError } from '@sim/utils/errors' +import { generateId } from '@sim/utils/id' import { and, eq, lt } from 'drizzle-orm' import type { ExecutionContext, ToolCallResult } from '@/lib/copilot/request/types' import { getBaseUrl } from '@/lib/core/utils/urls' @@ -140,7 +141,7 @@ export async function generateOAuthLink( await db .insert(pendingCredentialDraft) .values({ - id: crypto.randomUUID(), + id: generateId(), userId, workspaceId, providerId, diff --git a/apps/sim/lib/copilot/tools/handlers/workflow/mutations.ts b/apps/sim/lib/copilot/tools/handlers/workflow/mutations.ts index ef3a652c38d..7e1d8281830 100644 --- a/apps/sim/lib/copilot/tools/handlers/workflow/mutations.ts +++ b/apps/sim/lib/copilot/tools/handlers/workflow/mutations.ts @@ -1,10 +1,10 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db, workflow as workflowTable } from '@sim/db' import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' import { generateId } from '@sim/utils/id' import { eq } from 'drizzle-orm' import { createWorkspaceApiKey } from '@/lib/api-key/auth' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import type { ExecutionContext, ToolCallResult } from '@/lib/copilot/request/types' import { env } from '@/lib/core/config/env' import { generateRequestId } from '@/lib/core/utils/request' diff --git a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/index.ts b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/index.ts index 974388e9ece..3c6367fb638 100644 --- a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/index.ts +++ b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/index.ts @@ -2,6 +2,7 @@ import { db } from '@sim/db' import { workflow as workflowTable } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' +import { authorizeWorkflowByWorkspacePermission } from '@sim/workflow-authz' import { eq } from 'drizzle-orm' import { EditWorkflow } from '@/lib/copilot/generated/tool-catalog-v1' import { @@ -26,7 +27,6 @@ import { saveWorkflowToNormalizedTables, } from '@/lib/workflows/persistence/utils' import { validateWorkflowState } from '@/lib/workflows/sanitization/validation' -import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils' import { getUserPermissionConfig } from '@/ee/access-control/utils/permission-check' import { generateLoopBlocks, generateParallelBlocks } from '@/stores/workflows/workflow/utils' import { normalizeWorkflowState } from '@/stores/workflows/workflow/validation' diff --git a/apps/sim/lib/copilot/tools/server/workflow/get-workflow-logs.ts b/apps/sim/lib/copilot/tools/server/workflow/get-workflow-logs.ts index 2258ec101df..3ab0cc2d573 100644 --- a/apps/sim/lib/copilot/tools/server/workflow/get-workflow-logs.ts +++ b/apps/sim/lib/copilot/tools/server/workflow/get-workflow-logs.ts @@ -1,10 +1,10 @@ import { db } from '@sim/db' import { workflowExecutionLogs } from '@sim/db/schema' import { createLogger } from '@sim/logger' +import { authorizeWorkflowByWorkspacePermission } from '@sim/workflow-authz' import { and, desc, eq } from 'drizzle-orm' import { GetWorkflowLogs } from '@/lib/copilot/generated/tool-catalog-v1' import type { BaseServerTool } from '@/lib/copilot/tools/server/base-tool' -import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils' import type { TraceSpan } from '@/stores/logs/filters/types' const logger = createLogger('GetWorkflowLogsServerTool') diff --git a/apps/sim/lib/core/config/env.ts b/apps/sim/lib/core/config/env.ts index ea14dcad181..0acd2c4e9e3 100644 --- a/apps/sim/lib/core/config/env.ts +++ b/apps/sim/lib/core/config/env.ts @@ -248,7 +248,6 @@ export const env = createEnv({ // Real-time Communication SOCKET_SERVER_URL: z.string().url().optional(), // WebSocket server URL for real-time features - SOCKET_PORT: z.number().optional(), // Port for WebSocket server PORT: z.number().optional(), // Main application port INTERNAL_API_BASE_URL: z.string().optional(), // Optional internal base URL for server-side self-calls; must include protocol if set (e.g., http://sim-app.namespace.svc.cluster.local:3000) ALLOWED_ORIGINS: z.string().optional(), // CORS allowed origins diff --git a/apps/sim/lib/core/security/deployment.ts b/apps/sim/lib/core/security/deployment.ts index 10a0781f83a..c3658b9d39a 100644 --- a/apps/sim/lib/core/security/deployment.ts +++ b/apps/sim/lib/core/security/deployment.ts @@ -1,4 +1,6 @@ -import { createHash, createHmac, timingSafeEqual } from 'crypto' +import { safeCompare } from '@sim/security/compare' +import { sha256Hex } from '@sim/security/hash' +import { hmacSha256Hex } from '@sim/security/hmac' import type { NextRequest, NextResponse } from 'next/server' import { env } from '@/lib/core/config/env' import { isDev } from '@/lib/core/config/feature-flags' @@ -9,12 +11,12 @@ import { isDev } from '@/lib/core/config/feature-flags' */ function signPayload(payload: string): string { - return createHmac('sha256', env.BETTER_AUTH_SECRET).update(payload).digest('hex') + return hmacSha256Hex(payload, env.BETTER_AUTH_SECRET) } function passwordSlot(encryptedPassword?: string | null): string { if (!encryptedPassword) return '' - return createHash('sha256').update(encryptedPassword).digest('hex').slice(0, 8) + return sha256Hex(encryptedPassword).slice(0, 8) } function generateAuthToken( @@ -46,10 +48,7 @@ export function validateAuthToken( const sig = decoded.slice(lastColon + 1) const expectedSig = signPayload(payload) - if ( - sig.length !== expectedSig.length || - !timingSafeEqual(Buffer.from(sig), Buffer.from(expectedSig)) - ) { + if (!safeCompare(sig, expectedSig)) { return false } diff --git a/apps/sim/lib/core/security/encryption.ts b/apps/sim/lib/core/security/encryption.ts index 811a60ada48..3d2e7390ec1 100644 --- a/apps/sim/lib/core/security/encryption.ts +++ b/apps/sim/lib/core/security/encryption.ts @@ -1,5 +1,6 @@ -import { createCipheriv, createDecipheriv, createHmac, randomBytes, timingSafeEqual } from 'crypto' import { createLogger } from '@sim/logger' +import { decrypt, encrypt } from '@sim/security/encryption' +import { toError } from '@sim/utils/errors' import { env } from '@/lib/core/config/env' const logger = createLogger('Encryption') @@ -13,59 +14,23 @@ function getEncryptionKey(): Buffer { } /** - * Encrypts a secret using AES-256-GCM + * Encrypts a secret using AES-256-GCM with the app's `ENCRYPTION_KEY`. * @param secret - The secret to encrypt - * @returns A promise that resolves to an object containing the encrypted secret and IV + * @returns A promise resolving to the encrypted value (`iv:ciphertext:authTag`) and the IV. */ export async function encryptSecret(secret: string): Promise<{ encrypted: string; iv: string }> { - const iv = randomBytes(16) - const key = getEncryptionKey() - - const cipher = createCipheriv('aes-256-gcm', key, iv, { authTagLength: 16 }) - let encrypted = cipher.update(secret, 'utf8', 'hex') - encrypted += cipher.final('hex') - - const authTag = cipher.getAuthTag() - const ivHex = iv.toString('hex') - - // Format: iv:encrypted:authTag - return { - encrypted: `${ivHex}:${encrypted}:${authTag.toString('hex')}`, - iv: ivHex, - } + return encrypt(secret, getEncryptionKey()) } /** - * Decrypts an encrypted secret - * @param encryptedValue - The encrypted value in format "iv:encrypted:authTag" - * @returns A promise that resolves to an object containing the decrypted secret + * Decrypts a secret previously produced by {@link encryptSecret}. Logs and + * rethrows on malformed input or tampered ciphertext. */ export async function decryptSecret(encryptedValue: string): Promise<{ decrypted: string }> { - const parts = encryptedValue.split(':') - const ivHex = parts[0] - const authTagHex = parts[parts.length - 1] - const encrypted = parts.slice(1, -1).join(':') - - if (!ivHex || !encrypted || !authTagHex) { - throw new Error('Invalid encrypted value format. Expected "iv:encrypted:authTag"') - } - - const key = getEncryptionKey() - const iv = Buffer.from(ivHex, 'hex') - const authTag = Buffer.from(authTagHex, 'hex') - try { - const decipher = createDecipheriv('aes-256-gcm', key, iv, { authTagLength: 16 }) - decipher.setAuthTag(authTag) - - let decrypted = decipher.update(encrypted, 'hex', 'utf8') - decrypted += decipher.final('utf8') - - return { decrypted } - } catch (error: unknown) { - logger.error('Decryption error:', { - error: error instanceof Error ? error.message : 'Unknown error', - }) + return await decrypt(encryptedValue, getEncryptionKey()) + } catch (error) { + logger.error('Decryption error:', { error: toError(error).message }) throw error } } @@ -85,17 +50,3 @@ export function generatePassword(length = 24): string { return result } - -/** - * Compares two strings in constant time to prevent timing attacks. - * Used for HMAC signature validation. - * @param a - First string to compare - * @param b - Second string to compare - * @returns True if strings are equal, false otherwise - */ -export function safeCompare(a: string, b: string): boolean { - const key = 'safeCompare' - const ha = createHmac('sha256', key).update(a).digest() - const hb = createHmac('sha256', key).update(b).digest() - return timingSafeEqual(ha, hb) -} diff --git a/apps/sim/lib/execution/preprocessing.test.ts b/apps/sim/lib/execution/preprocessing.test.ts index a90f9368ffb..a74950ec982 100644 --- a/apps/sim/lib/execution/preprocessing.test.ts +++ b/apps/sim/lib/execution/preprocessing.test.ts @@ -28,7 +28,7 @@ vi.mock('@/lib/workspaces/utils', () => ({ getWorkspaceBilledAccountUserId: mockGetWorkspaceBilledAccountUserId, })) -vi.mock('@/lib/workflows/active-context', () => ({ +vi.mock('@sim/workflow-authz', () => ({ getActiveWorkflowRecord: vi.fn().mockResolvedValue({ id: 'workflow-1', workspaceId: 'workspace-1', diff --git a/apps/sim/lib/execution/preprocessing.ts b/apps/sim/lib/execution/preprocessing.ts index 2aab6c5d6a1..567dbede2a8 100644 --- a/apps/sim/lib/execution/preprocessing.ts +++ b/apps/sim/lib/execution/preprocessing.ts @@ -1,5 +1,6 @@ import type { workflow } from '@sim/db/schema' import { createLogger } from '@sim/logger' +import { getActiveWorkflowRecord } from '@sim/workflow-authz' import { checkServerSideUsageLimits } from '@/lib/billing/calculations/usage-monitor' import type { HighestPrioritySubscription } from '@/lib/billing/core/plan' import { getHighestPrioritySubscription } from '@/lib/billing/core/subscription' @@ -7,7 +8,6 @@ import { getExecutionTimeout } from '@/lib/core/execution-limits' import { RateLimiter } from '@/lib/core/rate-limiter/rate-limiter' import type { SubscriptionPlan } from '@/lib/core/rate-limiter/types' import { LoggingSession, type SessionStartParams } from '@/lib/logs/execution/logging-session' -import { getActiveWorkflowRecord } from '@/lib/workflows/active-context' import { getWorkspaceBilledAccountUserId } from '@/lib/workspaces/utils' import type { CoreTriggerType } from '@/stores/logs/filters/types' diff --git a/apps/sim/lib/execution/preprocessing.webhook-correlation.test.ts b/apps/sim/lib/execution/preprocessing.webhook-correlation.test.ts index 35195972dd5..2fea91bfe0a 100644 --- a/apps/sim/lib/execution/preprocessing.webhook-correlation.test.ts +++ b/apps/sim/lib/execution/preprocessing.webhook-correlation.test.ts @@ -28,7 +28,7 @@ vi.mock('@/lib/workspaces/utils', () => ({ getWorkspaceBilledAccountUserId: mockGetWorkspaceBilledAccountUserId, })) -vi.mock('@/lib/workflows/active-context', () => ({ +vi.mock('@sim/workflow-authz', () => ({ getActiveWorkflowRecord: vi.fn().mockResolvedValue({ id: 'workflow-1', workspaceId: 'workspace-1', diff --git a/apps/sim/lib/knowledge/chunks/service.ts b/apps/sim/lib/knowledge/chunks/service.ts index e8bfac6679e..5e3cf68918a 100644 --- a/apps/sim/lib/knowledge/chunks/service.ts +++ b/apps/sim/lib/knowledge/chunks/service.ts @@ -1,7 +1,7 @@ -import { createHash } from 'crypto' import { db } from '@sim/db' import { document, embedding, knowledgeBase } from '@sim/db/schema' import { createLogger } from '@sim/logger' +import { sha256Hex } from '@sim/security/hash' import { generateId } from '@sim/utils/id' import { and, asc, desc, eq, ilike, inArray, isNull, sql } from 'drizzle-orm' import type { @@ -155,7 +155,7 @@ export async function createChunk( knowledgeBaseId, documentId, chunkIndex: nextChunkIndex, - chunkHash: createHash('sha256').update(chunkData.content).digest('hex'), + chunkHash: sha256Hex(chunkData.content), content: chunkData.content, contentLength: chunkData.content.length, tokenCount: tokenCount.count, @@ -368,7 +368,7 @@ export async function updateChunk( dbUpdateData.content = content dbUpdateData.contentLength = newContentLength dbUpdateData.tokenCount = tokenCount.count - dbUpdateData.chunkHash = createHash('sha256').update(content).digest('hex') + dbUpdateData.chunkHash = sha256Hex(content) // Add the embedding field to the update data dbUpdateData.embedding = embeddings[0] } else { @@ -376,7 +376,7 @@ export async function updateChunk( dbUpdateData.content = content dbUpdateData.contentLength = newContentLength dbUpdateData.tokenCount = oldTokenCount // Keep the same token count if content is identical - dbUpdateData.chunkHash = createHash('sha256').update(content).digest('hex') + dbUpdateData.chunkHash = sha256Hex(content) } if (updateData.enabled !== undefined) { diff --git a/apps/sim/lib/knowledge/documents/service.ts b/apps/sim/lib/knowledge/documents/service.ts index 14a7858e006..84241c57725 100644 --- a/apps/sim/lib/knowledge/documents/service.ts +++ b/apps/sim/lib/knowledge/documents/service.ts @@ -1,4 +1,3 @@ -import crypto from 'crypto' import { db } from '@sim/db' import { document, @@ -8,6 +7,7 @@ import { knowledgeConnector, } from '@sim/db/schema' import { createLogger } from '@sim/logger' +import { sha256Hex } from '@sim/security/hash' import { toError } from '@sim/utils/errors' import { generateId } from '@sim/utils/id' import { tasks } from '@trigger.dev/sdk' @@ -520,7 +520,7 @@ export async function processDocumentAsync( knowledgeBaseId, documentId, chunkIndex, - chunkHash: crypto.createHash('sha256').update(chunk.text).digest('hex'), + chunkHash: sha256Hex(chunk.text), content: chunk.text, contentLength: chunk.text.length, tokenCount: Math.ceil(chunk.text.length / 4), diff --git a/apps/sim/lib/logs/events.ts b/apps/sim/lib/logs/events.ts index 02703ba7ce0..5a40fab190d 100644 --- a/apps/sim/lib/logs/events.ts +++ b/apps/sim/lib/logs/events.ts @@ -2,6 +2,7 @@ import { db } from '@sim/db' import { workspaceNotificationDelivery, workspaceNotificationSubscription } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { generateId } from '@sim/utils/id' +import { getActiveWorkflowContext } from '@sim/workflow-authz' import { and, eq, or, sql } from 'drizzle-orm' import { isTriggerDevEnabled } from '@/lib/core/config/feature-flags' import type { WorkflowExecutionLog } from '@/lib/logs/types' @@ -10,7 +11,6 @@ import { type AlertConfig, shouldTriggerAlert, } from '@/lib/notifications/alert-rules' -import { getActiveWorkflowContext } from '@/lib/workflows/active-context' import { executeNotificationDelivery, workspaceNotificationDeliveryTask, diff --git a/apps/sim/lib/logs/execution/snapshot/service.ts b/apps/sim/lib/logs/execution/snapshot/service.ts index 82327c1903e..ea6cec7e6d1 100644 --- a/apps/sim/lib/logs/execution/snapshot/service.ts +++ b/apps/sim/lib/logs/execution/snapshot/service.ts @@ -1,7 +1,7 @@ -import { createHash } from 'crypto' import { db } from '@sim/db' import { workflowExecutionLogs, workflowExecutionSnapshots } from '@sim/db/schema' import { createLogger } from '@sim/logger' +import { sha256Hex } from '@sim/security/hash' import { generateId } from '@sim/utils/id' import { and, eq, lt, notExists, sql } from 'drizzle-orm' import type { @@ -85,7 +85,7 @@ export class SnapshotService implements ISnapshotService { computeStateHash(state: WorkflowState): string { const normalizedState = normalizeWorkflowState(state) const stateString = normalizedStringify(normalizedState) - return createHash('sha256').update(stateString).digest('hex') + return sha256Hex(stateString) } async cleanupOrphanedSnapshots(olderThanDays: number): Promise { diff --git a/apps/sim/lib/messaging/email/unsubscribe.ts b/apps/sim/lib/messaging/email/unsubscribe.ts index 30ace1d69a4..3c02fdfe3a3 100644 --- a/apps/sim/lib/messaging/email/unsubscribe.ts +++ b/apps/sim/lib/messaging/email/unsubscribe.ts @@ -1,7 +1,8 @@ -import { createHash, randomBytes } from 'crypto' +import { randomBytes } from 'crypto' import { db } from '@sim/db' import { settings, user } from '@sim/db/schema' import { createLogger } from '@sim/logger' +import { sha256Hex } from '@sim/security/hash' import { eq } from 'drizzle-orm' import { env } from '@/lib/core/config/env' import type { EmailType } from '@/lib/messaging/email/mailer' @@ -20,9 +21,7 @@ export interface EmailPreferences { */ export function generateUnsubscribeToken(email: string, emailType = 'marketing'): string { const salt = randomBytes(16).toString('hex') - const hash = createHash('sha256') - .update(`${email}:${salt}:${emailType}:${env.BETTER_AUTH_SECRET}`) - .digest('hex') + const hash = sha256Hex(`${email}:${salt}:${emailType}:${env.BETTER_AUTH_SECRET}`) return `${salt}:${hash}:${emailType}` } @@ -40,9 +39,7 @@ export function verifyUnsubscribeToken( if (parts.length === 2) { const [salt, expectedHash] = parts - const hash = createHash('sha256') - .update(`${email}:${salt}:${env.BETTER_AUTH_SECRET}`) - .digest('hex') + const hash = sha256Hex(`${email}:${salt}:${env.BETTER_AUTH_SECRET}`) return { valid: hash === expectedHash, emailType: 'marketing' } } @@ -50,9 +47,7 @@ export function verifyUnsubscribeToken( const [salt, expectedHash, emailType] = parts if (!salt || !expectedHash || !emailType) return { valid: false } - const hash = createHash('sha256') - .update(`${email}:${salt}:${emailType}:${env.BETTER_AUTH_SECRET}`) - .digest('hex') + const hash = sha256Hex(`${email}:${salt}:${emailType}:${env.BETTER_AUTH_SECRET}`) return { valid: hash === expectedHash, emailType } } catch (error) { diff --git a/apps/sim/lib/webhooks/processor.test.ts b/apps/sim/lib/webhooks/processor.test.ts index 27840dbaf0e..03123a2bd3d 100644 --- a/apps/sim/lib/webhooks/processor.test.ts +++ b/apps/sim/lib/webhooks/processor.test.ts @@ -56,7 +56,7 @@ vi.mock('@/lib/core/async-jobs', () => ({ vi.mock('@/lib/core/config/feature-flags', () => featureFlagsMock) -vi.mock('@/lib/core/security/encryption', () => ({ +vi.mock('@sim/security/compare', () => ({ safeCompare: vi.fn().mockReturnValue(true), })) diff --git a/apps/sim/lib/webhooks/providers/ashby.ts b/apps/sim/lib/webhooks/providers/ashby.ts index a6c112ff8d1..0580a899b8f 100644 --- a/apps/sim/lib/webhooks/providers/ashby.ts +++ b/apps/sim/lib/webhooks/providers/ashby.ts @@ -1,7 +1,7 @@ -import crypto from 'crypto' import { createLogger } from '@sim/logger' +import { safeCompare } from '@sim/security/compare' +import { hmacSha256Hex } from '@sim/security/hmac' import { generateId } from '@sim/utils/id' -import { safeCompare } from '@/lib/core/security/encryption' import { getNotificationUrl, getProviderConfig } from '@/lib/webhooks/provider-subscription-utils' import type { DeleteSubscriptionContext, @@ -24,7 +24,7 @@ function validateAshbySignature(secretToken: string, signature: string, body: st return false } const providedSignature = signature.substring(7) - const computedHash = crypto.createHmac('sha256', secretToken).update(body, 'utf8').digest('hex') + const computedHash = hmacSha256Hex(body, secretToken) return safeCompare(computedHash, providedSignature) } catch (error) { logger.error('Error validating Ashby signature:', error) diff --git a/apps/sim/lib/webhooks/providers/attio.ts b/apps/sim/lib/webhooks/providers/attio.ts index 7ac8f87c7b5..ea9debff8f5 100644 --- a/apps/sim/lib/webhooks/providers/attio.ts +++ b/apps/sim/lib/webhooks/providers/attio.ts @@ -1,8 +1,8 @@ -import crypto from 'crypto' import { createLogger } from '@sim/logger' +import { safeCompare } from '@sim/security/compare' +import { hmacSha256Hex } from '@sim/security/hmac' import { toError } from '@sim/utils/errors' import { NextResponse } from 'next/server' -import { safeCompare } from '@/lib/core/security/encryption' import { getBaseUrl } from '@/lib/core/utils/urls' import { getCredentialOwner, getProviderConfig } from '@/lib/webhooks/provider-subscription-utils' import type { @@ -29,7 +29,7 @@ function validateAttioSignature(secret: string, signature: string, body: string) }) return false } - const computedHash = crypto.createHmac('sha256', secret).update(body, 'utf8').digest('hex') + const computedHash = hmacSha256Hex(body, secret) logger.debug('Attio signature comparison', { computedSignature: `${computedHash.substring(0, 10)}...`, providedSignature: `${signature.substring(0, 10)}...`, diff --git a/apps/sim/lib/webhooks/providers/calcom.ts b/apps/sim/lib/webhooks/providers/calcom.ts index b018b16f581..798cea64f20 100644 --- a/apps/sim/lib/webhooks/providers/calcom.ts +++ b/apps/sim/lib/webhooks/providers/calcom.ts @@ -1,6 +1,6 @@ -import crypto from 'crypto' import { createLogger } from '@sim/logger' -import { safeCompare } from '@/lib/core/security/encryption' +import { safeCompare } from '@sim/security/compare' +import { hmacSha256Hex } from '@sim/security/hmac' import type { WebhookProviderHandler } from '@/lib/webhooks/providers/types' import { createHmacVerifier } from '@/lib/webhooks/providers/utils' @@ -22,7 +22,7 @@ function validateCalcomSignature(secret: string, signature: string, body: string } else { providedSignature = signature } - const computedHash = crypto.createHmac('sha256', secret).update(body, 'utf8').digest('hex') + const computedHash = hmacSha256Hex(body, secret) logger.debug('Cal.com signature comparison', { computedSignature: `${computedHash.substring(0, 10)}...`, providedSignature: `${providedSignature.substring(0, 10)}...`, diff --git a/apps/sim/lib/webhooks/providers/circleback.ts b/apps/sim/lib/webhooks/providers/circleback.ts index 1beb8a6814f..ef0c9facd2e 100644 --- a/apps/sim/lib/webhooks/providers/circleback.ts +++ b/apps/sim/lib/webhooks/providers/circleback.ts @@ -1,6 +1,6 @@ -import crypto from 'crypto' import { createLogger } from '@sim/logger' -import { safeCompare } from '@/lib/core/security/encryption' +import { safeCompare } from '@sim/security/compare' +import { hmacSha256Hex } from '@sim/security/hmac' import type { FormatInputContext, FormatInputResult, @@ -20,7 +20,7 @@ function validateCirclebackSignature(secret: string, signature: string, body: st }) return false } - const computedHash = crypto.createHmac('sha256', secret).update(body, 'utf8').digest('hex') + const computedHash = hmacSha256Hex(body, secret) logger.debug('Circleback signature comparison', { computedSignature: `${computedHash.substring(0, 10)}...`, providedSignature: `${signature.substring(0, 10)}...`, diff --git a/apps/sim/lib/webhooks/providers/fireflies.ts b/apps/sim/lib/webhooks/providers/fireflies.ts index 0897a7ff161..e7e67da0599 100644 --- a/apps/sim/lib/webhooks/providers/fireflies.ts +++ b/apps/sim/lib/webhooks/providers/fireflies.ts @@ -1,6 +1,6 @@ -import crypto from 'crypto' import { createLogger } from '@sim/logger' -import { safeCompare } from '@/lib/core/security/encryption' +import { safeCompare } from '@sim/security/compare' +import { hmacSha256Hex } from '@sim/security/hmac' import type { FormatInputContext, FormatInputResult, @@ -27,7 +27,7 @@ function validateFirefliesSignature(secret: string, signature: string, body: str return false } const providedSignature = signature.substring(7) - const computedHash = crypto.createHmac('sha256', secret).update(body, 'utf8').digest('hex') + const computedHash = hmacSha256Hex(body, secret) logger.debug('Fireflies signature comparison', { computedSignature: `${computedHash.substring(0, 10)}...`, providedSignature: `${providedSignature.substring(0, 10)}...`, diff --git a/apps/sim/lib/webhooks/providers/github.ts b/apps/sim/lib/webhooks/providers/github.ts index a0fd90f2e6d..20d1a3fff33 100644 --- a/apps/sim/lib/webhooks/providers/github.ts +++ b/apps/sim/lib/webhooks/providers/github.ts @@ -1,7 +1,7 @@ import crypto from 'crypto' import { createLogger } from '@sim/logger' +import { safeCompare } from '@sim/security/compare' import { NextResponse } from 'next/server' -import { safeCompare } from '@/lib/core/security/encryption' import type { AuthContext, EventMatchContext, diff --git a/apps/sim/lib/webhooks/providers/gong.ts b/apps/sim/lib/webhooks/providers/gong.ts index f1c3f355dc3..399e9074d20 100644 --- a/apps/sim/lib/webhooks/providers/gong.ts +++ b/apps/sim/lib/webhooks/providers/gong.ts @@ -1,5 +1,5 @@ -import { createHash } from 'node:crypto' import { createLogger } from '@sim/logger' +import { sha256Hex } from '@sim/security/hash' import { toError } from '@sim/utils/errors' import * as jose from 'jose' import { NextResponse } from 'next/server' @@ -102,7 +102,7 @@ export async function verifyGongJwtAuth(ctx: AuthContext): Promise): string { - return crypto.createHash('sha256').update(stableSerialize(body), 'utf8').digest('hex') + return sha256Hex(stableSerialize(body)) } function pickRecordId(body: Record, record: Record): string { diff --git a/apps/sim/lib/webhooks/providers/slack.ts b/apps/sim/lib/webhooks/providers/slack.ts index a5488f0aa4e..a48032c08d5 100644 --- a/apps/sim/lib/webhooks/providers/slack.ts +++ b/apps/sim/lib/webhooks/providers/slack.ts @@ -1,8 +1,8 @@ -import crypto from 'crypto' import { createLogger } from '@sim/logger' +import { safeCompare } from '@sim/security/compare' +import { hmacSha256Hex } from '@sim/security/hmac' import { toError } from '@sim/utils/errors' import { NextResponse } from 'next/server' -import { safeCompare } from '@/lib/core/security/encryption' import { secureFetchWithPinnedIP, validateUrlWithDNS, @@ -207,10 +207,7 @@ function validateSlackSignature( const providedSignature = signature.substring(3) const basestring = `v0:${timestamp}:${rawBody}` - const computedHash = crypto - .createHmac('sha256', signingSecret) - .update(basestring, 'utf8') - .digest('hex') + const computedHash = hmacSha256Hex(basestring, signingSecret) return safeCompare(computedHash, providedSignature) } catch (error) { diff --git a/apps/sim/lib/webhooks/providers/twilio-voice.ts b/apps/sim/lib/webhooks/providers/twilio-voice.ts index be0417ef71d..543264cd730 100644 --- a/apps/sim/lib/webhooks/providers/twilio-voice.ts +++ b/apps/sim/lib/webhooks/providers/twilio-voice.ts @@ -1,7 +1,7 @@ import crypto from 'crypto' import { createLogger } from '@sim/logger' +import { safeCompare } from '@sim/security/compare' import { NextResponse } from 'next/server' -import { safeCompare } from '@/lib/core/security/encryption' import type { AuthContext, FormatInputContext, diff --git a/apps/sim/lib/webhooks/providers/typeform.ts b/apps/sim/lib/webhooks/providers/typeform.ts index 8c96d05907f..16df0e6c47d 100644 --- a/apps/sim/lib/webhooks/providers/typeform.ts +++ b/apps/sim/lib/webhooks/providers/typeform.ts @@ -1,6 +1,6 @@ -import crypto from 'crypto' import { createLogger } from '@sim/logger' -import { safeCompare } from '@/lib/core/security/encryption' +import { safeCompare } from '@sim/security/compare' +import { hmacSha256Base64 } from '@sim/security/hmac' import { getNotificationUrl, getProviderConfig } from '@/lib/webhooks/provider-subscription-utils' import type { DeleteSubscriptionContext, @@ -23,7 +23,7 @@ function validateTypeformSignature(secret: string, signature: string, body: stri return false } const providedSignature = signature.substring(7) - const computedHash = crypto.createHmac('sha256', secret).update(body, 'utf8').digest('base64') + const computedHash = hmacSha256Base64(body, secret) return safeCompare(computedHash, providedSignature) } catch (error) { logger.error('Error validating Typeform signature:', error) diff --git a/apps/sim/lib/webhooks/providers/utils.ts b/apps/sim/lib/webhooks/providers/utils.ts index f2a56047081..dff3db7ce1e 100644 --- a/apps/sim/lib/webhooks/providers/utils.ts +++ b/apps/sim/lib/webhooks/providers/utils.ts @@ -1,7 +1,7 @@ import type { Logger } from '@sim/logger' import { createLogger } from '@sim/logger' +import { safeCompare } from '@sim/security/compare' import { NextResponse } from 'next/server' -import { safeCompare } from '@/lib/core/security/encryption' import type { AuthContext, EventFilterContext } from '@/lib/webhooks/providers/types' const logger = createLogger('WebhookProviderAuth') diff --git a/apps/sim/lib/webhooks/providers/vercel.ts b/apps/sim/lib/webhooks/providers/vercel.ts index 218afb3d6fd..edf5f9d6220 100644 --- a/apps/sim/lib/webhooks/providers/vercel.ts +++ b/apps/sim/lib/webhooks/providers/vercel.ts @@ -1,7 +1,7 @@ import crypto from 'crypto' import { createLogger } from '@sim/logger' +import { safeCompare } from '@sim/security/compare' import { NextResponse } from 'next/server' -import { safeCompare } from '@/lib/core/security/encryption' import { getNotificationUrl, getProviderConfig } from '@/lib/webhooks/provider-subscription-utils' import type { AuthContext, diff --git a/apps/sim/lib/webhooks/providers/whatsapp.ts b/apps/sim/lib/webhooks/providers/whatsapp.ts index e1a76ef2188..6e20d4f7287 100644 --- a/apps/sim/lib/webhooks/providers/whatsapp.ts +++ b/apps/sim/lib/webhooks/providers/whatsapp.ts @@ -1,10 +1,11 @@ -import { createHash, createHmac } from 'crypto' import { db, workflowDeploymentVersion } from '@sim/db' import { webhook } from '@sim/db/schema' import { createLogger } from '@sim/logger' +import { safeCompare } from '@sim/security/compare' +import { sha256Hex } from '@sim/security/hash' +import { hmacSha256Hex } from '@sim/security/hmac' import { and, eq, isNull, or } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' -import { safeCompare } from '@/lib/core/security/encryption' import type { FormatInputContext, FormatInputResult, @@ -110,7 +111,7 @@ function validateWhatsAppSignature(secret: string, signature: string, body: stri } const providedSignature = signature.substring(7) - const computedSignature = createHmac('sha256', secret).update(body, 'utf8').digest('hex') + const computedSignature = hmacSha256Hex(body, secret) return safeCompare(computedSignature, providedSignature) } catch (error) { @@ -125,7 +126,7 @@ function buildWhatsAppIdempotencyKey(keys: Set): string | null { } const sortedKeys = Array.from(keys).sort() - const digest = createHash('sha256').update(sortedKeys.join('|'), 'utf8').digest('hex') + const digest = sha256Hex(sortedKeys.join('|')) return `whatsapp:${sortedKeys.length}:${digest}` } diff --git a/apps/sim/lib/webhooks/providers/zoom.ts b/apps/sim/lib/webhooks/providers/zoom.ts index 409d7c8f921..60f4e0ef749 100644 --- a/apps/sim/lib/webhooks/providers/zoom.ts +++ b/apps/sim/lib/webhooks/providers/zoom.ts @@ -1,11 +1,11 @@ -import crypto from 'crypto' import { db, webhook, workflow } from '@sim/db' import { createLogger } from '@sim/logger' +import { safeCompare } from '@sim/security/compare' +import { hmacSha256Hex } from '@sim/security/hmac' import { toError } from '@sim/utils/errors' import { and, eq } from 'drizzle-orm' import type { NextRequest } from 'next/server' import { NextResponse } from 'next/server' -import { safeCompare } from '@/lib/core/security/encryption' import { resolveEnvVarsInObject } from '@/lib/webhooks/env-resolver' import type { AuthContext, @@ -41,7 +41,7 @@ export function validateZoomSignature( } const message = `v0:${timestamp}:${body}` - const computedHash = crypto.createHmac('sha256', secretToken).update(message).digest('hex') + const computedHash = hmacSha256Hex(message, secretToken) const expectedSignature = `v0=${computedHash}` return safeCompare(expectedSignature, signature) @@ -206,10 +206,7 @@ export const zoomHandler: WebhookProviderHandler = { secretToken && validateZoomSignature(secretToken, signature, timestamp, bodyForSignature) ) { - const hashForValidate = crypto - .createHmac('sha256', secretToken) - .update(plainToken) - .digest('hex') + const hashForValidate = hmacSha256Hex(plainToken, secretToken) return NextResponse.json({ plainToken, diff --git a/apps/sim/lib/workflows/active-context.ts b/apps/sim/lib/workflows/active-context.ts deleted file mode 100644 index 612a00e4d37..00000000000 --- a/apps/sim/lib/workflows/active-context.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { db } from '@sim/db' -import { workflow, workspace } from '@sim/db/schema' -import { and, eq, isNull } from 'drizzle-orm' - -export type ActiveWorkflowRecord = typeof workflow.$inferSelect - -export interface ActiveWorkflowContext { - workflow: ActiveWorkflowRecord - workspaceId: string -} - -/** - * Returns the workflow and workspace context only when both are still active. - */ -export async function getActiveWorkflowContext( - workflowId: string -): Promise { - const rows = await db - .select({ - workflow, - workspaceId: workspace.id, - }) - .from(workflow) - .innerJoin(workspace, eq(workflow.workspaceId, workspace.id)) - .where( - and(eq(workflow.id, workflowId), isNull(workflow.archivedAt), isNull(workspace.archivedAt)) - ) - .limit(1) - - if (rows.length === 0) { - return null - } - - return { - workflow: rows[0].workflow, - workspaceId: rows[0].workspaceId, - } -} - -/** - * Returns the workflow row only when its parent workspace is also active. - */ -export async function getActiveWorkflowRecord( - workflowId: string -): Promise { - const context = await getActiveWorkflowContext(workflowId) - return context?.workflow ?? null -} - -export async function assertActiveWorkflowContext( - workflowId: string -): Promise { - const context = await getActiveWorkflowContext(workflowId) - if (!context) { - throw new Error(`Active workflow not found: ${workflowId}`) - } - return context -} diff --git a/apps/sim/lib/workflows/executor/execution-core.test.ts b/apps/sim/lib/workflows/executor/execution-core.test.ts index 86b3a085bdb..da8092d3366 100644 --- a/apps/sim/lib/workflows/executor/execution-core.test.ts +++ b/apps/sim/lib/workflows/executor/execution-core.test.ts @@ -59,7 +59,7 @@ vi.mock('@/lib/logs/execution/trace-spans/trace-spans', () => ({ vi.mock('@/lib/workflows/persistence/utils', () => workflowsPersistenceUtilsMock) -vi.mock('@/lib/workflows/subblocks', () => ({ +vi.mock('@sim/workflow-persistence/subblocks', () => ({ mergeSubblockStateWithValues: mergeSubblockStateWithValuesMock, })) diff --git a/apps/sim/lib/workflows/executor/execution-core.ts b/apps/sim/lib/workflows/executor/execution-core.ts index d115b17486d..13ff2510570 100644 --- a/apps/sim/lib/workflows/executor/execution-core.ts +++ b/apps/sim/lib/workflows/executor/execution-core.ts @@ -4,6 +4,7 @@ */ import { createLogger } from '@sim/logger' +import { mergeSubblockStateWithValues } from '@sim/workflow-persistence/subblocks' import type { Edge } from 'reactflow' import { z } from 'zod' import { getPersonalAndWorkspaceEnv } from '@/lib/environment/utils' @@ -14,7 +15,6 @@ import { loadDeployedWorkflowState, loadWorkflowFromNormalizedTables, } from '@/lib/workflows/persistence/utils' -import { mergeSubblockStateWithValues } from '@/lib/workflows/subblocks' import { TriggerUtils } from '@/lib/workflows/triggers/triggers' import { updateWorkflowRunCounts } from '@/lib/workflows/utils' import { Executor } from '@/executor' diff --git a/apps/sim/lib/workflows/orchestration/chat-deploy.ts b/apps/sim/lib/workflows/orchestration/chat-deploy.ts index e10323445d4..d8c93253167 100644 --- a/apps/sim/lib/workflows/orchestration/chat-deploy.ts +++ b/apps/sim/lib/workflows/orchestration/chat-deploy.ts @@ -1,9 +1,9 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { chat } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { generateId } from '@sim/utils/id' import { and, eq, isNull } from 'drizzle-orm' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { encryptSecret } from '@/lib/core/security/encryption' import { getBaseUrl } from '@/lib/core/utils/urls' import { performFullDeploy } from '@/lib/workflows/orchestration/deploy' diff --git a/apps/sim/lib/workflows/orchestration/deploy.ts b/apps/sim/lib/workflows/orchestration/deploy.ts index 4f81701b558..2b23ae23dd8 100644 --- a/apps/sim/lib/workflows/orchestration/deploy.ts +++ b/apps/sim/lib/workflows/orchestration/deploy.ts @@ -1,8 +1,8 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db, workflowDeploymentVersion, workflow as workflowTable } from '@sim/db' import { createLogger } from '@sim/logger' import { and, eq } from 'drizzle-orm' import { NextRequest } from 'next/server' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { env } from '@/lib/core/config/env' import { generateRequestId } from '@/lib/core/utils/request' import { getBaseUrl, getSocketServerUrl } from '@/lib/core/utils/urls' diff --git a/apps/sim/lib/workflows/orchestration/folder-lifecycle.ts b/apps/sim/lib/workflows/orchestration/folder-lifecycle.ts index 03264f20153..6d3f52fd49f 100644 --- a/apps/sim/lib/workflows/orchestration/folder-lifecycle.ts +++ b/apps/sim/lib/workflows/orchestration/folder-lifecycle.ts @@ -1,3 +1,4 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { a2aAgent, @@ -11,7 +12,6 @@ import { } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { and, eq, inArray, isNull } from 'drizzle-orm' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { archiveWorkflowsByIdsInWorkspace } from '@/lib/workflows/lifecycle' import type { OrchestrationErrorCode } from '@/lib/workflows/orchestration/types' diff --git a/apps/sim/lib/workflows/orchestration/workflow-lifecycle.ts b/apps/sim/lib/workflows/orchestration/workflow-lifecycle.ts index 3e757e6b2bf..eaf0b2a656a 100644 --- a/apps/sim/lib/workflows/orchestration/workflow-lifecycle.ts +++ b/apps/sim/lib/workflows/orchestration/workflow-lifecycle.ts @@ -1,8 +1,8 @@ +import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit' import { db } from '@sim/db' import { templates, workflow } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { and, eq, isNull } from 'drizzle-orm' -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' import { generateRequestId } from '@/lib/core/utils/request' import { archiveWorkflow } from '@/lib/workflows/lifecycle' import type { OrchestrationErrorCode } from '@/lib/workflows/orchestration/types' diff --git a/apps/sim/lib/workflows/persistence/duplicate.test.ts b/apps/sim/lib/workflows/persistence/duplicate.test.ts index e2aa9033520..0bd948cdeca 100644 --- a/apps/sim/lib/workflows/persistence/duplicate.test.ts +++ b/apps/sim/lib/workflows/persistence/duplicate.test.ts @@ -4,6 +4,7 @@ import { permissionsMock, permissionsMockFns, + workflowAuthzMockFns, workflowsUtilsMock, workflowsUtilsMockFns, } from '@sim/testing' @@ -11,7 +12,7 @@ import { drizzleOrmMock } from '@sim/testing/mocks' import { beforeEach, describe, expect, it, vi } from 'vitest' const mockAuthorizeWorkflowByWorkspacePermission = - workflowsUtilsMockFns.mockAuthorizeWorkflowByWorkspacePermission + workflowAuthzMockFns.mockAuthorizeWorkflowByWorkspacePermission const mockGetUserEntityPermissions = permissionsMockFns.mockGetUserEntityPermissions const { mockDb } = vi.hoisted(() => ({ diff --git a/apps/sim/lib/workflows/persistence/duplicate.ts b/apps/sim/lib/workflows/persistence/duplicate.ts index ff111d203a0..5386455bb1a 100644 --- a/apps/sim/lib/workflows/persistence/duplicate.ts +++ b/apps/sim/lib/workflows/persistence/duplicate.ts @@ -8,12 +8,10 @@ import { } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { generateId } from '@sim/utils/id' +import { authorizeWorkflowByWorkspacePermission } from '@sim/workflow-authz' import { and, eq, isNull, min } from 'drizzle-orm' import { remapConditionBlockIds, remapConditionEdgeHandle } from '@/lib/workflows/condition-ids' -import { - authorizeWorkflowByWorkspacePermission, - deduplicateWorkflowName, -} from '@/lib/workflows/utils' +import { deduplicateWorkflowName } from '@/lib/workflows/utils' import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils' import type { Variable } from '@/stores/variables/types' import type { LoopConfig, ParallelConfig } from '@/stores/workflows/workflow/types' diff --git a/apps/sim/lib/workflows/persistence/utils.ts b/apps/sim/lib/workflows/persistence/utils.ts index bbf939b78a5..80af8e9dad4 100644 --- a/apps/sim/lib/workflows/persistence/utils.ts +++ b/apps/sim/lib/workflows/persistence/utils.ts @@ -1,33 +1,29 @@ -import { - db, - workflow, - workflowBlocks, - workflowDeploymentVersion, - workflowEdges, - workflowSubflows, -} from '@sim/db' +import { db, workflow, workflowDeploymentVersion } from '@sim/db' import { credential } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { generateId } from '@sim/utils/id' -import type { InferInsertModel, InferSelectModel } from 'drizzle-orm' +import { getActiveWorkflowContext } from '@sim/workflow-authz' +import { + loadWorkflowFromNormalizedTablesRaw, + persistMigratedBlocks, +} from '@sim/workflow-persistence/load' +import { saveWorkflowToNormalizedTables as saveWorkflowToNormalizedTablesRaw } from '@sim/workflow-persistence/save' +import type { DbOrTx, NormalizedWorkflowData } from '@sim/workflow-persistence/types' +import type { BlockState, Loop, Parallel, WorkflowState } from '@sim/workflow-types/workflow' +import type { InferSelectModel } from 'drizzle-orm' import { and, desc, eq, inArray, sql } from 'drizzle-orm' import type { Edge } from 'reactflow' -import type { DbOrTx } from '@/lib/db/types' -import { getActiveWorkflowContext } from '@/lib/workflows/active-context' import { remapConditionBlockIds, remapConditionEdgeHandle } from '@/lib/workflows/condition-ids' import { backfillCanonicalModes, migrateSubblockIds, } from '@/lib/workflows/migrations/subblock-migrations' import { sanitizeAgentToolsInBlocks } from '@/lib/workflows/sanitization/validation' -import type { BlockState, Loop, Parallel, WorkflowState } from '@/stores/workflows/workflow/types' -import { SUBFLOW_TYPES } from '@/stores/workflows/workflow/types' -import { generateLoopBlocks, generateParallelBlocks } from '@/stores/workflows/workflow/utils' const logger = createLogger('WorkflowDBHelpers') +export type { DbOrTx, NormalizedWorkflowData } from '@sim/workflow-persistence/types' export type WorkflowDeploymentVersion = InferSelectModel -type SubflowInsert = InferInsertModel export interface WorkflowDeploymentVersionResponse { id: string @@ -40,14 +36,6 @@ export interface WorkflowDeploymentVersionResponse { deployedBy?: string | null } -export interface NormalizedWorkflowData { - blocks: Record - edges: Edge[] - loops: Record - parallels: Record - isFromNormalizedTables: boolean // Flag to indicate source (true = normalized tables, false = deployed state) -} - export interface DeployedWorkflowData extends NormalizedWorkflowData { deploymentVersionId: string variables?: Record @@ -188,10 +176,6 @@ const applyBlockMigrations = createMigrationPipeline([ /** * Migrates agent blocks from old format (systemPrompt/userPrompt) to new format (messages array) - * This ensures backward compatibility for workflows created before the messages-input refactor. - * - * @param blocks - Record of block states to migrate - * @returns Migrated blocks with messages array format for agent blocks */ export function migrateAgentBlocksToMessagesFormat( blocks: Record @@ -203,11 +187,9 @@ export function migrateAgentBlocksToMessagesFormat( const userPrompt = block.subBlocks.userPrompt?.value const messages = block.subBlocks.messages?.value - // Only migrate if old format exists and new format doesn't if ((systemPrompt || userPrompt) && !messages) { const newMessages: Array<{ role: string; content: string }> = [] - // Add system message first (industry standard) if (systemPrompt) { newMessages.push({ role: 'system', @@ -215,16 +197,13 @@ export function migrateAgentBlocksToMessagesFormat( }) } - // Add user message if (userPrompt) { let userContent = userPrompt - // Handle object format (e.g., { input: "..." }) if (typeof userContent === 'object' && userContent !== null) { if ('input' in userContent) { userContent = (userContent as any).input } else { - // If it's an object but doesn't have 'input', stringify it userContent = JSON.stringify(userContent) } } @@ -235,7 +214,6 @@ export function migrateAgentBlocksToMessagesFormat( }) } - // Return block with migrated messages subBlock return [ id, { @@ -259,11 +237,6 @@ export function migrateAgentBlocksToMessagesFormat( const CREDENTIAL_SUBBLOCK_IDS = new Set(['credential', 'triggerCredentials']) -/** - * Migrates legacy `account.id` values to `credential.id` in OAuth subblocks. - * Collects all potential legacy IDs in a single batch query for efficiency. - * Also migrates `tool.params.credential` in agent block tool arrays. - */ async function migrateCredentialIds( blocks: Record, workspaceId: string @@ -359,275 +332,60 @@ async function migrateCredentialIds( } /** - * Load workflow state from normalized tables - * Returns null if no data found (fallback to JSON blob) + * Load workflow from normalized tables and apply all block migrations + * (credential ID rewrites, agent message migration, subblock ID migrations, + * canonical-mode backfill, tool sanitization). Returns null if the workflow + * has not been migrated to normalized tables yet. */ export async function loadWorkflowFromNormalizedTables( workflowId: string ): Promise { - try { - const [blocks, edges, subflows, [workflowRow]] = await Promise.all([ - db.select().from(workflowBlocks).where(eq(workflowBlocks.workflowId, workflowId)), - db.select().from(workflowEdges).where(eq(workflowEdges.workflowId, workflowId)), - db.select().from(workflowSubflows).where(eq(workflowSubflows.workflowId, workflowId)), - db - .select({ workspaceId: workflow.workspaceId }) - .from(workflow) - .where(eq(workflow.id, workflowId)) - .limit(1), - ]) + const raw = await loadWorkflowFromNormalizedTablesRaw(workflowId) + if (!raw) return null - // If no blocks found, assume this workflow hasn't been migrated yet - if (blocks.length === 0) { - return null - } + const { blocks: finalBlocks, migrated } = await applyBlockMigrations(raw.blocks, raw.workspaceId) - // Convert blocks to the expected format - const blocksMap: Record = {} - blocks.forEach((block) => { - const blockData = block.data || {} - - const assembled: BlockState = { - id: block.id, - type: block.type, - name: block.name, - position: { - x: Number(block.positionX), - y: Number(block.positionY), - }, - enabled: block.enabled, - horizontalHandles: block.horizontalHandles, - advancedMode: block.advancedMode, - triggerMode: block.triggerMode, - height: Number(block.height), - subBlocks: (block.subBlocks as BlockState['subBlocks']) || {}, - outputs: (block.outputs as BlockState['outputs']) || {}, - data: blockData, - locked: block.locked, - } + if (migrated) { + Promise.resolve().then(() => persistMigratedBlocks(workflowId, raw.blocks, finalBlocks)) + } - blocksMap[block.id] = assembled - }) + const patchedLoops: Record = { ...raw.loops } + const patchedParallels: Record = { ...raw.parallels } - if (!workflowRow?.workspaceId) { - throw new Error(`Workflow ${workflowId} has no workspace`) + for (const id of Object.keys(raw.loops)) { + if (finalBlocks[id]) { + patchedLoops[id] = { ...raw.loops[id], enabled: finalBlocks[id].enabled ?? true } } - - const { blocks: finalBlocks, migrated } = await applyBlockMigrations( - blocksMap, - workflowRow.workspaceId - ) - - if (migrated) { - Promise.resolve().then(async () => { - try { - for (const [blockId, block] of Object.entries(finalBlocks)) { - if (block !== blocksMap[blockId]) { - await db - .update(workflowBlocks) - .set({ - subBlocks: block.subBlocks, - data: block.data, - updatedAt: new Date(), - }) - .where( - and(eq(workflowBlocks.id, blockId), eq(workflowBlocks.workflowId, workflowId)) - ) - } - } - } catch (err) { - logger.warn('Failed to persist block migrations', { workflowId, error: err }) - } - }) - } - - // Convert edges to the expected format - const edgesArray: Edge[] = edges.map((edge) => ({ - id: edge.id, - source: edge.sourceBlockId, - target: edge.targetBlockId, - sourceHandle: edge.sourceHandle ?? undefined, - targetHandle: edge.targetHandle ?? undefined, - type: 'default', - data: {}, - })) - - // Convert subflows to loops and parallels - const loops: Record = {} - const parallels: Record = {} - - subflows.forEach((subflow) => { - const config = (subflow.config ?? {}) as Partial - - if (subflow.type === SUBFLOW_TYPES.LOOP) { - const loopType = - (config as Loop).loopType === 'for' || - (config as Loop).loopType === 'forEach' || - (config as Loop).loopType === 'while' || - (config as Loop).loopType === 'doWhile' - ? (config as Loop).loopType - : 'for' - - const loop: Loop = { - id: subflow.id, - nodes: Array.isArray((config as Loop).nodes) ? (config as Loop).nodes : [], - iterations: - typeof (config as Loop).iterations === 'number' ? (config as Loop).iterations : 1, - loopType, - forEachItems: (config as Loop).forEachItems ?? '', - whileCondition: (config as Loop).whileCondition ?? '', - doWhileCondition: (config as Loop).doWhileCondition ?? '', - enabled: finalBlocks[subflow.id]?.enabled ?? true, - } - loops[subflow.id] = loop - - if (finalBlocks[subflow.id]) { - const block = finalBlocks[subflow.id] - finalBlocks[subflow.id] = { - ...block, - data: { - ...block.data, - collection: loop.forEachItems ?? block.data?.collection ?? '', - whileCondition: loop.whileCondition ?? block.data?.whileCondition ?? '', - doWhileCondition: loop.doWhileCondition ?? block.data?.doWhileCondition ?? '', - }, - } - } - } else if (subflow.type === SUBFLOW_TYPES.PARALLEL) { - const parallel: Parallel = { - id: subflow.id, - nodes: Array.isArray((config as Parallel).nodes) ? (config as Parallel).nodes : [], - count: typeof (config as Parallel).count === 'number' ? (config as Parallel).count : 5, - distribution: (config as Parallel).distribution ?? '', - parallelType: - (config as Parallel).parallelType === 'count' || - (config as Parallel).parallelType === 'collection' - ? (config as Parallel).parallelType - : 'count', - enabled: finalBlocks[subflow.id]?.enabled ?? true, - } - parallels[subflow.id] = parallel - } else { - logger.warn(`Unknown subflow type: ${subflow.type} for subflow ${subflow.id}`) + } + for (const id of Object.keys(raw.parallels)) { + if (finalBlocks[id]) { + patchedParallels[id] = { + ...raw.parallels[id], + enabled: finalBlocks[id].enabled ?? true, } - }) - - return { - blocks: finalBlocks, - edges: edgesArray, - loops, - parallels, - isFromNormalizedTables: true, } - } catch (error) { - logger.error(`Error loading workflow ${workflowId} from normalized tables:`, error) - return null + } + + return { + blocks: finalBlocks, + edges: raw.edges, + loops: patchedLoops, + parallels: patchedParallels, + isFromNormalizedTables: true, } } -/** - * Save workflow state to normalized tables - */ export async function saveWorkflowToNormalizedTables( workflowId: string, state: WorkflowState, externalTx?: DbOrTx ): Promise<{ success: boolean; error?: string }> { - const blockRecords = state.blocks as Record - const canonicalLoops = generateLoopBlocks(blockRecords) - const canonicalParallels = generateParallelBlocks(blockRecords) - - const execute = async (tx: DbOrTx) => { - await Promise.all([ - tx.delete(workflowBlocks).where(eq(workflowBlocks.workflowId, workflowId)), - tx.delete(workflowEdges).where(eq(workflowEdges.workflowId, workflowId)), - tx.delete(workflowSubflows).where(eq(workflowSubflows.workflowId, workflowId)), - ]) - - if (Object.keys(state.blocks).length > 0) { - const blockInserts = Object.values(state.blocks).map((block) => ({ - id: block.id, - workflowId: workflowId, - type: block.type, - name: block.name || '', - positionX: String(block.position?.x || 0), - positionY: String(block.position?.y || 0), - enabled: block.enabled ?? true, - horizontalHandles: block.horizontalHandles ?? true, - advancedMode: block.advancedMode ?? false, - triggerMode: block.triggerMode ?? false, - height: String(block.height || 0), - subBlocks: block.subBlocks || {}, - outputs: block.outputs || {}, - data: block.data || {}, - parentId: block.data?.parentId || null, - extent: block.data?.extent || null, - locked: block.locked ?? false, - })) - - await tx.insert(workflowBlocks).values(blockInserts) - } - - if (state.edges.length > 0) { - const edgeInserts = state.edges.map((edge) => ({ - id: edge.id, - workflowId: workflowId, - sourceBlockId: edge.source, - targetBlockId: edge.target, - sourceHandle: edge.sourceHandle || null, - targetHandle: edge.targetHandle || null, - })) - - await tx.insert(workflowEdges).values(edgeInserts) - } - - const subflowInserts: SubflowInsert[] = [] - - Object.values(canonicalLoops).forEach((loop) => { - subflowInserts.push({ - id: loop.id, - workflowId: workflowId, - type: SUBFLOW_TYPES.LOOP, - config: loop, - }) - }) - - Object.values(canonicalParallels).forEach((parallel) => { - subflowInserts.push({ - id: parallel.id, - workflowId: workflowId, - type: SUBFLOW_TYPES.PARALLEL, - config: parallel, - }) - }) - - if (subflowInserts.length > 0) { - await tx.insert(workflowSubflows).values(subflowInserts) - } - } - - if (externalTx) { - await execute(externalTx) - return { success: true } - } - - try { - await db.transaction(execute) - return { success: true } - } catch (error) { - logger.error(`Error saving workflow ${workflowId} to normalized tables:`, error) - return { - success: false, - error: error instanceof Error ? error.message : 'Unknown error', - } - } + return saveWorkflowToNormalizedTablesRaw(workflowId, state, externalTx) } -/** - * Check if a workflow exists in normalized tables - */ export async function workflowExistsInNormalizedTables(workflowId: string): Promise { try { + const { workflowBlocks } = await import('@sim/db') const blocks = await db .select({ id: workflowBlocks.id }) .from(workflowBlocks) @@ -641,12 +399,9 @@ export async function workflowExistsInNormalizedTables(workflowId: string): Prom } } -/** - * Deploy a workflow by creating a new deployment version - */ export async function deployWorkflow(params: { workflowId: string - deployedBy: string // User ID of the person deploying + deployedBy: string workflowName?: string }): Promise<{ success: boolean @@ -664,7 +419,6 @@ export async function deployWorkflow(params: { return { success: false, error: 'Failed to load workflow state' } } - // Also fetch workflow variables const [workflowRecord] = await db .select({ variables: workflow.variables }) .from(workflow) @@ -683,7 +437,6 @@ export async function deployWorkflow(params: { const now = new Date() const deployedVersion = await db.transaction(async (tx) => { - // Get next version number const [{ maxVersion }] = await tx .select({ maxVersion: sql`COALESCE(MAX("version"), 0)` }) .from(workflowDeploymentVersion) @@ -692,13 +445,11 @@ export async function deployWorkflow(params: { const nextVersion = Number(maxVersion) + 1 const deploymentVersionId = generateId() - // Deactivate all existing versions await tx .update(workflowDeploymentVersion) .set({ isActive: false }) .where(eq(workflowDeploymentVersion.workflowId, workflowId)) - // Create new deployment version await tx.insert(workflowDeploymentVersion).values({ id: deploymentVersionId, workflowId, @@ -709,7 +460,6 @@ export async function deployWorkflow(params: { createdAt: now, }) - // Update workflow to deployed const updateData: Record = { isDeployed: true, deployedAt: now, @@ -717,9 +467,6 @@ export async function deployWorkflow(params: { await tx.update(workflow).set(updateData).where(eq(workflow.id, workflowId)) - // Note: Templates are NOT automatically updated on deployment - // Template updates must be done explicitly through the "Update Template" button - return { version: nextVersion, deploymentVersionId } }) @@ -766,7 +513,6 @@ export async function deployWorkflow(params: { } } -/** Input state for ID regeneration - partial to handle external sources */ export interface RegenerateStateInput { blocks?: Record edges?: Edge[] @@ -777,7 +523,6 @@ export interface RegenerateStateInput { metadata?: Record } -/** Output state after ID regeneration */ interface RegenerateStateOutput { blocks: Record edges: Edge[] @@ -788,49 +533,35 @@ interface RegenerateStateOutput { metadata?: Record } -/** - * Regenerates all IDs in a workflow state to avoid conflicts when duplicating or using templates - * Returns a new state with all IDs regenerated and references updated - */ export function regenerateWorkflowStateIds(state: RegenerateStateInput): RegenerateStateOutput { - // Create ID mappings const blockIdMapping = new Map() const edgeIdMapping = new Map() const loopIdMapping = new Map() const parallelIdMapping = new Map() - // First pass: Create all ID mappings - // Map block IDs Object.keys(state.blocks || {}).forEach((oldId) => { blockIdMapping.set(oldId, generateId()) }) - // Map edge IDs - ;(state.edges || []).forEach((edge: Edge) => { edgeIdMapping.set(edge.id, generateId()) }) - // Map loop IDs Object.keys(state.loops || {}).forEach((oldId) => { loopIdMapping.set(oldId, generateId()) }) - // Map parallel IDs Object.keys(state.parallels || {}).forEach((oldId) => { parallelIdMapping.set(oldId, generateId()) }) - // Second pass: Create new state with regenerated IDs and updated references const newBlocks: Record = {} const newEdges: Edge[] = [] const newLoops: Record = {} const newParallels: Record = {} - // Regenerate blocks with updated references Object.entries(state.blocks || {}).forEach(([oldId, block]) => { const newId = blockIdMapping.get(oldId)! - // Duplicated blocks are always unlocked so users can edit them const newBlock: BlockState = { ...block, id: newId, @@ -838,7 +569,6 @@ export function regenerateWorkflowStateIds(state: RegenerateStateInput): Regener locked: false, } - // Update parentId reference if it exists if (newBlock.data?.parentId) { const newParentId = blockIdMapping.get(newBlock.data.parentId) if (newParentId) { @@ -846,13 +576,11 @@ export function regenerateWorkflowStateIds(state: RegenerateStateInput): Regener } } - // Update any block references in subBlocks if (newBlock.subBlocks) { const updatedSubBlocks: Record = {} Object.entries(newBlock.subBlocks).forEach(([subId, subBlock]) => { const updatedSubBlock = { ...subBlock } - // If subblock value contains block references, update them if ( typeof updatedSubBlock.value === 'string' && blockIdMapping.has(updatedSubBlock.value) @@ -860,7 +588,6 @@ export function regenerateWorkflowStateIds(state: RegenerateStateInput): Regener updatedSubBlock.value = blockIdMapping.get(updatedSubBlock.value) ?? updatedSubBlock.value } - // Remap condition/router IDs embedded in condition-input/router-input subBlocks if ( (updatedSubBlock.type === 'condition-input' || updatedSubBlock.type === 'router-input') && typeof updatedSubBlock.value === 'string' @@ -870,9 +597,7 @@ export function regenerateWorkflowStateIds(state: RegenerateStateInput): Regener if (Array.isArray(parsed) && remapConditionBlockIds(parsed, oldId, newId)) { updatedSubBlock.value = JSON.stringify(parsed) } - } catch { - // Not valid JSON, skip - } + } catch {} } updatedSubBlocks[subId] = updatedSubBlock @@ -883,8 +608,6 @@ export function regenerateWorkflowStateIds(state: RegenerateStateInput): Regener newBlocks[newId] = newBlock }) - // Regenerate edges with updated source/target references - ;(state.edges || []).forEach((edge: Edge) => { const newId = edgeIdMapping.get(edge.id)! const newSource = blockIdMapping.get(edge.source) || edge.source @@ -903,12 +626,10 @@ export function regenerateWorkflowStateIds(state: RegenerateStateInput): Regener }) }) - // Regenerate loops with updated node references Object.entries(state.loops || {}).forEach(([oldId, loop]) => { const newId = loopIdMapping.get(oldId)! const newLoop: Loop = { ...loop, id: newId } - // Update nodes array with new block IDs if (newLoop.nodes) { newLoop.nodes = newLoop.nodes.map((nodeId: string) => blockIdMapping.get(nodeId) || nodeId) } @@ -916,12 +637,10 @@ export function regenerateWorkflowStateIds(state: RegenerateStateInput): Regener newLoops[newId] = newLoop }) - // Regenerate parallels with updated node references Object.entries(state.parallels || {}).forEach(([oldId, parallel]) => { const newId = parallelIdMapping.get(oldId)! const newParallel: Parallel = { ...parallel, id: newId } - // Update nodes array with new block IDs if (newParallel.nodes) { newParallel.nodes = newParallel.nodes.map( (nodeId: string) => blockIdMapping.get(nodeId) || nodeId @@ -942,10 +661,6 @@ export function regenerateWorkflowStateIds(state: RegenerateStateInput): Regener } } -/** - * Undeploy a workflow by deactivating all versions and clearing deployment state. - * Handles schedule deletion and returns the result. - */ export async function undeployWorkflow(params: { workflowId: string; tx?: DbOrTx }): Promise<{ success: boolean error?: string @@ -987,10 +702,6 @@ export async function undeployWorkflow(params: { workflowId: string; tx?: DbOrTx } } -/** - * Activate a specific deployment version for a workflow. - * Deactivates the current active version and activates the specified one. - */ export async function activateWorkflowVersion(params: { workflowId: string version: number @@ -1133,9 +844,6 @@ export async function activateWorkflowVersionById(params: { } } -/** - * List all deployment versions for a workflow. - */ export async function listWorkflowVersions(workflowId: string): Promise<{ versions: Array<{ id: string diff --git a/apps/sim/lib/workflows/utils.test.ts b/apps/sim/lib/workflows/utils.test.ts index f5fd238a235..5f630f39e65 100644 --- a/apps/sim/lib/workflows/utils.test.ts +++ b/apps/sim/lib/workflows/utils.test.ts @@ -11,24 +11,24 @@ import { authMockFns, createSession, createWorkflowRecord, - databaseMock, expectWorkflowAccessDenied, expectWorkflowAccessGranted, } from '@sim/testing' import { beforeEach, describe, expect, it, vi } from 'vitest' -const { mockGetActiveWorkflowContext } = vi.hoisted(() => ({ - mockGetActiveWorkflowContext: vi.fn(), +const { mockAuthorizeWorkflow } = vi.hoisted(() => ({ + mockAuthorizeWorkflow: vi.fn(), })) -vi.mock('@/lib/workflows/active-context', () => ({ - getActiveWorkflowContext: mockGetActiveWorkflowContext, +vi.mock('@sim/workflow-authz', () => ({ + authorizeWorkflowByWorkspacePermission: mockAuthorizeWorkflow, + getActiveWorkflowContext: vi.fn(), + getActiveWorkflowRecord: vi.fn(), + assertActiveWorkflowContext: vi.fn(), })) import { validateWorkflowPermissions } from '@/lib/workflows/utils' -const mockDb = databaseMock.db - const mockSession = createSession({ userId: 'user-1', email: 'user1@test.com' }) const mockWorkflow = createWorkflowRecord({ id: 'wf-1', @@ -36,9 +36,25 @@ const mockWorkflow = createWorkflowRecord({ workspaceId: 'ws-1', }) +const allowed = (workspacePermission: 'read' | 'write' | 'admin') => ({ + allowed: true, + status: 200, + workflow: mockWorkflow, + workspacePermission, +}) + +const denied = (status: number, message: string, workspacePermission: string | null = null) => ({ + allowed: false, + status, + message, + workflow: mockWorkflow, + workspacePermission, +}) + describe('validateWorkflowPermissions', () => { beforeEach(() => { vi.clearAllMocks() + authMockFns.mockGetSession.mockResolvedValue(mockSession) }) describe('authentication', () => { @@ -62,8 +78,13 @@ describe('validateWorkflowPermissions', () => { describe('workflow not found', () => { it('should return 404 when workflow does not exist', async () => { - authMockFns.mockGetSession.mockResolvedValue(mockSession) - mockGetActiveWorkflowContext.mockResolvedValue(null) + mockAuthorizeWorkflow.mockResolvedValue({ + allowed: false, + status: 404, + message: 'Workflow not found', + workflow: null, + workspacePermission: null, + }) const result = await validateWorkflowPermissions('non-existent', 'req-1', 'read') @@ -77,18 +98,11 @@ describe('validateWorkflowPermissions', () => { authMockFns.mockGetSession.mockResolvedValue({ user: { id: 'owner-1', email: 'owner-1@test.com' }, }) - mockGetActiveWorkflowContext.mockResolvedValue({ - workflow: mockWorkflow, - workspaceId: 'ws-1', - }) - - const mockLimit = vi.fn().mockResolvedValue([]) - const mockWhere = vi.fn(() => ({ limit: mockLimit })) - const mockFrom = vi.fn(() => ({ where: mockWhere })) - vi.mocked(mockDb.select).mockReturnValue({ from: mockFrom } as any) + mockAuthorizeWorkflow.mockResolvedValue( + denied(403, 'Unauthorized: Access denied to read this workflow') + ) const result = await validateWorkflowPermissions('wf-1', 'req-1', 'read') - expectWorkflowAccessDenied(result, 403) }) @@ -96,18 +110,11 @@ describe('validateWorkflowPermissions', () => { authMockFns.mockGetSession.mockResolvedValue({ user: { id: 'owner-1', email: 'owner-1@test.com' }, }) - mockGetActiveWorkflowContext.mockResolvedValue({ - workflow: mockWorkflow, - workspaceId: 'ws-1', - }) - - const mockLimit = vi.fn().mockResolvedValue([]) - const mockWhere = vi.fn(() => ({ limit: mockLimit })) - const mockFrom = vi.fn(() => ({ where: mockWhere })) - vi.mocked(mockDb.select).mockReturnValue({ from: mockFrom } as any) + mockAuthorizeWorkflow.mockResolvedValue( + denied(403, 'Unauthorized: Access denied to write this workflow') + ) const result = await validateWorkflowPermissions('wf-1', 'req-1', 'write') - expectWorkflowAccessDenied(result, 403) }) @@ -115,198 +122,111 @@ describe('validateWorkflowPermissions', () => { authMockFns.mockGetSession.mockResolvedValue({ user: { id: 'owner-1', email: 'owner-1@test.com' }, }) - mockGetActiveWorkflowContext.mockResolvedValue({ - workflow: mockWorkflow, - workspaceId: 'ws-1', - }) - - const mockLimit = vi.fn().mockResolvedValue([]) - const mockWhere = vi.fn(() => ({ limit: mockLimit })) - const mockFrom = vi.fn(() => ({ where: mockWhere })) - vi.mocked(mockDb.select).mockReturnValue({ from: mockFrom } as any) + mockAuthorizeWorkflow.mockResolvedValue( + denied(403, 'Unauthorized: Access denied to admin this workflow') + ) const result = await validateWorkflowPermissions('wf-1', 'req-1', 'admin') - expectWorkflowAccessDenied(result, 403) }) }) describe('workspace member access with permissions', () => { - beforeEach(() => { - authMockFns.mockGetSession.mockResolvedValue(mockSession) - }) - it('should grant read access to user with read permission', async () => { - mockGetActiveWorkflowContext.mockResolvedValue({ - workflow: mockWorkflow, - workspaceId: 'ws-1', - }) - - const mockLimit = vi.fn().mockResolvedValue([{ permissionType: 'read' }]) - const mockWhere = vi.fn(() => ({ limit: mockLimit })) - const mockFrom = vi.fn(() => ({ where: mockWhere })) - vi.mocked(mockDb.select).mockReturnValue({ from: mockFrom } as any) + mockAuthorizeWorkflow.mockResolvedValue(allowed('read')) const result = await validateWorkflowPermissions('wf-1', 'req-1', 'read') - expectWorkflowAccessGranted(result) }) it('should deny write access to user with only read permission', async () => { - mockGetActiveWorkflowContext.mockResolvedValue({ - workflow: mockWorkflow, - workspaceId: 'ws-1', - }) - - const mockLimit = vi.fn().mockResolvedValue([{ permissionType: 'read' }]) - const mockWhere = vi.fn(() => ({ limit: mockLimit })) - const mockFrom = vi.fn(() => ({ where: mockWhere })) - vi.mocked(mockDb.select).mockReturnValue({ from: mockFrom } as any) + mockAuthorizeWorkflow.mockResolvedValue( + denied(403, 'Unauthorized: Access denied to write this workflow', 'read') + ) const result = await validateWorkflowPermissions('wf-1', 'req-1', 'write') - expectWorkflowAccessDenied(result, 403) expect(result.error?.message).toContain('write') }) it('should grant write access to user with write permission', async () => { - mockGetActiveWorkflowContext.mockResolvedValue({ - workflow: mockWorkflow, - workspaceId: 'ws-1', - }) - - const mockLimit = vi.fn().mockResolvedValue([{ permissionType: 'write' }]) - const mockWhere = vi.fn(() => ({ limit: mockLimit })) - const mockFrom = vi.fn(() => ({ where: mockWhere })) - vi.mocked(mockDb.select).mockReturnValue({ from: mockFrom } as any) + mockAuthorizeWorkflow.mockResolvedValue(allowed('write')) const result = await validateWorkflowPermissions('wf-1', 'req-1', 'write') - expectWorkflowAccessGranted(result) }) it('should grant write access to user with admin permission', async () => { - mockGetActiveWorkflowContext.mockResolvedValue({ - workflow: mockWorkflow, - workspaceId: 'ws-1', - }) - - const mockLimit = vi.fn().mockResolvedValue([{ permissionType: 'admin' }]) - const mockWhere = vi.fn(() => ({ limit: mockLimit })) - const mockFrom = vi.fn(() => ({ where: mockWhere })) - vi.mocked(mockDb.select).mockReturnValue({ from: mockFrom } as any) + mockAuthorizeWorkflow.mockResolvedValue(allowed('admin')) const result = await validateWorkflowPermissions('wf-1', 'req-1', 'write') - expectWorkflowAccessGranted(result) }) it('should deny admin access to user with only write permission', async () => { - mockGetActiveWorkflowContext.mockResolvedValue({ - workflow: mockWorkflow, - workspaceId: 'ws-1', - }) - - const mockLimit = vi.fn().mockResolvedValue([{ permissionType: 'write' }]) - const mockWhere = vi.fn(() => ({ limit: mockLimit })) - const mockFrom = vi.fn(() => ({ where: mockWhere })) - vi.mocked(mockDb.select).mockReturnValue({ from: mockFrom } as any) + mockAuthorizeWorkflow.mockResolvedValue( + denied(403, 'Unauthorized: Access denied to admin this workflow', 'write') + ) const result = await validateWorkflowPermissions('wf-1', 'req-1', 'admin') - expectWorkflowAccessDenied(result, 403) expect(result.error?.message).toContain('admin') }) it('should grant admin access to user with admin permission', async () => { - mockGetActiveWorkflowContext.mockResolvedValue({ - workflow: mockWorkflow, - workspaceId: 'ws-1', - }) - - const mockLimit = vi.fn().mockResolvedValue([{ permissionType: 'admin' }]) - const mockWhere = vi.fn(() => ({ limit: mockLimit })) - const mockFrom = vi.fn(() => ({ where: mockWhere })) - vi.mocked(mockDb.select).mockReturnValue({ from: mockFrom } as any) + mockAuthorizeWorkflow.mockResolvedValue(allowed('admin')) const result = await validateWorkflowPermissions('wf-1', 'req-1', 'admin') - expectWorkflowAccessGranted(result) }) }) describe('no workspace permission', () => { it('should deny access to user without any workspace permission', async () => { - authMockFns.mockGetSession.mockResolvedValue(mockSession) - mockGetActiveWorkflowContext.mockResolvedValue({ - workflow: mockWorkflow, - workspaceId: 'ws-1', - }) - - const mockLimit = vi.fn().mockResolvedValue([]) - const mockWhere = vi.fn(() => ({ limit: mockLimit })) - const mockFrom = vi.fn(() => ({ where: mockWhere })) - vi.mocked(mockDb.select).mockReturnValue({ from: mockFrom } as any) + mockAuthorizeWorkflow.mockResolvedValue( + denied(403, 'Unauthorized: Access denied to read this workflow') + ) const result = await validateWorkflowPermissions('wf-1', 'req-1', 'read') - expectWorkflowAccessDenied(result, 403) }) }) describe('workflow without workspace', () => { it('should deny access to non-owner for workflow without workspace', async () => { - const workflowWithoutWorkspace = createWorkflowRecord({ - id: 'wf-2', - userId: 'other-user', - workspaceId: null, - }) - - authMockFns.mockGetSession.mockResolvedValue(mockSession) - mockGetActiveWorkflowContext.mockResolvedValue({ - workflow: workflowWithoutWorkspace, - workspaceId: '', + mockAuthorizeWorkflow.mockResolvedValue({ + allowed: false, + status: 403, + message: + 'This workflow is not attached to a workspace. Personal workflows are deprecated and cannot be accessed.', + workflow: createWorkflowRecord({ id: 'wf-2', userId: 'other-user', workspaceId: null }), + workspacePermission: null, }) const result = await validateWorkflowPermissions('wf-2', 'req-1', 'read') - expectWorkflowAccessDenied(result, 403) }) it('should deny access to owner for workflow without workspace', async () => { - const workflowWithoutWorkspace = createWorkflowRecord({ - id: 'wf-2', - userId: 'user-1', - workspaceId: null, - }) - - authMockFns.mockGetSession.mockResolvedValue(mockSession) - mockGetActiveWorkflowContext.mockResolvedValue({ - workflow: workflowWithoutWorkspace, - workspaceId: '', + mockAuthorizeWorkflow.mockResolvedValue({ + allowed: false, + status: 403, + message: + 'This workflow is not attached to a workspace. Personal workflows are deprecated and cannot be accessed.', + workflow: createWorkflowRecord({ id: 'wf-2', userId: 'user-1', workspaceId: null }), + workspacePermission: null, }) const result = await validateWorkflowPermissions('wf-2', 'req-1', 'read') - expectWorkflowAccessDenied(result, 403) }) }) describe('default action', () => { it('should default to read action when not specified', async () => { - authMockFns.mockGetSession.mockResolvedValue(mockSession) - mockGetActiveWorkflowContext.mockResolvedValue({ - workflow: mockWorkflow, - workspaceId: 'ws-1', - }) - - const mockLimit = vi.fn().mockResolvedValue([{ permissionType: 'read' }]) - const mockWhere = vi.fn(() => ({ limit: mockLimit })) - const mockFrom = vi.fn(() => ({ where: mockWhere })) - vi.mocked(mockDb.select).mockReturnValue({ from: mockFrom } as any) + mockAuthorizeWorkflow.mockResolvedValue(allowed('read')) const result = await validateWorkflowPermissions('wf-1', 'req-1') - expectWorkflowAccessGranted(result) }) }) diff --git a/apps/sim/lib/workflows/utils.ts b/apps/sim/lib/workflows/utils.ts index 5dfdc5c0668..a6b971f932a 100644 --- a/apps/sim/lib/workflows/utils.ts +++ b/apps/sim/lib/workflows/utils.ts @@ -2,14 +2,13 @@ import { db } from '@sim/db' import { permissions, userStats, workflowFolder, workflow as workflowTable } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { generateId } from '@sim/utils/id' +import { authorizeWorkflowByWorkspacePermission } from '@sim/workflow-authz' import { and, asc, eq, inArray, isNull, max, min, sql } from 'drizzle-orm' import { NextResponse } from 'next/server' import { getSession } from '@/lib/auth' -import { getActiveWorkflowContext } from '@/lib/workflows/active-context' import { getNextWorkflowColor } from '@/lib/workflows/colors' import { buildDefaultWorkflowArtifacts } from '@/lib/workflows/defaults' import { saveWorkflowToNormalizedTables } from '@/lib/workflows/persistence/utils' -import type { PermissionType } from '@/lib/workspaces/permissions/utils' import { getWorkspaceBilledAccountUserId } from '@/lib/workspaces/utils' import type { ExecutionResult } from '@/executor/types' @@ -228,18 +227,6 @@ export async function resolveWorkflowIdForUser( } } -type WorkflowRecord = ReturnType extends Promise - ? NonNullable - : never - -export interface WorkflowWorkspaceAuthorizationResult { - allowed: boolean - status: number - message?: string - workflow: WorkflowRecord | null - workspacePermission: PermissionType | null -} - export async function updateWorkflowRunCounts(workflowId: string, runs = 1) { try { const workflow = await getWorkflowById(workflowId) @@ -402,86 +389,6 @@ export async function validateWorkflowPermissions( } } -export async function authorizeWorkflowByWorkspacePermission(params: { - workflowId: string - userId: string - action?: 'read' | 'write' | 'admin' -}): Promise { - const { workflowId, userId, action = 'read' } = params - - const activeContext = await getActiveWorkflowContext(workflowId) - if (!activeContext) { - return { - allowed: false, - status: 404, - message: 'Workflow not found', - workflow: null, - workspacePermission: null, - } - } - - const workflow = activeContext.workflow - - if (!workflow.workspaceId) { - return { - allowed: false, - status: 403, - message: - 'This workflow is not attached to a workspace. Personal workflows are deprecated and cannot be accessed.', - workflow, - workspacePermission: null, - } - } - - const [permissionRow] = await db - .select({ permissionType: permissions.permissionType }) - .from(permissions) - .where( - and( - eq(permissions.userId, userId), - eq(permissions.entityType, 'workspace'), - eq(permissions.entityId, workflow.workspaceId) - ) - ) - .limit(1) - - const workspacePermission = permissionRow?.permissionType ?? null - - if (workspacePermission === null) { - return { - allowed: false, - status: 403, - message: `Unauthorized: Access denied to ${action} this workflow`, - workflow, - workspacePermission, - } - } - - const permissionSatisfied = - action === 'read' - ? true - : action === 'write' - ? workspacePermission === 'write' || workspacePermission === 'admin' - : workspacePermission === 'admin' - - if (!permissionSatisfied) { - return { - allowed: false, - status: 403, - message: `Unauthorized: Access denied to ${action} this workflow`, - workflow, - workspacePermission, - } - } - - return { - allowed: true, - status: 200, - workflow, - workspacePermission, - } -} - // ── Workflow CRUD ── export interface CreateWorkflowInput { diff --git a/apps/sim/package.json b/apps/sim/package.json index 1a84029605a..1322e2302c2 100644 --- a/apps/sim/package.json +++ b/apps/sim/package.json @@ -10,8 +10,6 @@ "scripts": { "dev": "next dev --port 3000", "dev:webpack": "next dev --webpack", - "dev:sockets": "bun run socket/index.ts", - "dev:full": "bunx concurrently -n \"App,Realtime\" -c \"cyan,magenta\" \"bun run dev\" \"bun run dev:sockets\"", "load:workflow": "bun run load:workflow:baseline", "load:workflow:baseline": "BASE_URL=${BASE_URL:-http://localhost:3000} WARMUP_DURATION=${WARMUP_DURATION:-10} WARMUP_RATE=${WARMUP_RATE:-2} PEAK_RATE=${PEAK_RATE:-8} HOLD_DURATION=${HOLD_DURATION:-20} bunx artillery run scripts/load/workflow-concurrency.yml", "load:workflow:waves": "BASE_URL=${BASE_URL:-http://localhost:3000} WAVE_ONE_DURATION=${WAVE_ONE_DURATION:-10} WAVE_ONE_RATE=${WAVE_ONE_RATE:-6} QUIET_DURATION=${QUIET_DURATION:-5} WAVE_TWO_DURATION=${WAVE_TWO_DURATION:-15} WAVE_TWO_RATE=${WAVE_TWO_RATE:-8} WAVE_THREE_DURATION=${WAVE_THREE_DURATION:-20} WAVE_THREE_RATE=${WAVE_THREE_RATE:-10} bunx artillery run scripts/load/workflow-waves.yml", @@ -35,6 +33,7 @@ "@1password/sdk": "0.3.1", "@a2a-js/sdk": "0.3.7", "@anthropic-ai/sdk": "0.71.2", + "@aws-sdk/client-athena": "3.1024.0", "@aws-sdk/client-bedrock-runtime": "3.940.0", "@aws-sdk/client-cloudformation": "3.1019.0", "@aws-sdk/client-cloudwatch": "3.940.0", @@ -94,9 +93,14 @@ "@radix-ui/react-visually-hidden": "1.2.4", "@react-email/components": "^0.0.34", "@react-email/render": "2.0.0", + "@sim/audit": "workspace:*", "@sim/logger": "workspace:*", + "@sim/realtime-protocol": "workspace:*", + "@sim/security": "workspace:*", "@sim/utils": "workspace:*", - "@socket.io/redis-adapter": "8.3.0", + "@sim/workflow-authz": "workspace:*", + "@sim/workflow-persistence": "workspace:*", + "@sim/workflow-types": "workspace:*", "@t3-oss/env-nextjs": "0.13.4", "@tanstack/react-query": "5.90.8", "@tanstack/react-query-devtools": "5.90.2", @@ -185,7 +189,6 @@ "safe-regex2": "5.1.0", "sharp": "0.34.3", "soap": "1.8.0", - "socket.io": "^4.8.1", "socket.io-client": "4.8.1", "ssh2": "^1.17.0", "streamdown": "2.5.0", diff --git a/apps/sim/socket/handlers/index.ts b/apps/sim/socket/handlers/index.ts deleted file mode 100644 index 4afeed40e36..00000000000 --- a/apps/sim/socket/handlers/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { setupConnectionHandlers } from '@/socket/handlers/connection' -import { setupOperationsHandlers } from '@/socket/handlers/operations' -import { setupPresenceHandlers } from '@/socket/handlers/presence' -import { setupSubblocksHandlers } from '@/socket/handlers/subblocks' -import { setupVariablesHandlers } from '@/socket/handlers/variables' -import { setupWorkflowHandlers } from '@/socket/handlers/workflow' -import type { AuthenticatedSocket } from '@/socket/middleware/auth' -import type { IRoomManager } from '@/socket/rooms' - -export function setupAllHandlers(socket: AuthenticatedSocket, roomManager: IRoomManager) { - setupWorkflowHandlers(socket, roomManager) - setupOperationsHandlers(socket, roomManager) - setupSubblocksHandlers(socket, roomManager) - setupVariablesHandlers(socket, roomManager) - setupPresenceHandlers(socket, roomManager) - setupConnectionHandlers(socket, roomManager) -} diff --git a/apps/sim/socket/rooms/index.ts b/apps/sim/socket/rooms/index.ts deleted file mode 100644 index 3cbdc41348a..00000000000 --- a/apps/sim/socket/rooms/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { MemoryRoomManager } from '@/socket/rooms/memory-manager' -export { RedisRoomManager } from '@/socket/rooms/redis-manager' -export type { IRoomManager, UserPresence, UserSession, WorkflowRoom } from '@/socket/rooms/types' diff --git a/apps/sim/stores/undo-redo/store.ts b/apps/sim/stores/undo-redo/store.ts index 6776bf66e1d..30ec1a3a237 100644 --- a/apps/sim/stores/undo-redo/store.ts +++ b/apps/sim/stores/undo-redo/store.ts @@ -1,8 +1,8 @@ import { createLogger } from '@sim/logger' +import { UNDO_REDO_OPERATIONS } from '@sim/realtime-protocol/constants' import type { Edge } from 'reactflow' import { create } from 'zustand' import { createJSONStorage, persist } from 'zustand/middleware' -import { UNDO_REDO_OPERATIONS } from '@/socket/constants' import type { BatchAddBlocksOperation, BatchAddEdgesOperation, diff --git a/apps/sim/stores/undo-redo/types.ts b/apps/sim/stores/undo-redo/types.ts index 8d5df192fd3..672383ad47d 100644 --- a/apps/sim/stores/undo-redo/types.ts +++ b/apps/sim/stores/undo-redo/types.ts @@ -1,5 +1,5 @@ +import type { UNDO_REDO_OPERATIONS, UndoRedoOperation } from '@sim/realtime-protocol/constants' import type { Edge } from 'reactflow' -import type { UNDO_REDO_OPERATIONS, UndoRedoOperation } from '@/socket/constants' import type { BlockState } from '@/stores/workflows/workflow/types' export type OperationType = UndoRedoOperation diff --git a/apps/sim/stores/undo-redo/utils.ts b/apps/sim/stores/undo-redo/utils.ts index 861277e02e4..1803970cd68 100644 --- a/apps/sim/stores/undo-redo/utils.ts +++ b/apps/sim/stores/undo-redo/utils.ts @@ -1,6 +1,6 @@ +import { UNDO_REDO_OPERATIONS } from '@sim/realtime-protocol/constants' import { generateId } from '@sim/utils/id' import type { Edge } from 'reactflow' -import { UNDO_REDO_OPERATIONS } from '@/socket/constants' import type { BatchAddBlocksOperation, BatchAddEdgesOperation, diff --git a/apps/sim/stores/workflows/utils.ts b/apps/sim/stores/workflows/utils.ts index 25b372693b1..fbb97f22fca 100644 --- a/apps/sim/stores/workflows/utils.ts +++ b/apps/sim/stores/workflows/utils.ts @@ -1,9 +1,9 @@ import { generateId } from '@sim/utils/id' +import { mergeSubblockStateWithValues } from '@sim/workflow-persistence/subblocks' import type { Edge } from 'reactflow' import { DEFAULT_DUPLICATE_OFFSET } from '@/lib/workflows/autolayout/constants' import { getEffectiveBlockOutputs } from '@/lib/workflows/blocks/block-outputs' import { remapConditionBlockIds, remapConditionEdgeHandle } from '@/lib/workflows/condition-ids' -import { mergeSubblockStateWithValues } from '@/lib/workflows/subblocks' import { buildDefaultCanonicalModes } from '@/lib/workflows/subblocks/visibility' import { hasTriggerCapability } from '@/lib/workflows/triggers/trigger-utils' import { getBlock } from '@/blocks' diff --git a/apps/sim/stores/workflows/workflow/types.ts b/apps/sim/stores/workflows/workflow/types.ts index 37d86847bcb..c209cfd0eef 100644 --- a/apps/sim/stores/workflows/workflow/types.ts +++ b/apps/sim/stores/workflows/workflow/types.ts @@ -1,179 +1,42 @@ +import type { + BlockData, + BlockLayoutState, + BlockState, + DragStartPosition, + Loop, + LoopBlock, + LoopConfig, + Parallel, + ParallelBlock, + ParallelConfig, + Position, + SubBlockState, + Subflow, + SubflowType, + Variable, + WorkflowState, +} from '@sim/workflow-types/workflow' import type { Edge } from 'reactflow' -import type { OutputFieldDefinition, SubBlockType } from '@/blocks/types' -export const SUBFLOW_TYPES = { - LOOP: 'loop', - PARALLEL: 'parallel', -} as const - -export type SubflowType = (typeof SUBFLOW_TYPES)[keyof typeof SUBFLOW_TYPES] - -export function isValidSubflowType(type: string): type is SubflowType { - return Object.values(SUBFLOW_TYPES).includes(type as SubflowType) -} - -export interface LoopConfig { - nodes: string[] - iterations: number - loopType: 'for' | 'forEach' | 'while' | 'doWhile' - forEachItems?: unknown[] | Record | string - whileCondition?: string // JS expression that evaluates to boolean (for while loops) - doWhileCondition?: string // JS expression that evaluates to boolean (for do-while loops) -} - -export interface ParallelConfig { - nodes: string[] - distribution?: unknown[] | Record | string - parallelType?: 'count' | 'collection' -} - -export interface Subflow { - id: string - workflowId: string - type: SubflowType - config: LoopConfig | ParallelConfig - createdAt: Date - updatedAt: Date -} - -export interface Position { - x: number - y: number -} - -export interface BlockData { - // Parent-child relationships for container nodes - parentId?: string - extent?: 'parent' - - // Container dimensions - width?: number - height?: number - - // Loop-specific properties - collection?: any // The items to iterate over in a forEach loop - count?: number // Number of iterations for numeric loops - loopType?: 'for' | 'forEach' | 'while' | 'doWhile' // Type of loop - must match Loop interface - whileCondition?: string // While loop condition (JS expression) - doWhileCondition?: string // Do-While loop condition (JS expression) - - // Parallel-specific properties - parallelType?: 'collection' | 'count' // Type of parallel execution - - // Container node type (for ReactFlow node type determination) - type?: string - - /** Canonical swap overrides keyed by canonicalParamId */ - canonicalModes?: Record -} - -export interface BlockLayoutState { - measuredWidth?: number - measuredHeight?: number -} - -export interface BlockState { - id: string - type: string - name: string - position: Position - subBlocks: Record - outputs: Record - enabled: boolean - horizontalHandles?: boolean - height?: number - advancedMode?: boolean - triggerMode?: boolean - data?: BlockData - layout?: BlockLayoutState - locked?: boolean -} - -export interface SubBlockState { - id: string - type: SubBlockType - value: string | number | string[][] | null -} - -export interface LoopBlock { - id: string - loopType: 'for' | 'forEach' - count: number - collection: string - width: number - height: number - executionState: { - isExecuting: boolean - startTime: null | number - endTime: null | number - } -} - -export interface ParallelBlock { - id: string - collection: string - width: number - height: number - executionState: { - currentExecution: number - isExecuting: boolean - startTime: null | number - endTime: null | number - } -} - -export interface Loop { - id: string - nodes: string[] - iterations: number - loopType: 'for' | 'forEach' | 'while' | 'doWhile' - forEachItems?: any[] | Record | string // Items or expression - whileCondition?: string // JS expression that evaluates to boolean (for while loops) - doWhileCondition?: string // JS expression that evaluates to boolean (for do-while loops) - enabled: boolean - locked?: boolean -} - -export interface Parallel { - id: string - nodes: string[] - distribution?: any[] | Record | string // Items or expression - count?: number // Number of parallel executions for count-based parallel - parallelType?: 'count' | 'collection' // Explicit parallel type to avoid inference bugs - enabled: boolean - locked?: boolean -} - -export interface Variable { - id: string - name: string - type: 'string' | 'number' | 'boolean' | 'object' | 'array' | 'plain' - value: unknown -} - -export interface DragStartPosition { - id: string - x: number - y: number - parentId?: string | null -} - -export interface WorkflowState { - currentWorkflowId?: string | null - blocks: Record - edges: Edge[] - lastSaved?: number - loops: Record - parallels: Record - lastUpdate?: number - metadata?: { - name?: string - description?: string - exportedAt?: string - } - variables?: Record - dragStartPosition?: DragStartPosition | null -} +export type { + BlockData, + BlockLayoutState, + BlockState, + DragStartPosition, + Loop, + LoopBlock, + LoopConfig, + Parallel, + ParallelBlock, + ParallelConfig, + Position, + SubBlockState, + Subflow, + SubflowType, + Variable, + WorkflowState, +} +export { isValidSubflowType, SUBFLOW_TYPES } from '@sim/workflow-types/workflow' export interface WorkflowActions { updateNodeDimensions: (id: string, dimensions: { width: number; height: number }) => void diff --git a/apps/sim/tools/http/webhook_request.ts b/apps/sim/tools/http/webhook_request.ts index 569f535e34a..39e4e8e21b5 100644 --- a/apps/sim/tools/http/webhook_request.ts +++ b/apps/sim/tools/http/webhook_request.ts @@ -1,4 +1,4 @@ -import { createHmac } from 'crypto' +import { hmacSha256Hex } from '@sim/security/hmac' import { generateId } from '@sim/utils/id' import type { RequestResponse, WebhookRequestParams } from '@/tools/http/types' import type { ToolConfig } from '@/tools/types' @@ -8,7 +8,7 @@ import type { ToolConfig } from '@/tools/types' */ function generateSignature(secret: string, timestamp: number, body: string): string { const signatureBase = `${timestamp}.${body}` - return createHmac('sha256', secret).update(signatureBase).digest('hex') + return hmacSha256Hex(signatureBase, secret) } export const webhookRequestTool: ToolConfig = { diff --git a/apps/sim/vitest.setup.ts b/apps/sim/vitest.setup.ts index 588a053bb23..922e8b7a919 100644 --- a/apps/sim/vitest.setup.ts +++ b/apps/sim/vitest.setup.ts @@ -8,6 +8,7 @@ import { schemaMock, setupGlobalFetchMock, setupGlobalStorageMocks, + workflowAuthzMock, } from '@sim/testing' import { afterAll, vi } from 'vitest' import '@testing-library/jest-dom/vitest' @@ -19,6 +20,7 @@ vi.mock('@sim/db', () => databaseMock) vi.mock('@sim/db/schema', () => schemaMock) vi.mock('drizzle-orm', () => drizzleOrmMock) vi.mock('@sim/logger', () => loggerMock) +vi.mock('@sim/workflow-authz', () => workflowAuthzMock) vi.mock('@/lib/auth', () => authMock) vi.mock('@/lib/auth/hybrid', () => hybridAuthMock) vi.mock('@/lib/core/utils/request', () => requestUtilsMock) diff --git a/bun.lock b/bun.lock index ac1e89e4922..916bc86fb35 100644 --- a/bun.lock +++ b/bun.lock @@ -1,11 +1,9 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "simstudio", - "dependencies": { - "@aws-sdk/client-athena": "3.1024.0", - }, "devDependencies": { "@biomejs/biome": "2.0.0-beta.5", "@octokit/rest": "^21.0.0", @@ -52,6 +50,36 @@ "typescript": "^5.8.2", }, }, + "apps/realtime": { + "name": "@sim/realtime", + "version": "0.1.0", + "dependencies": { + "@sim/audit": "workspace:*", + "@sim/auth": "workspace:*", + "@sim/db": "workspace:*", + "@sim/logger": "workspace:*", + "@sim/realtime-protocol": "workspace:*", + "@sim/security": "workspace:*", + "@sim/utils": "workspace:*", + "@sim/workflow-authz": "workspace:*", + "@sim/workflow-persistence": "workspace:*", + "@sim/workflow-types": "workspace:*", + "@socket.io/redis-adapter": "8.3.0", + "drizzle-orm": "^0.45.2", + "postgres": "^3.4.5", + "redis": "5.10.0", + "socket.io": "^4.8.1", + "zod": "^3.24.2", + }, + "devDependencies": { + "@sim/testing": "workspace:*", + "@sim/tsconfig": "workspace:*", + "@types/node": "24.2.1", + "socket.io-client": "4.8.1", + "typescript": "^5.7.3", + "vitest": "^3.0.8", + }, + }, "apps/sim": { "name": "sim", "version": "0.1.0", @@ -59,6 +87,7 @@ "@1password/sdk": "0.3.1", "@a2a-js/sdk": "0.3.7", "@anthropic-ai/sdk": "0.71.2", + "@aws-sdk/client-athena": "3.1024.0", "@aws-sdk/client-bedrock-runtime": "3.940.0", "@aws-sdk/client-cloudformation": "3.1019.0", "@aws-sdk/client-cloudwatch": "3.940.0", @@ -118,9 +147,14 @@ "@radix-ui/react-visually-hidden": "1.2.4", "@react-email/components": "^0.0.34", "@react-email/render": "2.0.0", + "@sim/audit": "workspace:*", "@sim/logger": "workspace:*", + "@sim/realtime-protocol": "workspace:*", + "@sim/security": "workspace:*", "@sim/utils": "workspace:*", - "@socket.io/redis-adapter": "8.3.0", + "@sim/workflow-authz": "workspace:*", + "@sim/workflow-persistence": "workspace:*", + "@sim/workflow-types": "workspace:*", "@t3-oss/env-nextjs": "0.13.4", "@tanstack/react-query": "5.90.8", "@tanstack/react-query-devtools": "5.90.2", @@ -209,7 +243,6 @@ "safe-regex2": "5.1.0", "sharp": "0.34.3", "soap": "1.8.0", - "socket.io": "^4.8.1", "socket.io-client": "4.8.1", "ssh2": "^1.17.0", "streamdown": "2.5.0", @@ -259,6 +292,34 @@ "vitest": "^3.0.8", }, }, + "packages/audit": { + "name": "@sim/audit", + "version": "0.1.0", + "dependencies": { + "@sim/db": "workspace:*", + "@sim/logger": "workspace:*", + "@sim/utils": "workspace:*", + "drizzle-orm": "^0.45.2", + }, + "devDependencies": { + "@sim/testing": "workspace:*", + "@sim/tsconfig": "workspace:*", + "typescript": "^5.7.3", + "vitest": "^3.0.8", + }, + }, + "packages/auth": { + "name": "@sim/auth", + "version": "0.1.0", + "dependencies": { + "@sim/db": "workspace:*", + "better-auth": "1.3.12", + }, + "devDependencies": { + "@sim/tsconfig": "workspace:*", + "typescript": "^5.7.3", + }, + }, "packages/cli": { "name": "simstudio", "version": "0.1.19", @@ -307,6 +368,27 @@ "vitest": "^3.0.8", }, }, + "packages/realtime-protocol": { + "name": "@sim/realtime-protocol", + "version": "0.1.0", + "dependencies": { + "zod": "^3.24.2", + }, + "devDependencies": { + "@sim/tsconfig": "workspace:*", + "typescript": "^5.7.3", + }, + }, + "packages/security": { + "name": "@sim/security", + "version": "0.1.0", + "devDependencies": { + "@sim/tsconfig": "workspace:*", + "@types/node": "24.2.1", + "typescript": "^5.7.3", + "vitest": "^3.0.8", + }, + }, "packages/testing": { "name": "@sim/testing", "version": "0.1.0", @@ -346,6 +428,46 @@ "vitest": "^3.0.8", }, }, + "packages/workflow-authz": { + "name": "@sim/workflow-authz", + "version": "0.1.0", + "dependencies": { + "@sim/db": "workspace:*", + "drizzle-orm": "^0.45.2", + }, + "devDependencies": { + "@sim/tsconfig": "workspace:*", + "typescript": "^5.7.3", + }, + }, + "packages/workflow-persistence": { + "name": "@sim/workflow-persistence", + "version": "0.1.0", + "dependencies": { + "@sim/db": "workspace:*", + "@sim/logger": "workspace:*", + "@sim/utils": "workspace:*", + "@sim/workflow-types": "workspace:*", + "drizzle-orm": "^0.45.2", + "reactflow": "^11.11.4", + }, + "devDependencies": { + "@sim/tsconfig": "workspace:*", + "typescript": "^5.7.3", + }, + }, + "packages/workflow-types": { + "name": "@sim/workflow-types", + "version": "0.1.0", + "devDependencies": { + "@sim/tsconfig": "workspace:*", + "reactflow": "^11.11.4", + "typescript": "^5.7.3", + }, + "peerDependencies": { + "reactflow": "^11.11.4", + }, + }, }, "trustedDependencies": [ "ffmpeg-static", @@ -1351,16 +1473,32 @@ "@shuding/opentype.js": ["@shuding/opentype.js@1.4.0-beta.0", "", { "dependencies": { "fflate": "^0.7.3", "string.prototype.codepointat": "^0.2.1" }, "bin": { "ot": "bin/ot" } }, "sha512-3NgmNyH3l/Hv6EvsWJbsvpcpUba6R8IREQ83nH83cyakCw7uM1arZKNfHwv1Wz6jgqrF/j4x5ELvR6PnK9nTcA=="], + "@sim/audit": ["@sim/audit@workspace:packages/audit"], + + "@sim/auth": ["@sim/auth@workspace:packages/auth"], + "@sim/db": ["@sim/db@workspace:packages/db"], "@sim/logger": ["@sim/logger@workspace:packages/logger"], + "@sim/realtime": ["@sim/realtime@workspace:apps/realtime"], + + "@sim/realtime-protocol": ["@sim/realtime-protocol@workspace:packages/realtime-protocol"], + + "@sim/security": ["@sim/security@workspace:packages/security"], + "@sim/testing": ["@sim/testing@workspace:packages/testing"], "@sim/tsconfig": ["@sim/tsconfig@workspace:packages/tsconfig"], "@sim/utils": ["@sim/utils@workspace:packages/utils"], + "@sim/workflow-authz": ["@sim/workflow-authz@workspace:packages/workflow-authz"], + + "@sim/workflow-persistence": ["@sim/workflow-persistence@workspace:packages/workflow-persistence"], + + "@sim/workflow-types": ["@sim/workflow-types@workspace:packages/workflow-types"], + "@simplewebauthn/browser": ["@simplewebauthn/browser@13.3.0", "", {}, "sha512-BE/UWv6FOToAdVk0EokzkqQQDOWtNydYlY6+OrmiZ5SCNmb41VehttboTetUM3T/fr6EAFYVXjz4My2wg230rQ=="], "@simplewebauthn/server": ["@simplewebauthn/server@13.3.0", "", { "dependencies": { "@hexagon/base64": "^1.1.27", "@levischuck/tiny-cbor": "^0.2.2", "@peculiar/asn1-android": "^2.6.0", "@peculiar/asn1-ecc": "^2.6.1", "@peculiar/asn1-rsa": "^2.6.1", "@peculiar/asn1-schema": "^2.6.0", "@peculiar/asn1-x509": "^2.6.1", "@peculiar/x509": "^1.14.3" } }, "sha512-MLHYFrYG8/wK2i+86XMhiecK72nMaHKKt4bo+7Q1TbuG9iGjlSdfkPWKO5ZFE/BX+ygCJ7pr8H/AJeyAj1EaTQ=="], @@ -4995,6 +5133,10 @@ "@shuding/opentype.js/fflate": ["fflate@0.7.4", "", {}, "sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw=="], + "@sim/realtime/@types/node": ["@types/node@24.2.1", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ=="], + + "@sim/security/@types/node": ["@types/node@24.2.1", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ=="], + "@smithy/core/@smithy/util-stream": ["@smithy/util-stream@4.5.21", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.15", "@smithy/node-http-handler": "^4.5.1", "@smithy/types": "^4.13.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-buffer-from": "^4.2.2", "@smithy/util-hex-encoding": "^4.2.2", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-KzSg+7KKywLnkoKejRtIBXDmwBfjGvg1U1i/etkC7XSWUyFCoLno1IohV2c74IzQqdhX5y3uE44r/8/wuK+A7Q=="], "@smithy/middleware-compression/fflate": ["fflate@0.8.1", "", {}, "sha512-/exOvEuc+/iaUm105QIiOt4LpBdMTWsXxqR0HDF35vx3fmaKzw7354gTilCh5rkzEt8WYyG//ku3h3nRmd7CHQ=="], @@ -6003,6 +6145,10 @@ "@shikijs/rehype/shiki/@shikijs/themes": ["@shikijs/themes@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA=="], + "@sim/realtime/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], + + "@sim/security/@types/node/undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], + "@trigger.dev/core/@opentelemetry/core/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], "@trigger.dev/core/@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.203.0", "", { "dependencies": { "@opentelemetry/core": "2.0.1", "@opentelemetry/otlp-transformer": "0.203.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-Wbxf7k+87KyvxFr5D7uOiSq/vHXWommvdnNE7vECO3tAhsA2GfOlpWINCMWUEPdHZ7tCXxw6Epp3vgx3jU7llQ=="], diff --git a/docker/app.Dockerfile b/docker/app.Dockerfile index eab09c9b157..366312273fd 100644 --- a/docker/app.Dockerfile +++ b/docker/app.Dockerfile @@ -18,13 +18,30 @@ FROM base AS deps WORKDIR /app COPY package.json bun.lock turbo.json ./ -RUN mkdir -p apps packages/db packages/testing packages/logger packages/tsconfig packages/utils +RUN mkdir -p apps \ + packages/audit \ + packages/db \ + packages/logger \ + packages/realtime-protocol \ + packages/security \ + packages/testing \ + packages/tsconfig \ + packages/utils \ + packages/workflow-authz \ + packages/workflow-persistence \ + packages/workflow-types COPY apps/sim/package.json ./apps/sim/package.json +COPY packages/audit/package.json ./packages/audit/package.json COPY packages/db/package.json ./packages/db/package.json -COPY packages/testing/package.json ./packages/testing/package.json COPY packages/logger/package.json ./packages/logger/package.json +COPY packages/realtime-protocol/package.json ./packages/realtime-protocol/package.json +COPY packages/security/package.json ./packages/security/package.json +COPY packages/testing/package.json ./packages/testing/package.json COPY packages/tsconfig/package.json ./packages/tsconfig/package.json COPY packages/utils/package.json ./packages/utils/package.json +COPY packages/workflow-authz/package.json ./packages/workflow-authz/package.json +COPY packages/workflow-persistence/package.json ./packages/workflow-persistence/package.json +COPY packages/workflow-types/package.json ./packages/workflow-types/package.json # Install dependencies, then rebuild isolated-vm for Node.js # Use --linker=hoisted for flat node_modules layout (required for Docker multi-stage builds) @@ -49,10 +66,17 @@ COPY --from=deps /app/node_modules ./node_modules # Copy package configuration files (needed for build) COPY package.json bun.lock turbo.json ./ COPY apps/sim/package.json ./apps/sim/package.json +COPY packages/audit/package.json ./packages/audit/package.json COPY packages/db/package.json ./packages/db/package.json -COPY packages/testing/package.json ./packages/testing/package.json COPY packages/logger/package.json ./packages/logger/package.json +COPY packages/realtime-protocol/package.json ./packages/realtime-protocol/package.json +COPY packages/security/package.json ./packages/security/package.json +COPY packages/testing/package.json ./packages/testing/package.json +COPY packages/tsconfig/package.json ./packages/tsconfig/package.json COPY packages/utils/package.json ./packages/utils/package.json +COPY packages/workflow-authz/package.json ./packages/workflow-authz/package.json +COPY packages/workflow-persistence/package.json ./packages/workflow-persistence/package.json +COPY packages/workflow-types/package.json ./packages/workflow-types/package.json # Copy workspace configuration files (needed for turbo) COPY apps/sim/next.config.ts ./apps/sim/next.config.ts diff --git a/docker/realtime.Dockerfile b/docker/realtime.Dockerfile index 4bd565c39e5..8092709267f 100644 --- a/docker/realtime.Dockerfile +++ b/docker/realtime.Dockerfile @@ -6,45 +6,28 @@ FROM oven/bun:1.3.11-alpine AS base RUN apk add --no-cache libc6-compat curl # ======================================== -# Dependencies Stage: Install Dependencies +# Pruner Stage: Emit a minimal monorepo subset that @sim/realtime depends on # ======================================== -FROM base AS deps +FROM base AS pruner WORKDIR /app -COPY package.json bun.lock turbo.json ./ -RUN mkdir -p apps packages/db packages/testing packages/logger packages/tsconfig packages/utils -COPY apps/sim/package.json ./apps/sim/package.json -COPY packages/db/package.json ./packages/db/package.json -COPY packages/testing/package.json ./packages/testing/package.json -COPY packages/logger/package.json ./packages/logger/package.json -COPY packages/tsconfig/package.json ./packages/tsconfig/package.json -COPY packages/utils/package.json ./packages/utils/package.json +RUN bun add -g turbo -# Install dependencies with hoisted layout for Docker compatibility -# Using --linker=hoisted to avoid .bun directory symlinks that don't copy between stages -RUN --mount=type=cache,id=bun-cache,target=/root/.bun/install/cache \ - bun install --omit=dev --ignore-scripts --linker=hoisted +COPY . . + +RUN turbo prune @sim/realtime --docker # ======================================== -# Builder Stage: Prepare source code +# Dependencies Stage: Install Dependencies # ======================================== -FROM base AS builder +FROM base AS deps WORKDIR /app -# Copy node_modules from deps stage (cached if dependencies don't change) -COPY --from=deps /app/node_modules ./node_modules +COPY --from=pruner /app/out/json/ ./ +COPY --from=pruner /app/out/bun.lock ./bun.lock -# Copy package configuration files (needed for build) -COPY package.json bun.lock turbo.json ./ -COPY apps/sim/package.json ./apps/sim/package.json -COPY packages/db/package.json ./packages/db/package.json -COPY packages/testing/package.json ./packages/testing/package.json -COPY packages/logger/package.json ./packages/logger/package.json -COPY packages/utils/package.json ./packages/utils/package.json - -# Copy source code (changes most frequently - placed last to maximize cache hits) -COPY apps/sim ./apps/sim -COPY packages ./packages +RUN --mount=type=cache,id=bun-cache,target=/root/.bun/install/cache \ + bun install --linker=hoisted --omit=dev --ignore-scripts # ======================================== # Runner Stage: Run the Socket Server @@ -52,38 +35,18 @@ COPY packages ./packages FROM base AS runner WORKDIR /app -ENV NODE_ENV=production +ENV NODE_ENV=production \ + PORT=3002 \ + HOSTNAME="0.0.0.0" -# Create non-root user and group (cached separately) RUN addgroup -g 1001 -S nodejs && \ adduser -S nextjs -u 1001 -# Copy package.json first (changes less frequently) -COPY --from=builder --chown=nextjs:nodejs /app/package.json ./package.json - -# Copy node_modules from builder (cached if dependencies don't change) -COPY --from=builder --chown=nextjs:nodejs /app/node_modules ./node_modules - -# Copy db package (needed by socket) -COPY --from=builder --chown=nextjs:nodejs /app/packages/db ./packages/db - -# Copy logger package (workspace dependency used by socket) -COPY --from=builder --chown=nextjs:nodejs /app/packages/logger ./packages/logger +COPY --from=deps --chown=nextjs:nodejs /app ./ +COPY --from=pruner --chown=nextjs:nodejs /app/out/full/ ./ -# Copy utils package (workspace dependency used by socket) -COPY --from=builder --chown=nextjs:nodejs /app/packages/utils ./packages/utils - -# Copy sim app (changes most frequently - placed last) -COPY --from=builder --chown=nextjs:nodejs /app/apps/sim ./apps/sim - -# Switch to non-root user USER nextjs -# Expose socket server port (default 3002, but configurable via PORT env var) EXPOSE 3002 -ENV PORT=3002 \ - SOCKET_PORT=3002 \ - HOSTNAME="0.0.0.0" -# Run the socket server directly -CMD ["bun", "apps/sim/socket/index.ts"] \ No newline at end of file +CMD ["bun", "apps/realtime/src/index.ts"] diff --git a/package.json b/package.json index 335e8831934..ee8302a95b5 100644 --- a/package.json +++ b/package.json @@ -11,8 +11,8 @@ "scripts": { "build": "turbo run build", "dev": "turbo run dev", - "dev:sockets": "cd apps/sim && bun run dev:sockets", - "dev:full": "cd apps/sim && bun run dev:full", + "dev:sockets": "cd apps/realtime && bun run dev", + "dev:full": "bunx concurrently -n \"App,Realtime\" -c \"cyan,magenta\" \"cd apps/sim && bun run dev\" \"cd apps/realtime && bun run dev\"", "test": "turbo run test", "format": "turbo run format", "format:check": "turbo run format:check", @@ -21,6 +21,8 @@ "lint:helm": "helm lint ./helm/sim --strict --values ./helm/sim/test/values-lint.yaml", "lint:all": "turbo run lint && bun run lint:helm", "check": "turbo run format:check", + "check:boundaries": "bun run scripts/check-monorepo-boundaries.ts", + "check:realtime-prune": "bun run scripts/check-realtime-prune-graph.ts", "mship-contracts:generate": "bun run scripts/sync-mothership-stream-contract.ts", "mship-contracts:check": "bun run scripts/sync-mothership-stream-contract.ts --check", "mship-tools:generate": "bun run scripts/sync-tool-catalog.ts", @@ -65,8 +67,5 @@ }, "trustedDependencies": [ "sharp" - ], - "dependencies": { - "@aws-sdk/client-athena": "3.1024.0" - } + ] } diff --git a/packages/audit/package.json b/packages/audit/package.json new file mode 100644 index 00000000000..bad5fe28b39 --- /dev/null +++ b/packages/audit/package.json @@ -0,0 +1,39 @@ +{ + "name": "@sim/audit", + "version": "0.1.0", + "private": true, + "sideEffects": false, + "type": "module", + "license": "Apache-2.0", + "engines": { + "bun": ">=1.2.13", + "node": ">=20.0.0" + }, + "exports": { + ".": { + "types": "./src/index.ts", + "default": "./src/index.ts" + } + }, + "scripts": { + "type-check": "tsc --noEmit", + "lint": "biome check --write --unsafe .", + "lint:check": "biome check .", + "format": "biome format --write .", + "format:check": "biome format .", + "test": "vitest run", + "test:watch": "vitest" + }, + "dependencies": { + "@sim/db": "workspace:*", + "@sim/logger": "workspace:*", + "@sim/utils": "workspace:*", + "drizzle-orm": "^0.45.2" + }, + "devDependencies": { + "@sim/testing": "workspace:*", + "@sim/tsconfig": "workspace:*", + "typescript": "^5.7.3", + "vitest": "^3.0.8" + } +} diff --git a/packages/audit/src/index.ts b/packages/audit/src/index.ts new file mode 100644 index 00000000000..f3ec73f8c5d --- /dev/null +++ b/packages/audit/src/index.ts @@ -0,0 +1,3 @@ +export { recordAudit } from './log' +export type { AuditActionType, AuditResourceTypeValue } from './types' +export { AuditAction, AuditResourceType } from './types' diff --git a/apps/sim/lib/audit/log.test.ts b/packages/audit/src/log.test.ts similarity index 93% rename from apps/sim/lib/audit/log.test.ts rename to packages/audit/src/log.test.ts index 1c755867462..19a44aea416 100644 --- a/apps/sim/lib/audit/log.test.ts +++ b/packages/audit/src/log.test.ts @@ -13,6 +13,21 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' vi.mock('@sim/db', () => ({ ...dbChainMock, auditLog: { id: 'id', workspaceId: 'workspace_id' }, + user: { id: 'id', name: 'name', email: 'email' }, +})) +vi.mock('drizzle-orm', () => ({ + eq: vi.fn(), + and: vi.fn(), + or: vi.fn(), + sql: vi.fn(), +})) +vi.mock('@sim/logger', () => ({ + createLogger: () => ({ + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + debug: vi.fn(), + }), })) vi.mock('@sim/utils/id', () => ({ generateId: () => 'test-uuid-123', @@ -21,7 +36,7 @@ vi.mock('@sim/utils/id', () => ({ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(v), })) -import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log' +import { AuditAction, AuditResourceType, recordAudit } from './index' const flush = () => new Promise((resolve) => setTimeout(resolve, 10)) @@ -356,8 +371,9 @@ describe('auditMock sync', () => { }) it('has the same AuditAction values as the source', () => { + const mockActions = auditMock.AuditAction as Record for (const key of Object.keys(AuditAction)) { - expect(auditMock.AuditAction[key]).toBe(AuditAction[key as keyof typeof AuditAction]) + expect(mockActions[key]).toBe(AuditAction[key as keyof typeof AuditAction]) } }) @@ -368,10 +384,9 @@ describe('auditMock sync', () => { }) it('has the same AuditResourceType values as the source', () => { + const mockResourceTypes = auditMock.AuditResourceType as Record for (const key of Object.keys(AuditResourceType)) { - expect(auditMock.AuditResourceType[key]).toBe( - AuditResourceType[key as keyof typeof AuditResourceType] - ) + expect(mockResourceTypes[key]).toBe(AuditResourceType[key as keyof typeof AuditResourceType]) } }) }) diff --git a/apps/sim/lib/audit/log.ts b/packages/audit/src/log.ts similarity index 75% rename from apps/sim/lib/audit/log.ts rename to packages/audit/src/log.ts index 3176fcbee9a..4ee040ba43a 100644 --- a/apps/sim/lib/audit/log.ts +++ b/packages/audit/src/log.ts @@ -1,13 +1,8 @@ -import { auditLog, db } from '@sim/db' -import { user } from '@sim/db/schema' +import { auditLog, db, user } from '@sim/db' import { createLogger } from '@sim/logger' import { generateShortId } from '@sim/utils/id' import { eq } from 'drizzle-orm' -import type { AuditActionType, AuditResourceTypeValue } from '@/lib/audit/types' -import { getClientIp } from '@/lib/core/utils/request' - -export type { AuditActionType, AuditResourceTypeValue } from '@/lib/audit/types' -export { AuditAction, AuditResourceType } from '@/lib/audit/types' +import type { AuditActionType, AuditResourceTypeValue } from './types' const logger = createLogger('AuditLog') @@ -22,13 +17,20 @@ interface AuditLogParams { resourceName?: string description?: string metadata?: Record - request?: Request + request?: { headers: { get(name: string): string | null } } +} + +function getClientIp(request: { headers: { get(name: string): string | null } }): string { + return ( + request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() || + request.headers.get('x-real-ip')?.trim() || + 'unknown' + ) } /** - * Records an audit log entry. Fire-and-forget — never throws or blocks the caller. - * If actorName and actorEmail are both undefined (not provided by the caller), - * resolves them from the user table before inserting. + * Fire-and-forget audit log write. Never throws; failures are logged. + * Resolves actorName/actorEmail from the user table when both are omitted. */ export function recordAudit(params: AuditLogParams): void { insertAuditLog(params).catch((error) => { diff --git a/apps/sim/lib/audit/types.ts b/packages/audit/src/types.ts similarity index 100% rename from apps/sim/lib/audit/types.ts rename to packages/audit/src/types.ts diff --git a/packages/audit/tsconfig.json b/packages/audit/tsconfig.json new file mode 100644 index 00000000000..7b64383489d --- /dev/null +++ b/packages/audit/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "@sim/tsconfig/library.json", + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.test.tsx"] +} diff --git a/packages/audit/vitest.config.ts b/packages/audit/vitest.config.ts new file mode 100644 index 00000000000..471771e48fe --- /dev/null +++ b/packages/audit/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + globals: false, + environment: 'node', + include: ['src/**/*.test.ts'], + }, +}) diff --git a/packages/auth/package.json b/packages/auth/package.json new file mode 100644 index 00000000000..b44406abdfc --- /dev/null +++ b/packages/auth/package.json @@ -0,0 +1,33 @@ +{ + "name": "@sim/auth", + "version": "0.1.0", + "private": true, + "sideEffects": false, + "type": "module", + "license": "Apache-2.0", + "engines": { + "bun": ">=1.2.13", + "node": ">=20.0.0" + }, + "exports": { + "./verify": { + "types": "./src/verify.ts", + "default": "./src/verify.ts" + } + }, + "scripts": { + "type-check": "tsc --noEmit", + "lint": "biome check --write --unsafe .", + "lint:check": "biome check .", + "format": "biome format --write .", + "format:check": "biome format ." + }, + "dependencies": { + "@sim/db": "workspace:*", + "better-auth": "1.3.12" + }, + "devDependencies": { + "@sim/tsconfig": "workspace:*", + "typescript": "^5.7.3" + } +} diff --git a/packages/auth/src/verify.ts b/packages/auth/src/verify.ts new file mode 100644 index 00000000000..38fe06df323 --- /dev/null +++ b/packages/auth/src/verify.ts @@ -0,0 +1,36 @@ +import { db } from '@sim/db' +import * as schema from '@sim/db/schema' +import { betterAuth } from 'better-auth' +import { drizzleAdapter } from 'better-auth/adapters/drizzle' +import { oneTimeToken } from 'better-auth/plugins' + +export interface VerifyAuthOptions { + /** Better Auth shared secret. Must match the apps/sim Better Auth secret. */ + secret: string + /** Public-facing Better Auth URL (usually same as NEXT_PUBLIC_APP_URL). */ + baseURL: string +} + +/** + * Minimal Better Auth instance used by services that only need to verify + * one-time tokens issued by the main app. Shares the Better Auth DB schema + * (`verification` table) and secret with the main app, so tokens issued by + * `apps/sim`'s full auth config are accepted here. + */ +export function createVerifyAuth(options: VerifyAuthOptions) { + return betterAuth({ + baseURL: options.baseURL, + secret: options.secret, + database: drizzleAdapter(db, { + provider: 'pg', + schema, + }), + plugins: [ + oneTimeToken({ + expiresIn: 24 * 60 * 60, + }), + ], + }) +} + +export type VerifyAuth = ReturnType diff --git a/packages/auth/tsconfig.json b/packages/auth/tsconfig.json new file mode 100644 index 00000000000..1ffa3d2e844 --- /dev/null +++ b/packages/auth/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "@sim/tsconfig/library.json", + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/db/.env.example b/packages/db/.env.example index bc06b7099c5..14459e61f51 100644 --- a/packages/db/.env.example +++ b/packages/db/.env.example @@ -1,2 +1,5 @@ -# Database URL (Required for migrations and database operations) -DATABASE_URL="postgresql://postgres:your_password@localhost:5432/simstudio" \ No newline at end of file +# Database connection used by @sim/db scripts (drizzle-kit generate, +# db:migrate, register-sso-provider, etc.). Must match DATABASE_URL in +# apps/sim/.env and apps/realtime/.env. + +DATABASE_URL="postgresql://postgres:postgres@localhost:5432/simstudio" diff --git a/packages/realtime-protocol/package.json b/packages/realtime-protocol/package.json new file mode 100644 index 00000000000..63590ce1b8e --- /dev/null +++ b/packages/realtime-protocol/package.json @@ -0,0 +1,36 @@ +{ + "name": "@sim/realtime-protocol", + "version": "0.1.0", + "private": true, + "sideEffects": false, + "type": "module", + "license": "Apache-2.0", + "engines": { + "bun": ">=1.2.13", + "node": ">=20.0.0" + }, + "exports": { + "./constants": { + "types": "./src/constants.ts", + "default": "./src/constants.ts" + }, + "./schemas": { + "types": "./src/schemas.ts", + "default": "./src/schemas.ts" + } + }, + "scripts": { + "type-check": "tsc --noEmit", + "lint": "biome check --write --unsafe .", + "lint:check": "biome check .", + "format": "biome format --write .", + "format:check": "biome format ." + }, + "dependencies": { + "zod": "^3.24.2" + }, + "devDependencies": { + "@sim/tsconfig": "workspace:*", + "typescript": "^5.7.3" + } +} diff --git a/apps/sim/socket/constants.ts b/packages/realtime-protocol/src/constants.ts similarity index 100% rename from apps/sim/socket/constants.ts rename to packages/realtime-protocol/src/constants.ts diff --git a/apps/sim/socket/validation/schemas.ts b/packages/realtime-protocol/src/schemas.ts similarity index 99% rename from apps/sim/socket/validation/schemas.ts rename to packages/realtime-protocol/src/schemas.ts index fc48500ab64..19d76437ab2 100644 --- a/apps/sim/socket/validation/schemas.ts +++ b/packages/realtime-protocol/src/schemas.ts @@ -8,7 +8,7 @@ import { SUBFLOW_OPERATIONS, VARIABLE_OPERATIONS, WORKFLOW_OPERATIONS, -} from '@/socket/constants' +} from './constants' const PositionSchema = z.object({ x: z.number(), diff --git a/packages/realtime-protocol/tsconfig.json b/packages/realtime-protocol/tsconfig.json new file mode 100644 index 00000000000..1ffa3d2e844 --- /dev/null +++ b/packages/realtime-protocol/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "@sim/tsconfig/library.json", + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/security/package.json b/packages/security/package.json new file mode 100644 index 00000000000..dc823111799 --- /dev/null +++ b/packages/security/package.json @@ -0,0 +1,50 @@ +{ + "name": "@sim/security", + "version": "0.1.0", + "private": true, + "sideEffects": false, + "type": "module", + "license": "Apache-2.0", + "engines": { + "bun": ">=1.2.13", + "node": ">=20.0.0" + }, + "exports": { + "./compare": { + "types": "./src/compare.ts", + "default": "./src/compare.ts" + }, + "./encryption": { + "types": "./src/encryption.ts", + "default": "./src/encryption.ts" + }, + "./hash": { + "types": "./src/hash.ts", + "default": "./src/hash.ts" + }, + "./hmac": { + "types": "./src/hmac.ts", + "default": "./src/hmac.ts" + }, + "./tokens": { + "types": "./src/tokens.ts", + "default": "./src/tokens.ts" + } + }, + "scripts": { + "type-check": "tsc --noEmit", + "lint": "biome check --write --unsafe .", + "lint:check": "biome check .", + "format": "biome format --write .", + "format:check": "biome format .", + "test": "vitest run", + "test:watch": "vitest" + }, + "dependencies": {}, + "devDependencies": { + "@sim/tsconfig": "workspace:*", + "@types/node": "24.2.1", + "typescript": "^5.7.3", + "vitest": "^3.0.8" + } +} diff --git a/packages/security/src/compare.test.ts b/packages/security/src/compare.test.ts new file mode 100644 index 00000000000..265235e65b5 --- /dev/null +++ b/packages/security/src/compare.test.ts @@ -0,0 +1,40 @@ +import { describe, expect, it } from 'vitest' +import { safeCompare } from './compare' + +describe('safeCompare', () => { + it('returns true for identical strings', () => { + expect(safeCompare('abc', 'abc')).toBe(true) + }) + + it('returns false for equal-length different strings', () => { + expect(safeCompare('abc', 'abd')).toBe(false) + }) + + it('returns false for different-length strings without throwing', () => { + expect(safeCompare('short', 'longer-value')).toBe(false) + expect(safeCompare('', 'a')).toBe(false) + expect(safeCompare('a', '')).toBe(false) + }) + + it('returns true for two empty strings', () => { + expect(safeCompare('', '')).toBe(true) + }) + + it('handles long inputs', () => { + const a = 'x'.repeat(10_000) + const b = 'x'.repeat(10_000) + expect(safeCompare(a, b)).toBe(true) + expect(safeCompare(a, `${b.slice(0, -1)}y`)).toBe(false) + }) + + it('is case-sensitive', () => { + expect(safeCompare('ABC', 'abc')).toBe(false) + }) + + it('distinguishes hex digests that differ in one nibble', () => { + const a = 'b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7' + const b = 'b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff8' + expect(safeCompare(a, b)).toBe(false) + expect(safeCompare(a, a)).toBe(true) + }) +}) diff --git a/packages/security/src/compare.ts b/packages/security/src/compare.ts new file mode 100644 index 00000000000..3d68040e145 --- /dev/null +++ b/packages/security/src/compare.ts @@ -0,0 +1,13 @@ +import { createHmac, timingSafeEqual } from 'node:crypto' + +/** + * Constant-time string comparison using HMAC-digest wrapping to handle + * inputs of differing length. Use for HMAC signatures, API keys, and other + * secrets where leaking length or content via timing must be avoided. + */ +export function safeCompare(a: string, b: string): boolean { + const key = 'safeCompare' + const ha = createHmac('sha256', key).update(a).digest() + const hb = createHmac('sha256', key).update(b).digest() + return timingSafeEqual(ha, hb) +} diff --git a/packages/security/src/encryption.test.ts b/packages/security/src/encryption.test.ts new file mode 100644 index 00000000000..13369500fd1 --- /dev/null +++ b/packages/security/src/encryption.test.ts @@ -0,0 +1,74 @@ +import { randomBytes } from 'node:crypto' +import { describe, expect, it } from 'vitest' +import { decrypt, encrypt } from './encryption' + +const KEY = Buffer.from('0'.repeat(64), 'hex') + +describe('encrypt', () => { + it('returns iv:ciphertext:authTag and a 32-char hex IV', async () => { + const result = await encrypt('secret', KEY) + expect(result.encrypted.split(':')).toHaveLength(3) + expect(result.iv).toHaveLength(32) + }) + + it('produces distinct ciphertexts for the same input', async () => { + const a = await encrypt('same', KEY) + const b = await encrypt('same', KEY) + expect(a.encrypted).not.toBe(b.encrypted) + }) + + it('rejects keys that are not 32 bytes', async () => { + await expect(encrypt('x', Buffer.alloc(16))).rejects.toThrow(/32 bytes/) + }) +}) + +describe('decrypt', () => { + it('round-trips arbitrary UTF-8 input', async () => { + const plaintext = 'Hello, !"#$%&\'()*+,-./0123456789:;<=>?@' + const { encrypted } = await encrypt(plaintext, KEY) + const { decrypted } = await decrypt(encrypted, KEY) + expect(decrypted).toBe(plaintext) + }) + + it('round-trips empty strings', async () => { + const { encrypted } = await encrypt('', KEY) + const { decrypted } = await decrypt(encrypted, KEY) + expect(decrypted).toBe('') + }) + + it('round-trips long inputs', async () => { + const plaintext = 'a'.repeat(10_000) + const { encrypted } = await encrypt(plaintext, KEY) + const { decrypted } = await decrypt(encrypted, KEY) + expect(decrypted).toBe(plaintext) + }) + + it('throws on malformed input', async () => { + await expect(decrypt('invalid', KEY)).rejects.toThrow( + 'Invalid encrypted value format. Expected "iv:encrypted:authTag"' + ) + await expect(decrypt('part1:part2', KEY)).rejects.toThrow( + 'Invalid encrypted value format. Expected "iv:encrypted:authTag"' + ) + }) + + it('throws when ciphertext is tampered', async () => { + const { encrypted } = await encrypt('original', KEY) + const parts = encrypted.split(':') + parts[1] = `deadbeef${parts[1].slice(8)}` + await expect(decrypt(parts.join(':'), KEY)).rejects.toThrow() + }) + + it('throws when auth tag is tampered', async () => { + const { encrypted } = await encrypt('original', KEY) + const parts = encrypted.split(':') + parts[2] = '0'.repeat(32) + await expect(decrypt(parts.join(':'), KEY)).rejects.toThrow() + }) + + it('throws when decrypted with a different key', async () => { + const { encrypted } = await encrypt('original', KEY) + const otherKey = randomBytes(32) + await expect(decrypt(encrypted, otherKey)).rejects.toThrow() + }) +}) diff --git a/packages/security/src/encryption.ts b/packages/security/src/encryption.ts new file mode 100644 index 00000000000..c1bbf078e38 --- /dev/null +++ b/packages/security/src/encryption.ts @@ -0,0 +1,68 @@ +import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto' + +/** + * AES-256-GCM encryption primitive. Produces a self-contained string in the + * format `iv:ciphertext:authTag` (all hex-encoded) that can be stored and + * later passed directly to {@link decrypt}. + * + * @param plaintext - UTF-8 string to encrypt + * @param key - 32-byte encryption key + */ +export async function encrypt( + plaintext: string, + key: Buffer +): Promise<{ encrypted: string; iv: string }> { + assertKey(key) + + const iv = randomBytes(16) + const cipher = createCipheriv('aes-256-gcm', key, iv, { authTagLength: 16 }) + let encrypted = cipher.update(plaintext, 'utf8', 'hex') + encrypted += cipher.final('hex') + + const authTag = cipher.getAuthTag() + const ivHex = iv.toString('hex') + + return { + encrypted: `${ivHex}:${encrypted}:${authTag.toString('hex')}`, + iv: ivHex, + } +} + +/** + * AES-256-GCM decryption primitive. Expects input produced by {@link encrypt} + * in the format `iv:ciphertext:authTag`. Throws when the format is malformed + * or when the GCM auth tag does not verify (tampered ciphertext, wrong key). + */ +export async function decrypt(encryptedValue: string, key: Buffer): Promise<{ decrypted: string }> { + assertKey(key) + + const parts = encryptedValue.split(':') + if (parts.length < 3) { + throw new Error('Invalid encrypted value format. Expected "iv:encrypted:authTag"') + } + + const ivHex = parts[0] + const authTagHex = parts[parts.length - 1] + const encrypted = parts.slice(1, -1).join(':') + + if (!ivHex || !authTagHex) { + throw new Error('Invalid encrypted value format. Expected "iv:encrypted:authTag"') + } + + const iv = Buffer.from(ivHex, 'hex') + const authTag = Buffer.from(authTagHex, 'hex') + + const decipher = createDecipheriv('aes-256-gcm', key, iv, { authTagLength: 16 }) + decipher.setAuthTag(authTag) + + let decrypted = decipher.update(encrypted, 'hex', 'utf8') + decrypted += decipher.final('utf8') + + return { decrypted } +} + +function assertKey(key: Buffer): void { + if (key.length !== 32) { + throw new Error('Encryption key must be 32 bytes (256 bits)') + } +} diff --git a/packages/security/src/hash.test.ts b/packages/security/src/hash.test.ts new file mode 100644 index 00000000000..89f9b0929ae --- /dev/null +++ b/packages/security/src/hash.test.ts @@ -0,0 +1,20 @@ +import { describe, expect, it } from 'vitest' +import { sha256Hex } from './hash' + +describe('sha256Hex', () => { + it('is deterministic', () => { + expect(sha256Hex('hello')).toBe(sha256Hex('hello')) + }) + + it('returns a 64-char hex digest', () => { + expect(sha256Hex('hello')).toMatch(/^[0-9a-f]{64}$/) + }) + + it('matches the published vector for the empty string', () => { + expect(sha256Hex('')).toBe('e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855') + }) + + it('differs for different inputs', () => { + expect(sha256Hex('a')).not.toBe(sha256Hex('b')) + }) +}) diff --git a/packages/security/src/hash.ts b/packages/security/src/hash.ts new file mode 100644 index 00000000000..9a9a18b498d --- /dev/null +++ b/packages/security/src/hash.ts @@ -0,0 +1,10 @@ +import { createHash } from 'node:crypto' + +/** + * Deterministic SHA-256 digest of a UTF-8 string, hex-encoded. Use for + * indexed lookup of sensitive values (e.g. API key hash columns) where the + * caller only needs to verify equality without ever reversing the hash. + */ +export function sha256Hex(input: string): string { + return createHash('sha256').update(input, 'utf8').digest('hex') +} diff --git a/packages/security/src/hmac.test.ts b/packages/security/src/hmac.test.ts new file mode 100644 index 00000000000..8daecafa0d0 --- /dev/null +++ b/packages/security/src/hmac.test.ts @@ -0,0 +1,55 @@ +import { describe, expect, it } from 'vitest' +import { hmacSha256Base64, hmacSha256Hex } from './hmac' + +describe('hmacSha256Hex', () => { + it('is deterministic', () => { + expect(hmacSha256Hex('body', 'secret')).toBe(hmacSha256Hex('body', 'secret')) + }) + + it('returns a 64-char hex digest', () => { + expect(hmacSha256Hex('body', 'secret')).toMatch(/^[0-9a-f]{64}$/) + }) + + it('matches RFC 4231 test vector 1', () => { + const key = Buffer.from('0b'.repeat(20), 'hex').toString('binary') + expect(hmacSha256Hex('Hi There', key)).toBe( + 'b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7' + ) + }) + + it('differs when body changes', () => { + expect(hmacSha256Hex('a', 'k')).not.toBe(hmacSha256Hex('b', 'k')) + }) + + it('differs when secret changes', () => { + expect(hmacSha256Hex('body', 'k1')).not.toBe(hmacSha256Hex('body', 'k2')) + }) + + it('accepts a Buffer secret and matches the equivalent binary-string secret', () => { + const raw = Buffer.from('0b'.repeat(20), 'hex') + expect(hmacSha256Hex('Hi There', raw)).toBe(hmacSha256Hex('Hi There', raw.toString('binary'))) + }) +}) + +describe('hmacSha256Base64', () => { + it('is deterministic', () => { + expect(hmacSha256Base64('body', 'secret')).toBe(hmacSha256Base64('body', 'secret')) + }) + + it('returns a base64 digest', () => { + expect(hmacSha256Base64('body', 'secret')).toMatch(/^[A-Za-z0-9+/]+=*$/) + }) + + it('agrees with hex form via Buffer conversion', () => { + const hex = hmacSha256Hex('body', 'secret') + const b64 = hmacSha256Base64('body', 'secret') + expect(Buffer.from(b64, 'base64').toString('hex')).toBe(hex) + }) + + it('accepts a Buffer secret (Svix / MS-Teams scheme)', () => { + const secret = Buffer.from('whsec-decoded-bytes') + const hex = hmacSha256Hex('body', secret) + const b64 = hmacSha256Base64('body', secret) + expect(Buffer.from(b64, 'base64').toString('hex')).toBe(hex) + }) +}) diff --git a/packages/security/src/hmac.ts b/packages/security/src/hmac.ts new file mode 100644 index 00000000000..58512634106 --- /dev/null +++ b/packages/security/src/hmac.ts @@ -0,0 +1,24 @@ +import { createHmac } from 'node:crypto' + +/** + * HMAC-SHA256 of a UTF-8 body using the given secret, hex-encoded. Use for + * webhook signature verification where the provider sends a hex digest + * (e.g. `X-Signature: ` or `X-Hub-Signature-256: sha256=`). Pass the + * secret as a `Buffer` when the provider's scheme requires base64-decoding + * (e.g. Svix-compatible `whsec_...` secrets). Pair with + * {@link ../compare | safeCompare} for timing-safe comparison. + */ +export function hmacSha256Hex(body: string, secret: string | Buffer): string { + return createHmac('sha256', secret).update(body, 'utf8').digest('hex') +} + +/** + * HMAC-SHA256 of a UTF-8 body using the given secret, base64-encoded. Use for + * webhook signature verification where the provider sends a base64 digest + * (e.g. Typeform, Microsoft Teams outgoing webhooks). Pass the secret as a + * `Buffer` when the provider's scheme requires base64-decoding. Pair with + * {@link ../compare | safeCompare} for timing-safe comparison. + */ +export function hmacSha256Base64(body: string, secret: string | Buffer): string { + return createHmac('sha256', secret).update(body, 'utf8').digest('base64') +} diff --git a/packages/security/src/tokens.test.ts b/packages/security/src/tokens.test.ts new file mode 100644 index 00000000000..035b2a4e1f2 --- /dev/null +++ b/packages/security/src/tokens.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from 'vitest' +import { generateSecureToken } from './tokens' + +describe('generateSecureToken', () => { + it('defaults to 24 bytes (32-char base64url)', () => { + const token = generateSecureToken() + expect(token).toHaveLength(32) + expect(token).toMatch(/^[A-Za-z0-9_-]+$/) + }) + + it('honors a custom byte length', () => { + const token = generateSecureToken(16) + expect(token).toHaveLength(22) + }) + + it('never repeats across 1000 draws', () => { + const seen = new Set() + for (let i = 0; i < 1000; i++) seen.add(generateSecureToken()) + expect(seen.size).toBe(1000) + }) + + it('is URL-safe (no +, /, or = padding)', () => { + const token = generateSecureToken(64) + expect(token).not.toMatch(/[+/=]/) + }) +}) diff --git a/packages/security/src/tokens.ts b/packages/security/src/tokens.ts new file mode 100644 index 00000000000..311aaf46796 --- /dev/null +++ b/packages/security/src/tokens.ts @@ -0,0 +1,12 @@ +import { randomBytes } from 'node:crypto' + +/** + * Generate a cryptographically secure random token encoded as base64url. The + * returned string is URL-safe (no padding, no `+`/`/`) and suitable for use + * as an API key body, bearer token, or one-time identifier. + * + * @param byteLength - Number of random bytes to draw before encoding. Defaults to 24 (~32 chars). + */ +export function generateSecureToken(byteLength = 24): string { + return randomBytes(byteLength).toString('base64url') +} diff --git a/packages/security/tsconfig.json b/packages/security/tsconfig.json new file mode 100644 index 00000000000..1ffa3d2e844 --- /dev/null +++ b/packages/security/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "@sim/tsconfig/library.json", + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/security/vitest.config.ts b/packages/security/vitest.config.ts new file mode 100644 index 00000000000..471771e48fe --- /dev/null +++ b/packages/security/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + globals: false, + environment: 'node', + include: ['src/**/*.test.ts'], + }, +}) diff --git a/packages/testing/src/mocks/audit.mock.ts b/packages/testing/src/mocks/audit.mock.ts index 4dadec22819..58671d6ba46 100644 --- a/packages/testing/src/mocks/audit.mock.ts +++ b/packages/testing/src/mocks/audit.mock.ts @@ -1,7 +1,7 @@ import { vi } from 'vitest' /** - * Controllable mock functions for `@/lib/audit/log`. + * Controllable mock functions for `@sim/audit`. * Exposes `mockRecordAudit` so tests can assert or override behavior per test. * * @example @@ -17,11 +17,11 @@ export const auditMockFns = { } /** - * Static mock module for `@/lib/audit/log`. + * Static mock module for `@sim/audit`. * * @example * ```ts - * vi.mock('@/lib/audit/log', () => auditMock) + * vi.mock('@sim/audit', () => auditMock) * ``` */ export const auditMock = { diff --git a/packages/testing/src/mocks/index.ts b/packages/testing/src/mocks/index.ts index c3ab30078c5..46c55c2e0ac 100644 --- a/packages/testing/src/mocks/index.ts +++ b/packages/testing/src/mocks/index.ts @@ -120,6 +120,8 @@ export { export { telemetryMock } from './telemetry.mock' // URL mocks export { urlsMock, urlsMockFns } from './urls.mock' +// Workflow authz package mocks (for @sim/workflow-authz) +export { workflowAuthzMock, workflowAuthzMockFns } from './workflow-authz.mock' // Workflows API utils mocks (for @/app/api/workflows/utils) export { workflowsApiUtilsMock, workflowsApiUtilsMockFns } from './workflows-api-utils.mock' // Workflows orchestration mocks (for @/lib/workflows/orchestration) diff --git a/packages/testing/src/mocks/workflow-authz.mock.ts b/packages/testing/src/mocks/workflow-authz.mock.ts new file mode 100644 index 00000000000..dea86a272d4 --- /dev/null +++ b/packages/testing/src/mocks/workflow-authz.mock.ts @@ -0,0 +1,39 @@ +import { vi } from 'vitest' + +/** + * Controllable mocks for the `@sim/workflow-authz` package. + * + * @example + * ```ts + * import { workflowAuthzMockFns } from '@sim/testing' + * + * workflowAuthzMockFns.mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValue({ + * allowed: true, + * status: 200, + * workflow: { id: 'wf-1' }, + * workspacePermission: 'admin', + * }) + * ``` + */ +export const workflowAuthzMockFns = { + mockAuthorizeWorkflowByWorkspacePermission: vi.fn(), + mockGetActiveWorkflowContext: vi.fn(), + mockGetActiveWorkflowRecord: vi.fn(), + mockAssertActiveWorkflowContext: vi.fn(), +} + +/** + * Static mock module for `@sim/workflow-authz`. + * + * @example + * ```ts + * vi.mock('@sim/workflow-authz', () => workflowAuthzMock) + * ``` + */ +export const workflowAuthzMock = { + authorizeWorkflowByWorkspacePermission: + workflowAuthzMockFns.mockAuthorizeWorkflowByWorkspacePermission, + getActiveWorkflowContext: workflowAuthzMockFns.mockGetActiveWorkflowContext, + getActiveWorkflowRecord: workflowAuthzMockFns.mockGetActiveWorkflowRecord, + assertActiveWorkflowContext: workflowAuthzMockFns.mockAssertActiveWorkflowContext, +} diff --git a/packages/testing/src/mocks/workflows-utils.mock.ts b/packages/testing/src/mocks/workflows-utils.mock.ts index 0930d9b5dc9..1c70412f014 100644 --- a/packages/testing/src/mocks/workflows-utils.mock.ts +++ b/packages/testing/src/mocks/workflows-utils.mock.ts @@ -20,7 +20,6 @@ export const workflowsUtilsMockFns = { mockWorkflowHasResponseBlock: vi.fn(), mockCreateHttpResponseFromBlock: vi.fn(), mockValidateWorkflowPermissions: vi.fn(), - mockAuthorizeWorkflowByWorkspacePermission: vi.fn(), mockCreateWorkflowRecord: vi.fn(), mockUpdateWorkflowRecord: vi.fn(), mockDeleteWorkflowRecord: vi.fn(), @@ -38,10 +37,12 @@ export const workflowsUtilsMockFns = { * * Default behaviors: * - `getWorkflowById` resolves to `null` - * - `authorizeWorkflowByWorkspacePermission` resolves to allowed with `test-workspace-id` * - `validateWorkflowPermissions` resolves to an authorized result * - Other functions resolve to sensible empty/success defaults * + * `authorizeWorkflowByWorkspacePermission` moved to `@sim/workflow-authz`; + * use `workflowAuthzMock` / `workflowAuthzMockFns` for that surface. + * * @example * ```ts * vi.mock('@/lib/workflows/utils', () => workflowsUtilsMock) @@ -56,8 +57,6 @@ export const workflowsUtilsMock = { workflowHasResponseBlock: workflowsUtilsMockFns.mockWorkflowHasResponseBlock, createHttpResponseFromBlock: workflowsUtilsMockFns.mockCreateHttpResponseFromBlock, validateWorkflowPermissions: workflowsUtilsMockFns.mockValidateWorkflowPermissions, - authorizeWorkflowByWorkspacePermission: - workflowsUtilsMockFns.mockAuthorizeWorkflowByWorkspacePermission, createWorkflowRecord: workflowsUtilsMockFns.mockCreateWorkflowRecord, updateWorkflowRecord: workflowsUtilsMockFns.mockUpdateWorkflowRecord, deleteWorkflowRecord: workflowsUtilsMockFns.mockDeleteWorkflowRecord, diff --git a/packages/workflow-authz/package.json b/packages/workflow-authz/package.json new file mode 100644 index 00000000000..8bfd7b9ebe2 --- /dev/null +++ b/packages/workflow-authz/package.json @@ -0,0 +1,33 @@ +{ + "name": "@sim/workflow-authz", + "version": "0.1.0", + "private": true, + "sideEffects": false, + "type": "module", + "license": "Apache-2.0", + "engines": { + "bun": ">=1.2.13", + "node": ">=20.0.0" + }, + "exports": { + ".": { + "types": "./src/index.ts", + "default": "./src/index.ts" + } + }, + "scripts": { + "type-check": "tsc --noEmit", + "lint": "biome check --write --unsafe .", + "lint:check": "biome check .", + "format": "biome format --write .", + "format:check": "biome format ." + }, + "dependencies": { + "@sim/db": "workspace:*", + "drizzle-orm": "^0.45.2" + }, + "devDependencies": { + "@sim/tsconfig": "workspace:*", + "typescript": "^5.7.3" + } +} diff --git a/packages/workflow-authz/src/index.ts b/packages/workflow-authz/src/index.ts new file mode 100644 index 00000000000..534adc1b667 --- /dev/null +++ b/packages/workflow-authz/src/index.ts @@ -0,0 +1,143 @@ +import { db, permissions, type permissionTypeEnum, workflow, workspace } from '@sim/db' +import { and, eq, isNull } from 'drizzle-orm' + +export type ActiveWorkflowRecord = typeof workflow.$inferSelect + +export interface ActiveWorkflowContext { + workflow: ActiveWorkflowRecord + workspaceId: string +} + +export async function getActiveWorkflowContext( + workflowId: string +): Promise { + const rows = await db + .select({ + workflow, + workspaceId: workspace.id, + }) + .from(workflow) + .innerJoin(workspace, eq(workflow.workspaceId, workspace.id)) + .where( + and(eq(workflow.id, workflowId), isNull(workflow.archivedAt), isNull(workspace.archivedAt)) + ) + .limit(1) + + if (rows.length === 0) { + return null + } + + return { + workflow: rows[0].workflow, + workspaceId: rows[0].workspaceId, + } +} + +export async function getActiveWorkflowRecord( + workflowId: string +): Promise { + const context = await getActiveWorkflowContext(workflowId) + return context?.workflow ?? null +} + +export async function assertActiveWorkflowContext( + workflowId: string +): Promise { + const context = await getActiveWorkflowContext(workflowId) + if (!context) { + throw new Error(`Active workflow not found: ${workflowId}`) + } + return context +} + +export type PermissionType = (typeof permissionTypeEnum.enumValues)[number] + +type WorkflowRecord = typeof workflow.$inferSelect + +export interface WorkflowWorkspaceAuthorizationResult { + allowed: boolean + status: number + message?: string + workflow: WorkflowRecord | null + workspacePermission: PermissionType | null +} + +export async function authorizeWorkflowByWorkspacePermission(params: { + workflowId: string + userId: string + action?: 'read' | 'write' | 'admin' +}): Promise { + const { workflowId, userId, action = 'read' } = params + + const activeContext = await getActiveWorkflowContext(workflowId) + if (!activeContext) { + return { + allowed: false, + status: 404, + message: 'Workflow not found', + workflow: null, + workspacePermission: null, + } + } + + const wf = activeContext.workflow + + if (!wf.workspaceId) { + return { + allowed: false, + status: 403, + message: + 'This workflow is not attached to a workspace. Personal workflows are deprecated and cannot be accessed.', + workflow: wf, + workspacePermission: null, + } + } + + const [permissionRow] = await db + .select({ permissionType: permissions.permissionType }) + .from(permissions) + .where( + and( + eq(permissions.userId, userId), + eq(permissions.entityType, 'workspace'), + eq(permissions.entityId, wf.workspaceId) + ) + ) + .limit(1) + + const workspacePermission = (permissionRow?.permissionType as PermissionType | undefined) ?? null + + if (workspacePermission === null) { + return { + allowed: false, + status: 403, + message: `Unauthorized: Access denied to ${action} this workflow`, + workflow: wf, + workspacePermission, + } + } + + const permissionSatisfied = + action === 'read' + ? true + : action === 'write' + ? workspacePermission === 'write' || workspacePermission === 'admin' + : workspacePermission === 'admin' + + if (!permissionSatisfied) { + return { + allowed: false, + status: 403, + message: `Unauthorized: Access denied to ${action} this workflow`, + workflow: wf, + workspacePermission, + } + } + + return { + allowed: true, + status: 200, + workflow: wf, + workspacePermission, + } +} diff --git a/packages/workflow-authz/tsconfig.json b/packages/workflow-authz/tsconfig.json new file mode 100644 index 00000000000..1ffa3d2e844 --- /dev/null +++ b/packages/workflow-authz/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "@sim/tsconfig/library.json", + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/workflow-persistence/package.json b/packages/workflow-persistence/package.json new file mode 100644 index 00000000000..00a99f1dfff --- /dev/null +++ b/packages/workflow-persistence/package.json @@ -0,0 +1,57 @@ +{ + "name": "@sim/workflow-persistence", + "version": "0.1.0", + "private": true, + "sideEffects": false, + "type": "module", + "license": "Apache-2.0", + "engines": { + "bun": ">=1.2.13", + "node": ">=20.0.0" + }, + "exports": { + ".": { + "types": "./src/index.ts", + "default": "./src/index.ts" + }, + "./load": { + "types": "./src/load.ts", + "default": "./src/load.ts" + }, + "./save": { + "types": "./src/save.ts", + "default": "./src/save.ts" + }, + "./subblocks": { + "types": "./src/subblocks.ts", + "default": "./src/subblocks.ts" + }, + "./subflow-helpers": { + "types": "./src/subflow-helpers.ts", + "default": "./src/subflow-helpers.ts" + }, + "./types": { + "types": "./src/types.ts", + "default": "./src/types.ts" + } + }, + "scripts": { + "type-check": "tsc --noEmit", + "lint": "biome check --write --unsafe .", + "lint:check": "biome check .", + "format": "biome format --write .", + "format:check": "biome format ." + }, + "dependencies": { + "@sim/db": "workspace:*", + "@sim/logger": "workspace:*", + "@sim/utils": "workspace:*", + "@sim/workflow-types": "workspace:*", + "drizzle-orm": "^0.45.2", + "reactflow": "^11.11.4" + }, + "devDependencies": { + "@sim/tsconfig": "workspace:*", + "typescript": "^5.7.3" + } +} diff --git a/packages/workflow-persistence/src/index.ts b/packages/workflow-persistence/src/index.ts new file mode 100644 index 00000000000..ad0b35248c1 --- /dev/null +++ b/packages/workflow-persistence/src/index.ts @@ -0,0 +1,19 @@ +export { + loadWorkflowFromNormalizedTablesRaw, + persistMigratedBlocks, + type RawNormalizedWorkflow, +} from './load' +export { saveWorkflowToNormalizedTables } from './save' +export { + DEFAULT_SUBBLOCK_TYPE, + mergeSubBlockValues, + mergeSubblockStateWithValues, +} from './subblocks' +export { + convertLoopBlockToLoop, + convertParallelBlockToParallel, + findChildNodes, + generateLoopBlocks, + generateParallelBlocks, +} from './subflow-helpers' +export type { DbOrTx, NormalizedWorkflowData } from './types' diff --git a/packages/workflow-persistence/src/load.ts b/packages/workflow-persistence/src/load.ts new file mode 100644 index 00000000000..8f19375c811 --- /dev/null +++ b/packages/workflow-persistence/src/load.ts @@ -0,0 +1,182 @@ +import { db, workflow, workflowBlocks, workflowEdges, workflowSubflows } from '@sim/db' +import { createLogger } from '@sim/logger' +import type { BlockState, Loop, Parallel } from '@sim/workflow-types/workflow' +import { SUBFLOW_TYPES } from '@sim/workflow-types/workflow' +import { eq } from 'drizzle-orm' +import type { Edge } from 'reactflow' +import type { NormalizedWorkflowData } from './types' + +const logger = createLogger('WorkflowPersistenceLoad') + +export interface RawNormalizedWorkflow extends NormalizedWorkflowData { + workspaceId: string +} + +/** + * Load workflow state from normalized tables without running block migrations. + * Block migrations (credential rewrites, subblock ID migrations, canonical-mode + * backfill, tool sanitization) depend on the block/tool registry that lives in + * the Next app and should not be pulled into leaf services. Callers that want + * migrated state should wrap this with their own migration pipeline. + * + * Invariant: downstream migrations must not mutate `block.data.collection`, + * `block.data.whileCondition`, or `block.data.doWhileCondition`. Those fields + * are patched here from the subflow config on the pre-migration block, and + * callers re-sync only `loop.enabled`/`parallel.enabled` from the migrated + * block. If a future migration rewrites these data fields, the loop/parallel + * config on the returned object will silently diverge from the migrated block. + */ +export async function loadWorkflowFromNormalizedTablesRaw( + workflowId: string +): Promise { + try { + const [blocks, edges, subflows, [workflowRow]] = await Promise.all([ + db.select().from(workflowBlocks).where(eq(workflowBlocks.workflowId, workflowId)), + db.select().from(workflowEdges).where(eq(workflowEdges.workflowId, workflowId)), + db.select().from(workflowSubflows).where(eq(workflowSubflows.workflowId, workflowId)), + db + .select({ workspaceId: workflow.workspaceId }) + .from(workflow) + .where(eq(workflow.id, workflowId)) + .limit(1), + ]) + + if (blocks.length === 0) { + return null + } + + if (!workflowRow?.workspaceId) { + throw new Error(`Workflow ${workflowId} has no workspace`) + } + + const blocksMap: Record = {} + blocks.forEach((block) => { + const blockData = (block.data ?? {}) as BlockState['data'] + + const assembled: BlockState = { + id: block.id, + type: block.type, + name: block.name, + position: { + x: Number(block.positionX), + y: Number(block.positionY), + }, + enabled: block.enabled, + horizontalHandles: block.horizontalHandles, + advancedMode: block.advancedMode, + triggerMode: block.triggerMode, + height: Number(block.height), + subBlocks: (block.subBlocks as BlockState['subBlocks']) || {}, + outputs: (block.outputs as BlockState['outputs']) || {}, + data: blockData, + locked: block.locked, + } + + blocksMap[block.id] = assembled + }) + + const edgesArray: Edge[] = edges.map((edge) => ({ + id: edge.id, + source: edge.sourceBlockId, + target: edge.targetBlockId, + sourceHandle: edge.sourceHandle ?? undefined, + targetHandle: edge.targetHandle ?? undefined, + type: 'default', + data: {}, + })) + + const loops: Record = {} + const parallels: Record = {} + + subflows.forEach((subflow) => { + const config = (subflow.config ?? {}) as Partial + + if (subflow.type === SUBFLOW_TYPES.LOOP) { + const loopType = + (config as Loop).loopType === 'for' || + (config as Loop).loopType === 'forEach' || + (config as Loop).loopType === 'while' || + (config as Loop).loopType === 'doWhile' + ? (config as Loop).loopType + : 'for' + + const loop: Loop = { + id: subflow.id, + nodes: Array.isArray((config as Loop).nodes) ? (config as Loop).nodes : [], + iterations: + typeof (config as Loop).iterations === 'number' ? (config as Loop).iterations : 1, + loopType, + forEachItems: (config as Loop).forEachItems ?? '', + whileCondition: (config as Loop).whileCondition ?? '', + doWhileCondition: (config as Loop).doWhileCondition ?? '', + enabled: blocksMap[subflow.id]?.enabled ?? true, + } + loops[subflow.id] = loop + + if (blocksMap[subflow.id]) { + const block = blocksMap[subflow.id] + blocksMap[subflow.id] = { + ...block, + data: { + ...block.data, + collection: loop.forEachItems ?? block.data?.collection ?? '', + whileCondition: loop.whileCondition ?? block.data?.whileCondition ?? '', + doWhileCondition: loop.doWhileCondition ?? block.data?.doWhileCondition ?? '', + }, + } + } + } else if (subflow.type === SUBFLOW_TYPES.PARALLEL) { + const parallel: Parallel = { + id: subflow.id, + nodes: Array.isArray((config as Parallel).nodes) ? (config as Parallel).nodes : [], + count: typeof (config as Parallel).count === 'number' ? (config as Parallel).count : 5, + distribution: (config as Parallel).distribution ?? '', + parallelType: + (config as Parallel).parallelType === 'count' || + (config as Parallel).parallelType === 'collection' + ? (config as Parallel).parallelType + : 'count', + enabled: blocksMap[subflow.id]?.enabled ?? true, + } + parallels[subflow.id] = parallel + } else { + logger.warn(`Unknown subflow type: ${subflow.type} for subflow ${subflow.id}`) + } + }) + + return { + blocks: blocksMap, + edges: edgesArray, + loops, + parallels, + isFromNormalizedTables: true, + workspaceId: workflowRow.workspaceId, + } + } catch (error) { + logger.error(`Error loading workflow ${workflowId} from normalized tables:`, error) + return null + } +} + +export async function persistMigratedBlocks( + workflowId: string, + originalBlocks: Record, + migratedBlocks: Record +): Promise { + try { + for (const [blockId, block] of Object.entries(migratedBlocks)) { + if (block !== originalBlocks[blockId]) { + await db + .update(workflowBlocks) + .set({ + subBlocks: block.subBlocks, + data: block.data, + updatedAt: new Date(), + }) + .where(eq(workflowBlocks.id, blockId)) + } + } + } catch (err) { + logger.warn('Failed to persist block migrations', { workflowId, error: err }) + } +} diff --git a/packages/workflow-persistence/src/save.ts b/packages/workflow-persistence/src/save.ts new file mode 100644 index 00000000000..fefe1d354d6 --- /dev/null +++ b/packages/workflow-persistence/src/save.ts @@ -0,0 +1,108 @@ +import { db, workflowBlocks, workflowEdges, workflowSubflows } from '@sim/db' +import { createLogger } from '@sim/logger' +import { toError } from '@sim/utils/errors' +import type { BlockState, WorkflowState } from '@sim/workflow-types/workflow' +import { SUBFLOW_TYPES } from '@sim/workflow-types/workflow' +import type { InferInsertModel } from 'drizzle-orm' +import { eq } from 'drizzle-orm' +import { generateLoopBlocks, generateParallelBlocks } from './subflow-helpers' +import type { DbOrTx } from './types' + +const logger = createLogger('WorkflowPersistenceSave') + +type SubflowInsert = InferInsertModel + +export async function saveWorkflowToNormalizedTables( + workflowId: string, + state: WorkflowState, + externalTx?: DbOrTx +): Promise<{ success: boolean; error?: string }> { + const blockRecords = state.blocks as Record + const canonicalLoops = generateLoopBlocks(blockRecords) + const canonicalParallels = generateParallelBlocks(blockRecords) + + const execute = async (tx: DbOrTx) => { + await Promise.all([ + tx.delete(workflowBlocks).where(eq(workflowBlocks.workflowId, workflowId)), + tx.delete(workflowEdges).where(eq(workflowEdges.workflowId, workflowId)), + tx.delete(workflowSubflows).where(eq(workflowSubflows.workflowId, workflowId)), + ]) + + if (Object.keys(state.blocks).length > 0) { + const blockInserts = Object.values(state.blocks).map((block) => ({ + id: block.id, + workflowId, + type: block.type, + name: block.name || '', + positionX: String(block.position?.x || 0), + positionY: String(block.position?.y || 0), + enabled: block.enabled ?? true, + horizontalHandles: block.horizontalHandles ?? true, + advancedMode: block.advancedMode ?? false, + triggerMode: block.triggerMode ?? false, + height: String(block.height || 0), + subBlocks: block.subBlocks || {}, + outputs: block.outputs || {}, + data: block.data || {}, + parentId: block.data?.parentId || null, + extent: block.data?.extent || null, + locked: block.locked ?? false, + })) + + await tx.insert(workflowBlocks).values(blockInserts) + } + + if (state.edges.length > 0) { + const edgeInserts = state.edges.map((edge) => ({ + id: edge.id, + workflowId, + sourceBlockId: edge.source, + targetBlockId: edge.target, + sourceHandle: edge.sourceHandle || null, + targetHandle: edge.targetHandle || null, + })) + + await tx.insert(workflowEdges).values(edgeInserts) + } + + const subflowInserts: SubflowInsert[] = [] + + Object.values(canonicalLoops).forEach((loop) => { + subflowInserts.push({ + id: loop.id, + workflowId, + type: SUBFLOW_TYPES.LOOP, + config: loop, + }) + }) + + Object.values(canonicalParallels).forEach((parallel) => { + subflowInserts.push({ + id: parallel.id, + workflowId, + type: SUBFLOW_TYPES.PARALLEL, + config: parallel, + }) + }) + + if (subflowInserts.length > 0) { + await tx.insert(workflowSubflows).values(subflowInserts) + } + } + + if (externalTx) { + await execute(externalTx) + return { success: true } + } + + try { + await db.transaction(execute) + return { success: true } + } catch (error) { + logger.error(`Error saving workflow ${workflowId} to normalized tables:`, error) + return { + success: false, + error: toError(error).message, + } + } +} diff --git a/apps/sim/lib/workflows/subblocks.ts b/packages/workflow-persistence/src/subblocks.ts similarity index 96% rename from apps/sim/lib/workflows/subblocks.ts rename to packages/workflow-persistence/src/subblocks.ts index 6f5bdb92222..549fe7c303c 100644 --- a/apps/sim/lib/workflows/subblocks.ts +++ b/packages/workflow-persistence/src/subblocks.ts @@ -1,4 +1,4 @@ -import type { BlockState, SubBlockState } from '@/stores/workflows/workflow/types' +import type { BlockState, SubBlockState } from '@sim/workflow-types/workflow' export const DEFAULT_SUBBLOCK_TYPE = 'short-input' diff --git a/packages/workflow-persistence/src/subflow-helpers.ts b/packages/workflow-persistence/src/subflow-helpers.ts new file mode 100644 index 00000000000..b0f552f1977 --- /dev/null +++ b/packages/workflow-persistence/src/subflow-helpers.ts @@ -0,0 +1,94 @@ +import type { BlockState, Loop, Parallel } from '@sim/workflow-types/workflow' + +const DEFAULT_LOOP_ITERATIONS = 5 + +export function findChildNodes(containerId: string, blocks: Record): string[] { + return Object.values(blocks) + .filter((block) => block.data?.parentId === containerId) + .map((block) => block.id) +} + +export function convertLoopBlockToLoop( + loopBlockId: string, + blocks: Record +): Loop | undefined { + const loopBlock = blocks[loopBlockId] + if (!loopBlock || loopBlock.type !== 'loop') return undefined + + const loopType = loopBlock.data?.loopType || 'for' + + const loop: Loop = { + id: loopBlockId, + nodes: findChildNodes(loopBlockId, blocks), + iterations: loopBlock.data?.count || DEFAULT_LOOP_ITERATIONS, + loopType, + enabled: loopBlock.enabled, + } + + loop.forEachItems = loopBlock.data?.collection || '' + loop.whileCondition = loopBlock.data?.whileCondition || '' + loop.doWhileCondition = loopBlock.data?.doWhileCondition || '' + + return loop +} + +export function convertParallelBlockToParallel( + parallelBlockId: string, + blocks: Record +): Parallel | undefined { + const parallelBlock = blocks[parallelBlockId] + if (!parallelBlock || parallelBlock.type !== 'parallel') return undefined + + const parallelType = parallelBlock.data?.parallelType || 'count' + + const validParallelTypes = ['collection', 'count'] as const + const validatedParallelType = validParallelTypes.includes(parallelType as any) + ? parallelType + : 'collection' + + const distribution = + validatedParallelType === 'collection' ? parallelBlock.data?.collection || '' : undefined + + const count = parallelBlock.data?.count || 5 + + return { + id: parallelBlockId, + nodes: findChildNodes(parallelBlockId, blocks), + distribution, + count, + parallelType: validatedParallelType, + enabled: parallelBlock.enabled, + } +} + +export function generateLoopBlocks(blocks: Record): Record { + const loops: Record = {} + + Object.entries(blocks) + .filter(([_, block]) => block.type === 'loop') + .forEach(([id, block]) => { + const loop = convertLoopBlockToLoop(id, blocks) + if (loop) { + loops[id] = loop + } + }) + + return loops +} + +export function generateParallelBlocks( + blocks: Record +): Record { + const parallels: Record = {} + + Object.entries(blocks) + .filter(([_, block]) => block.type === 'parallel') + .forEach(([id, block]) => { + const parallel = convertParallelBlockToParallel(id, blocks) + if (parallel) { + parallels[id] = parallel + } + }) + + return parallels +} diff --git a/packages/workflow-persistence/src/types.ts b/packages/workflow-persistence/src/types.ts new file mode 100644 index 00000000000..4cbc4728582 --- /dev/null +++ b/packages/workflow-persistence/src/types.ts @@ -0,0 +1,23 @@ +import type { db } from '@sim/db' +import type * as schema from '@sim/db/schema' +import type { BlockState, Loop, Parallel } from '@sim/workflow-types/workflow' +import type { ExtractTablesWithRelations } from 'drizzle-orm' +import type { PgTransaction } from 'drizzle-orm/pg-core' +import type { PostgresJsQueryResultHKT } from 'drizzle-orm/postgres-js' +import type { Edge } from 'reactflow' + +export type DbOrTx = + | typeof db + | PgTransaction< + PostgresJsQueryResultHKT, + typeof schema, + ExtractTablesWithRelations + > + +export interface NormalizedWorkflowData { + blocks: Record + edges: Edge[] + loops: Record + parallels: Record + isFromNormalizedTables: boolean +} diff --git a/packages/workflow-persistence/tsconfig.json b/packages/workflow-persistence/tsconfig.json new file mode 100644 index 00000000000..1ffa3d2e844 --- /dev/null +++ b/packages/workflow-persistence/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "@sim/tsconfig/library.json", + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/workflow-types/package.json b/packages/workflow-types/package.json new file mode 100644 index 00000000000..cc041b0caaf --- /dev/null +++ b/packages/workflow-types/package.json @@ -0,0 +1,37 @@ +{ + "name": "@sim/workflow-types", + "version": "0.1.0", + "private": true, + "sideEffects": false, + "type": "module", + "license": "Apache-2.0", + "engines": { + "bun": ">=1.2.13", + "node": ">=20.0.0" + }, + "exports": { + "./blocks": { + "types": "./src/blocks.ts", + "default": "./src/blocks.ts" + }, + "./workflow": { + "types": "./src/workflow.ts", + "default": "./src/workflow.ts" + } + }, + "scripts": { + "type-check": "tsc --noEmit", + "lint": "biome check --write --unsafe .", + "lint:check": "biome check .", + "format": "biome format --write .", + "format:check": "biome format ." + }, + "peerDependencies": { + "reactflow": "^11.11.4" + }, + "devDependencies": { + "@sim/tsconfig": "workspace:*", + "reactflow": "^11.11.4", + "typescript": "^5.7.3" + } +} diff --git a/packages/workflow-types/src/blocks.ts b/packages/workflow-types/src/blocks.ts new file mode 100644 index 00000000000..4374dbfd587 --- /dev/null +++ b/packages/workflow-types/src/blocks.ts @@ -0,0 +1,88 @@ +export type PrimitiveValueType = + | 'string' + | 'number' + | 'boolean' + | 'json' + | 'array' + | 'file' + | 'file[]' + | 'any' + +export type SubBlockType = + | 'short-input' + | 'long-input' + | 'dropdown' + | 'combobox' + | 'slider' + | 'table' + | 'code' + | 'switch' + | 'tool-input' + | 'skill-input' + | 'checkbox-list' + | 'grouped-checkbox-list' + | 'condition-input' + | 'eval-input' + | 'time-input' + | 'oauth-input' + | 'webhook-config' + | 'schedule-info' + | 'file-selector' + | 'sheet-selector' + | 'project-selector' + | 'channel-selector' + | 'user-selector' + | 'folder-selector' + | 'knowledge-base-selector' + | 'knowledge-tag-filters' + | 'document-selector' + | 'document-tag-entry' + | 'mcp-server-selector' + | 'mcp-tool-selector' + | 'mcp-dynamic-args' + | 'input-format' + | 'response-format' + | 'filter-builder' + | 'sort-builder' + | 'file-upload' + | 'input-mapping' + | 'variables-input' + | 'messages-input' + | 'workflow-selector' + | 'workflow-input-mapper' + | 'text' + | 'router-input' + | 'table-selector' + | 'modal' + +export interface OutputCondition { + field: string + value: string | number | boolean | Array + not?: boolean + and?: { + field: string + value: + | string + | number + | boolean + | Array + | undefined + | null + not?: boolean + } +} + +export type OutputFieldDefinition = + | PrimitiveValueType + | { + type: PrimitiveValueType + description?: string + condition?: OutputCondition + hiddenFromDisplay?: boolean + } + +export function isHiddenFromDisplay(def: unknown): boolean { + return Boolean( + def && typeof def === 'object' && 'hiddenFromDisplay' in def && def.hiddenFromDisplay + ) +} diff --git a/packages/workflow-types/src/workflow.ts b/packages/workflow-types/src/workflow.ts new file mode 100644 index 00000000000..fb3c35e51b5 --- /dev/null +++ b/packages/workflow-types/src/workflow.ts @@ -0,0 +1,165 @@ +import type { Edge } from 'reactflow' +import type { OutputFieldDefinition, SubBlockType } from './blocks' + +export const SUBFLOW_TYPES = { + LOOP: 'loop', + PARALLEL: 'parallel', +} as const + +export type SubflowType = (typeof SUBFLOW_TYPES)[keyof typeof SUBFLOW_TYPES] + +export function isValidSubflowType(type: string): type is SubflowType { + return Object.values(SUBFLOW_TYPES).includes(type as SubflowType) +} + +export interface LoopConfig { + nodes: string[] + iterations: number + loopType: 'for' | 'forEach' | 'while' | 'doWhile' + forEachItems?: unknown[] | Record | string + whileCondition?: string + doWhileCondition?: string +} + +export interface ParallelConfig { + nodes: string[] + distribution?: unknown[] | Record | string + parallelType?: 'count' | 'collection' +} + +export interface Subflow { + id: string + workflowId: string + type: SubflowType + config: LoopConfig | ParallelConfig + createdAt: Date + updatedAt: Date +} + +export interface Position { + x: number + y: number +} + +export interface BlockData { + parentId?: string + extent?: 'parent' + width?: number + height?: number + collection?: any + count?: number + loopType?: 'for' | 'forEach' | 'while' | 'doWhile' + whileCondition?: string + doWhileCondition?: string + parallelType?: 'collection' | 'count' + type?: string + canonicalModes?: Record +} + +export interface BlockLayoutState { + measuredWidth?: number + measuredHeight?: number +} + +export interface BlockState { + id: string + type: string + name: string + position: Position + subBlocks: Record + outputs: Record + enabled: boolean + horizontalHandles?: boolean + height?: number + advancedMode?: boolean + triggerMode?: boolean + data?: BlockData + layout?: BlockLayoutState + locked?: boolean +} + +export interface SubBlockState { + id: string + type: SubBlockType + value: string | number | string[][] | null +} + +export interface LoopBlock { + id: string + loopType: 'for' | 'forEach' + count: number + collection: string + width: number + height: number + executionState: { + isExecuting: boolean + startTime: null | number + endTime: null | number + } +} + +export interface ParallelBlock { + id: string + collection: string + width: number + height: number + executionState: { + currentExecution: number + isExecuting: boolean + startTime: null | number + endTime: null | number + } +} + +export interface Loop { + id: string + nodes: string[] + iterations: number + loopType: 'for' | 'forEach' | 'while' | 'doWhile' + forEachItems?: any[] | Record | string + whileCondition?: string + doWhileCondition?: string + enabled: boolean + locked?: boolean +} + +export interface Parallel { + id: string + nodes: string[] + distribution?: any[] | Record | string + count?: number + parallelType?: 'count' | 'collection' + enabled: boolean + locked?: boolean +} + +export interface Variable { + id: string + name: string + type: 'string' | 'number' | 'boolean' | 'object' | 'array' | 'plain' + value: unknown +} + +export interface DragStartPosition { + id: string + x: number + y: number + parentId?: string | null +} + +export interface WorkflowState { + currentWorkflowId?: string | null + blocks: Record + edges: Edge[] + lastSaved?: number + loops: Record + parallels: Record + lastUpdate?: number + metadata?: { + name?: string + description?: string + exportedAt?: string + } + variables?: Record + dragStartPosition?: DragStartPosition | null +} diff --git a/packages/workflow-types/tsconfig.json b/packages/workflow-types/tsconfig.json new file mode 100644 index 00000000000..1ffa3d2e844 --- /dev/null +++ b/packages/workflow-types/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "@sim/tsconfig/library.json", + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/scripts/check-monorepo-boundaries.ts b/scripts/check-monorepo-boundaries.ts new file mode 100644 index 00000000000..c3623ee6fdc --- /dev/null +++ b/scripts/check-monorepo-boundaries.ts @@ -0,0 +1,77 @@ +#!/usr/bin/env bun +import { readdir, readFile } from 'node:fs/promises' +import path from 'node:path' + +const ROOT = path.resolve(import.meta.dir, '..') +const PACKAGES_DIR = path.join(ROOT, 'packages') + +const FORBIDDEN_PATTERNS: Array<{ pattern: RegExp; description: string }> = [ + { pattern: /from\s+['"]@\/(?!\*)/g, description: "'@/' path alias (apps/sim-only)" }, + { pattern: /from\s+['"]\.\.\/\.\.\/apps\//g, description: 'relative import into apps/' }, + { pattern: /from\s+['"]apps\//g, description: "bare 'apps/' import" }, +] + +const SKIP_DIRS = new Set(['node_modules', 'dist', '.next', '.turbo', 'coverage']) + +async function walk(dir: string, results: string[] = []): Promise { + const entries = await readdir(dir, { withFileTypes: true }) + for (const entry of entries) { + if (SKIP_DIRS.has(entry.name)) continue + const full = path.join(dir, entry.name) + if (entry.isDirectory()) { + await walk(full, results) + } else if (/\.(ts|tsx|mts|cts|js|jsx|mjs|cjs)$/.test(entry.name)) { + results.push(full) + } + } + return results +} + +async function main() { + const packagesEntries = await readdir(PACKAGES_DIR, { withFileTypes: true }) + const packageDirs = packagesEntries + .filter((entry) => entry.isDirectory()) + .map((entry) => path.join(PACKAGES_DIR, entry.name)) + + const offenders: Array<{ file: string; line: number; description: string; snippet: string }> = [] + + for (const dir of packageDirs) { + const files = await walk(dir) + for (const file of files) { + const content = await readFile(file, 'utf8') + const lines = content.split('\n') + for (let i = 0; i < lines.length; i++) { + const line = lines[i] + for (const { pattern, description } of FORBIDDEN_PATTERNS) { + pattern.lastIndex = 0 + if (pattern.test(line)) { + offenders.push({ + file: path.relative(ROOT, file), + line: i + 1, + description, + snippet: line.trim(), + }) + } + } + } + } + } + + if (offenders.length === 0) { + console.log('✅ Monorepo boundaries OK: no package imports from apps/*') + return + } + + console.error('❌ Monorepo boundary violations found:') + for (const offender of offenders) { + console.error( + ` ${offender.file}:${offender.line} — ${offender.description}\n ${offender.snippet}` + ) + } + process.exit(1) +} + +void main().catch((error) => { + console.error('Monorepo boundary check failed:', error) + process.exit(1) +}) diff --git a/scripts/check-realtime-prune-graph.ts b/scripts/check-realtime-prune-graph.ts new file mode 100644 index 00000000000..c12fc9b7642 --- /dev/null +++ b/scripts/check-realtime-prune-graph.ts @@ -0,0 +1,67 @@ +#!/usr/bin/env bun +import { mkdtemp, readdir, rm, stat } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import path from 'node:path' +import { $ } from 'bun' + +const MAX_PRUNED_PACKAGE_COUNT = 25 + +async function listPackages(root: string): Promise { + try { + const entries = await readdir(root) + const result: string[] = [] + for (const entry of entries) { + const full = path.join(root, entry) + const s = await stat(full) + if (s.isDirectory()) { + result.push(entry) + } + } + return result + } catch { + return [] + } +} + +async function main() { + const scratch = await mkdtemp(path.join(tmpdir(), 'sim-realtime-prune-')) + try { + console.log(`Pruning @sim/realtime into ${scratch}`) + await $`bunx turbo prune @sim/realtime --docker --out-dir=${scratch}`.quiet() + + const apps = await listPackages(path.join(scratch, 'json', 'apps')) + const packages = await listPackages(path.join(scratch, 'json', 'packages')) + const total = apps.length + packages.length + + console.log(`Pruned apps (${apps.length}): ${apps.join(', ') || '(none)'}`) + console.log(`Pruned packages (${packages.length}): ${packages.join(', ') || '(none)'}`) + console.log(`Total pruned workspaces: ${total}`) + + if (total > MAX_PRUNED_PACKAGE_COUNT) { + console.error( + `\n❌ Pruned realtime dep graph has ${total} workspaces (limit: ${MAX_PRUNED_PACKAGE_COUNT}).` + ) + console.error( + 'A new package was pulled into @sim/realtime. Ensure only pure, single-purpose packages are in its dep graph.' + ) + process.exit(1) + } + + const unexpectedApps = apps.filter((name) => name !== 'realtime') + if (unexpectedApps.length > 0) { + console.error( + `\n❌ Pruned realtime tree pulled in unexpected apps/: ${unexpectedApps.join(', ')}` + ) + process.exit(1) + } + + console.log('\n✅ Realtime prune size within expected bounds') + } finally { + await rm(scratch, { recursive: true, force: true }) + } +} + +void main().catch((error) => { + console.error('Realtime prune check failed:', error) + process.exit(1) +})