diff --git a/.claude/rules/sim-testing.md b/.claude/rules/sim-testing.md index 4e28169449..850d0fc764 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 7e3e480664..c5e6803e77 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/.github/workflows/test-build.yml b/.github/workflows/test-build.yml index c08ab495a2..26764604b4 100644 --- a/.github/workflows/test-build.yml +++ b/.github/workflows/test-build.yml @@ -103,6 +103,12 @@ jobs: - name: Lint code run: bun run lint:check + - name: Enforce monorepo boundaries + run: bun run scripts/check-monorepo-boundaries.ts + + - name: Verify realtime prune graph + run: bun run scripts/check-realtime-prune-graph.ts + - name: Run tests with coverage env: NODE_OPTIONS: '--no-warnings --max-old-space-size=8192' diff --git a/AGENTS.md b/AGENTS.md index 5e21c7e009..bb078a5163 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-image-size.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/package.json b/apps/realtime/package.json new file mode 100644 index 0000000000..ef737cf0f4 --- /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", + "socket.io-client": "4.8.1", + "zod": "^3.24.2" + }, + "devDependencies": { + "@sim/testing": "workspace:*", + "@sim/tsconfig": "workspace:*", + "@types/node": "24.2.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 0000000000..40491a62af --- /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 366558536b..3e6c50ecbe 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 93c117ea1d..9e2b4d1ddb 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 0000000000..7794ef4115 --- /dev/null +++ b/apps/realtime/src/env.ts @@ -0,0 +1,52 @@ +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(), + INTERNAL_API_BASE_URL: z.string().url().optional(), + ALLOWED_ORIGINS: z.string().optional(), + SOCKET_SERVER_URL: z.string().url().optional(), + PORT: z.coerce.number().int().positive().default(3002), + SOCKET_PORT: z.coerce.number().int().positive().optional(), + HOSTNAME: z.string().default('0.0.0.0'), + 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 +} + +export function getInternalApiBaseUrl(): string { + return env.INTERNAL_API_BASE_URL ?? 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 ee7a9a7743..90eddb8246 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 0000000000..6ded2e5474 --- /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 c47c380e89..6635d157b3 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 208183d2c5..13aadc22f3 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 997f8416c7..e71792ca68 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 bf5114c839..b67570674a 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 ac65399faf..9dd82db87c 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 8796f2a319..da977fcdb5 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 e50f4e97c2..21d0decc77 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,29 @@ 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, + HOSTNAME: '0.0.0.0', + DISABLE_AUTH: false, + }, + isProd: false, + isDev: false, + isTest: true, + isHosted: false, + isAuthDisabled: false, + getBaseUrl: () => 'http://localhost:3000', + getInternalApiBaseUrl: () => '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 +66,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 +76,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 +296,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 +353,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 +379,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 +414,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 +431,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 94% rename from apps/sim/socket/index.ts rename to apps/realtime/src/index.ts index adca5a9c37..aae6dfed3f 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') 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 a9daddcfc3..91cdb71b4b 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 784d4ea7ff..2d8cd12999 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 244cf1b07b..dcc893b147 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 0000000000..8067fc1b21 --- /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 1e14c9c7df..a032e785bb 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 adfe672048..0e6b3eadf2 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 5c555e9284..0f8ed73cc5 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 0000000000..cce62771d4 --- /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 0000000000..a458a21d8d --- /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 0000000000..132ea305c2 --- /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 078de88233..ac75315fff 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 ef20b11b29..2bf7be8782 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 185caf3e8b..db2db24fc7 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 13a3b4bc7e..3681ac5cc3 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 7a5372e652..51767bd482 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/billing/credits/route.ts b/apps/sim/app/api/billing/credits/route.ts index 5afe089ab2..7f7e539022 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 81c04439e3..32f3a6a831 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 3564aa00a7..4f937d7525 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.ts b/apps/sim/app/api/chat/utils.ts index 8d1147c1c8..3909dd599f 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 8252c49341..bc475a8bc0 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/chats/route.ts b/apps/sim/app/api/copilot/chats/route.ts index 512cd129bd..07b6974ed4 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 05520d5984..0730fe748f 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 01661bf4e3..b5c050d13d 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 e73b6ed0ca..e3da1f258f 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 b80fad00a0..0e00dbf1c3 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 de1a54ae18..287a78b0d2 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 a522cfcf41..d0f4c0ca00 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 ca3c189490..64d7228125 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 47e9c44d2d..8a7fcb5146 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 8c248db611..2d8e1b77a6 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 e198790b62..1e3846bd0d 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 55c06f8686..cc5ba88799 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 6b265e4636..a85a32a72c 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 465ae5340e..ebb429b438 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 9c0aca941e..7d74c42126 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/upload/route.ts b/apps/sim/app/api/files/upload/route.ts index 9ea1c53d87..705ea2d8b1 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 7cdc446b8d..9b7811a822 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 04f697e74a..477ada12fc 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 d3a3a173fa..e5040507b9 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 69e8c42921..14117e2b17 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 a8df0decc9..501f3edbf1 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 7b1ded808b..5336d32450 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 9f4bafd05a..55bbe65e17 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 157ed3f690..efd47375f0 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 928bcf002b..f3be2b8e1b 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 bc4be85a7b..7f9c311b4c 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 28d0dc1193..1841f93118 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 b6b15249ba..8e08cfc89d 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 3caf199209..dac212544a 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 3275d56ae8..927c33e24d 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 5034584ee5..57593fc973 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 210826cfe7..395da6b281 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 bfc04f5080..9f78afd961 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 3bc7bb41b4..77ca9942fb 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 cb5145a1be..b61ae4c7f2 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 57ea35e616..63244e1751 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 e4e64724f2..6a6cb4c93b 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 a3bc917402..6935272ad6 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 2562d3ff8e..26e76cbb3f 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 bf70faecef..f49a23a83a 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 355f570d80..d66f2cdd40 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 5e67472d53..40ed102fba 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 ebcae6ab05..8dcc1385b6 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 d8b0e89e7a..ece42f9f5d 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 3dd0603c9f..ff58a3149e 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 18951456d7..6f97a2515c 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 64c3638e4f..bc0ab08d75 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 e11c7838d4..7f8b0c1309 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 e9efa57270..52c1fc47cc 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 2bb7739948..6c9db51ccc 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 917470cf99..6ae73c4126 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 bcffb6b823..13005bb643 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 4b9c8d93d4..bab33a9b9c 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 21c76cf46f..f90a962cc2 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 76fa504a88..be511aeb86 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 08b71262c6..611a1b80c5 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 43105b9297..f5c9c83855 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 cd508506fb..fb81ec48e0 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 68281a96b4..dec09e2e1f 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 16b2ecf553..971b1e57c7 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 b810226fd5..36a4d1d4b6 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 671be8c67b..70326038cc 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 4203f0c5e8..6eac1e6d64 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 fc54802db7..ef1dab40e7 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 c52185a027..a3a17208d8 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 13b8e041e9..c79e30c017 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 01932c9fe3..df3407461f 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 73155a1589..e8e3a486e6 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 7a0b1828db..8892934261 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 2deecfebdf..6b0b17a845 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 069d5dcbfb..6c91c9d1d7 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 e8139035cf..6e5ee48c0a 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 d810fcf2f5..55b1a25445 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 ffca21baf9..96bb31b15a 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 4414e92277..67e1a186e2 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 5ef4bf0429..9145c58552 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 e604fe5835..147cc9d21a 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 098f9fe302..b66fc85dc2 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 d1cd6f9253..3ac24168fa 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/workflows/[id]/deploy/route.ts b/apps/sim/app/api/v1/admin/workflows/[id]/deploy/route.ts index 86873b8e54..5b10a1c381 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 6416ee6243..77e5e246da 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 3b1f972d00..9e27097db9 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 5633eef91c..6fdfc3fc0c 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 13dea76f5a..ef909a7da1 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 3beb70d273..e16f8b6c88 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 19b0a80cb2..21c6baf4e2 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 4cf12b2db2..32107b050c 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 700ac9a8a8..e0fe7d7c13 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 3f64ec3f7e..a24ce39496 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 09ef58e5f3..bf20d38216 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 67bfd5dd8f..dad51353a5 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 bd56470865..43618f9310 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 b3b6437c71..44994ebfa5 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 f24a213b37..5c2a5cd51a 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 762f0a1c7f..e1121b1caf 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 6893a3192f..9b659744d2 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 fef312d7ac..188a5a4758 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 334f87ef72..5b11700b56 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 48e7ce5045..a1fdc2de0d 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 6fd35a7543..5b8debdc36 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 9df86fbf77..b6e1aeab7b 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 c7e514a082..6a9808f1b0 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 1e7496fe87..1c23e6c6a0 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 8cf17e17a4..c89246cdec 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 5787f6cd26..3fa8cd7615 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 00ce06ce3a..ebe71b1ba2 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 54b3db3a3c..e020591749 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 8201561adc..2d3ec73334 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 c9b68ca180..7f862131f8 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 ac76548f8b..d6c164da53 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 511d46c09c..be22692608 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 9a069c8ed7..62d90a7e8a 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 af74556415..2a66a616c7 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 f01c4af4e0..ed10d8dc49 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 aa211fa9eb..d8b902388e 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 65bf80f1c4..4677eb2e54 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 b940458179..f0539f5b3a 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 29af19b139..aa9728b7df 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 9d8f02ed89..f1dbc043b3 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 d0b1bbaf92..3d0f939073 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 affbf18294..4cabaec258 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 0897b99e19..606978a927 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 b932c53f26..525db3167c 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 b692a15a2f..1e9f63a61d 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 ca9bb9e18f..a006dd2e96 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 53a625e4af..201a34bf70 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/route.ts b/apps/sim/app/api/workspaces/[id]/notifications/route.ts index ee8753fbdd..25bd80bba8 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 66a60ec2ce..a0031101bf 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 41579e1c97..d7a0cdc59b 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 0450d123c4..bc4f4643b5 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 1181890bc1..1680b7c112 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 f228f8ef03..236bcb7187 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 e1fffb08a3..a17a78a2a5 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 38d3cdd63f..e15b423606 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 5f6c848257..a994d6daa4 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 067112b74b..43add66c44 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 a83f115e83..e1e874170a 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 41b394b8a7..a9a319688e 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 a0e1eab3ac..18b8e38e18 100644 --- a/apps/sim/background/workspace-notification-delivery.ts +++ b/apps/sim/background/workspace-notification-delivery.ts @@ -9,6 +9,7 @@ import { createLogger } from '@sim/logger' 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') diff --git a/apps/sim/blocks/types.ts b/apps/sim/blocks/types.ts index 79242ff003..5d02261dca 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 445265f4f8..b1bd15fada 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 bbf54868a2..e119cdf8e4 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 023b2f1d8f..2140de378a 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 8c35956563..99e527b18f 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/auth/auth.ts b/apps/sim/lib/auth/auth.ts index f770920fc1..3bc9286c6e 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 2f5fd1cb50..712235df5a 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 dbb7222380..9e0c4dcb3e 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 556bfdb0fb..ab36213b8c 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 c96287a6d2..9dd3e5d7c0 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/process-contents.ts b/apps/sim/lib/copilot/chat/process-contents.ts index f62109e920..c82217c90f 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 713e01eabb..4f2ec7de37 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/tools/handlers/access.ts b/apps/sim/lib/copilot/tools/handlers/access.ts index b989ea3439..a435511a2f 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 b57bf076f8..149b0084d8 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 785fc7bca3..6af4a09b63 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 cde7a54178..3953878c88 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 eb797a771f..6f70ac1fa2 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 7db12ce4b5..f7341038b9 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/workflow/mutations.ts b/apps/sim/lib/copilot/tools/handlers/workflow/mutations.ts index ef3a652c38..7e1d828183 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 974388e9ec..3c6367fb63 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 2258ec101d..3ab0cc2d57 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/security/encryption.ts b/apps/sim/lib/core/security/encryption.ts index 811a60ada4..97d93aaca1 100644 --- a/apps/sim/lib/core/security/encryption.ts +++ b/apps/sim/lib/core/security/encryption.ts @@ -1,4 +1,4 @@ -import { createCipheriv, createDecipheriv, createHmac, randomBytes, timingSafeEqual } from 'crypto' +import { createCipheriv, createDecipheriv, randomBytes } from 'crypto' import { createLogger } from '@sim/logger' import { env } from '@/lib/core/config/env' @@ -85,17 +85,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 a90f9368ff..a74950ec98 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 2aab6c5d6a..567dbede2a 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 35195972dd..2fea91bfe0 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/logs/events.ts b/apps/sim/lib/logs/events.ts index 02703ba7ce..5a40fab190 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/webhooks/processor.test.ts b/apps/sim/lib/webhooks/processor.test.ts index 27840dbaf0..03123a2bd3 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 a6c112ff8d..e07dd11e1e 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 { generateId } from '@sim/utils/id' -import { safeCompare } from '@/lib/core/security/encryption' import { getNotificationUrl, getProviderConfig } from '@/lib/webhooks/provider-subscription-utils' import type { DeleteSubscriptionContext, diff --git a/apps/sim/lib/webhooks/providers/attio.ts b/apps/sim/lib/webhooks/providers/attio.ts index 7ac8f87c7b..fee73a4e17 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 { 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 { diff --git a/apps/sim/lib/webhooks/providers/calcom.ts b/apps/sim/lib/webhooks/providers/calcom.ts index b018b16f58..a17805233e 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 type { WebhookProviderHandler } from '@/lib/webhooks/providers/types' import { createHmacVerifier } from '@/lib/webhooks/providers/utils' diff --git a/apps/sim/lib/webhooks/providers/circleback.ts b/apps/sim/lib/webhooks/providers/circleback.ts index 1beb8a6814..2dacac8fef 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 type { FormatInputContext, FormatInputResult, diff --git a/apps/sim/lib/webhooks/providers/fireflies.ts b/apps/sim/lib/webhooks/providers/fireflies.ts index 0897a7ff16..47fd07dd4b 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 type { FormatInputContext, FormatInputResult, diff --git a/apps/sim/lib/webhooks/providers/github.ts b/apps/sim/lib/webhooks/providers/github.ts index a0fd90f2e6..20d1a3fff3 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/greenhouse.ts b/apps/sim/lib/webhooks/providers/greenhouse.ts index 241e2221d1..654fec9194 100644 --- a/apps/sim/lib/webhooks/providers/greenhouse.ts +++ b/apps/sim/lib/webhooks/providers/greenhouse.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 type { EventMatchContext, FormatInputContext, diff --git a/apps/sim/lib/webhooks/providers/intercom.ts b/apps/sim/lib/webhooks/providers/intercom.ts index 5957a93b1f..18abf6c57b 100644 --- a/apps/sim/lib/webhooks/providers/intercom.ts +++ b/apps/sim/lib/webhooks/providers/intercom.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/jira.ts b/apps/sim/lib/webhooks/providers/jira.ts index 0b5a80f073..28851ed913 100644 --- a/apps/sim/lib/webhooks/providers/jira.ts +++ b/apps/sim/lib/webhooks/providers/jira.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 type { EventMatchContext, FormatInputContext, diff --git a/apps/sim/lib/webhooks/providers/linear.ts b/apps/sim/lib/webhooks/providers/linear.ts index 457d0992ab..4da52f3a2a 100644 --- a/apps/sim/lib/webhooks/providers/linear.ts +++ b/apps/sim/lib/webhooks/providers/linear.ts @@ -1,9 +1,9 @@ import crypto from 'crypto' import { createLogger } from '@sim/logger' +import { safeCompare } from '@sim/security/compare' import { toError } from '@sim/utils/errors' import { generateId } from '@sim/utils/id' 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/microsoft-teams.ts b/apps/sim/lib/webhooks/providers/microsoft-teams.ts index 68e588d6f5..de8995d477 100644 --- a/apps/sim/lib/webhooks/providers/microsoft-teams.ts +++ b/apps/sim/lib/webhooks/providers/microsoft-teams.ts @@ -2,10 +2,10 @@ import crypto from 'crypto' import { db } from '@sim/db' import { account } from '@sim/db/schema' import { createLogger } from '@sim/logger' +import { safeCompare } from '@sim/security/compare' import { toError } from '@sim/utils/errors' import { eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' -import { safeCompare } from '@/lib/core/security/encryption' import { isMicrosoftContentUrl } from '@/lib/core/security/input-validation' import { type SecureFetchResponse, diff --git a/apps/sim/lib/webhooks/providers/notion.ts b/apps/sim/lib/webhooks/providers/notion.ts index ed3f1b1c96..d0881552b2 100644 --- a/apps/sim/lib/webhooks/providers/notion.ts +++ b/apps/sim/lib/webhooks/providers/notion.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 { EventMatchContext, FormatInputContext, diff --git a/apps/sim/lib/webhooks/providers/resend.ts b/apps/sim/lib/webhooks/providers/resend.ts index 2238863525..3061af91f7 100644 --- a/apps/sim/lib/webhooks/providers/resend.ts +++ b/apps/sim/lib/webhooks/providers/resend.ts @@ -1,7 +1,7 @@ import crypto from 'node: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/slack.ts b/apps/sim/lib/webhooks/providers/slack.ts index a5488f0aa4..6e99b1cfb9 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 { toError } from '@sim/utils/errors' import { NextResponse } from 'next/server' -import { safeCompare } from '@/lib/core/security/encryption' import { secureFetchWithPinnedIP, validateUrlWithDNS, diff --git a/apps/sim/lib/webhooks/providers/twilio-voice.ts b/apps/sim/lib/webhooks/providers/twilio-voice.ts index be0417ef71..543264cd73 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 8c96d05907..06c602e3ba 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 { getNotificationUrl, getProviderConfig } from '@/lib/webhooks/provider-subscription-utils' import type { DeleteSubscriptionContext, diff --git a/apps/sim/lib/webhooks/providers/utils.ts b/apps/sim/lib/webhooks/providers/utils.ts index f2a5604708..dff3db7ce1 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 218afb3d6f..edf5f9d622 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 e1a76ef218..783c728326 100644 --- a/apps/sim/lib/webhooks/providers/whatsapp.ts +++ b/apps/sim/lib/webhooks/providers/whatsapp.ts @@ -2,9 +2,9 @@ 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 { 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, diff --git a/apps/sim/lib/webhooks/providers/zoom.ts b/apps/sim/lib/webhooks/providers/zoom.ts index 409d7c8f92..48df3d1465 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 { 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, diff --git a/apps/sim/lib/workflows/active-context.ts b/apps/sim/lib/workflows/active-context.ts deleted file mode 100644 index 612a00e4d3..0000000000 --- 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.ts b/apps/sim/lib/workflows/executor/execution-core.ts index d115b17486..13ff251057 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 e10323445d..d8c9325316 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 4f81701b55..2b23ae23dd 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 03264f2015..6d3f52fd49 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 3e757e6b2b..eaf0b2a656 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 e2aa903352..0bd948cdec 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 ff111d203a..5386455bb1 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 bbf939b78a..80af8e9dad 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) + + if (migrated) { + Promise.resolve().then(() => persistMigratedBlocks(workflowId, raw.blocks, finalBlocks)) + } + + const patchedLoops: Record = { ...raw.loops } + const patchedParallels: Record = { ...raw.parallels } + + for (const id of Object.keys(raw.loops)) { + if (finalBlocks[id]) { + patchedLoops[id] = { ...raw.loops[id], enabled: finalBlocks[id].enabled ?? true } } - - // 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, + } + for (const id of Object.keys(raw.parallels)) { + if (finalBlocks[id]) { + patchedParallels[id] = { + ...raw.parallels[id], + enabled: finalBlocks[id].enabled ?? true, } - - blocksMap[block.id] = assembled - }) - - if (!workflowRow?.workspaceId) { - throw new Error(`Workflow ${workflowId} has no workspace`) } + } - 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}`) - } - }) - - 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 f5fd238a23..5f630f39e6 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 5dfdc5c066..a6b971f932 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 1a84029605..1322e2302c 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 4afeed40e3..0000000000 --- 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 3cbdc41348..0000000000 --- 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 6776bf66e1..30ec1a3a23 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 8d5df192fd..672383ad47 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 861277e02e..1803970cd6 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 25b372693b..fbb97f22fc 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 37d86847bc..c209cfd0ee 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/vitest.setup.ts b/apps/sim/vitest.setup.ts index 588a053bb2..922e8b7a91 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 ac1e89e492..8422392f0d 100644 --- a/bun.lock +++ b/bun.lock @@ -3,9 +3,6 @@ "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 +49,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", + "socket.io-client": "4.8.1", + "zod": "^3.24.2", + }, + "devDependencies": { + "@sim/testing": "workspace:*", + "@sim/tsconfig": "workspace:*", + "@types/node": "24.2.1", + "typescript": "^5.7.3", + "vitest": "^3.0.8", + }, + }, "apps/sim": { "name": "sim", "version": "0.1.0", @@ -59,6 +86,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 +146,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 +242,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 +291,32 @@ "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/tsconfig": "workspace:*", + "typescript": "^5.7.3", + }, + }, + "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 +365,26 @@ "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", + }, + }, "packages/testing": { "name": "@sim/testing", "version": "0.1.0", @@ -346,6 +424,44 @@ "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/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", + "dependencies": { + "reactflow": "^11.11.4", + }, + "devDependencies": { + "@sim/tsconfig": "workspace:*", + "typescript": "^5.7.3", + }, + }, }, "trustedDependencies": [ "ffmpeg-static", @@ -1351,16 +1467,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 +5127,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 +6139,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/realtime.Dockerfile b/docker/realtime.Dockerfile index 4bd565c39e..36891f49b6 100644 --- a/docker/realtime.Dockerfile +++ b/docker/realtime.Dockerfile @@ -5,46 +5,29 @@ FROM oven/bun:1.3.11-alpine AS base RUN apk add --no-cache libc6-compat curl +# ======================================== +# Pruner Stage: Emit a minimal monorepo subset that @sim/realtime depends on +# ======================================== +FROM base AS pruner +WORKDIR /app + +RUN bun add -g turbo + +COPY . . + +RUN turbo prune @sim/realtime --docker + # ======================================== # Dependencies Stage: Install Dependencies # ======================================== 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 -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 +COPY --from=pruner /app/out/json/ ./ +COPY --from=pruner /app/out/bun.lock ./bun.lock -# 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 - -# ======================================== -# Builder Stage: Prepare source code -# ======================================== -FROM base AS builder -WORKDIR /app - -# Copy node_modules from deps stage (cached if dependencies don't change) -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/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 + bun install --linker=hoisted --omit=dev --ignore-scripts # ======================================== # Runner Stage: Run the Socket Server @@ -52,38 +35,19 @@ COPY packages ./packages FROM base AS runner WORKDIR /app -ENV NODE_ENV=production - -# 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 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 \ +ENV NODE_ENV=production \ + 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 +RUN addgroup -g 1001 -S nodejs && \ + adduser -S nextjs -u 1001 + +COPY --from=deps --chown=nextjs:nodejs /app ./ +COPY --from=pruner --chown=nextjs:nodejs /app/out/full/ ./ + +USER nextjs + +EXPOSE 3002 + +CMD ["bun", "apps/realtime/src/index.ts"] diff --git a/package.json b/package.json index 335e883193..281a1daab1 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": "bun --cwd apps/realtime run dev", + "dev:full": "bunx concurrently -n \"App,Realtime\" -c \"cyan,magenta\" \"bun --cwd apps/sim run dev\" \"bun --cwd apps/realtime run dev\"", "test": "turbo run test", "format": "turbo run format", "format:check": "turbo run format:check", @@ -65,8 +65,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 0000000000..79317dd40e --- /dev/null +++ b/packages/audit/package.json @@ -0,0 +1,35 @@ +{ + "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 ." + }, + "dependencies": { + "@sim/db": "workspace:*", + "@sim/logger": "workspace:*", + "@sim/utils": "workspace:*", + "drizzle-orm": "^0.45.2" + }, + "devDependencies": { + "@sim/tsconfig": "workspace:*", + "typescript": "^5.7.3" + } +} diff --git a/packages/audit/src/index.ts b/packages/audit/src/index.ts new file mode 100644 index 0000000000..f3ec73f8c5 --- /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 96% rename from apps/sim/lib/audit/log.test.ts rename to packages/audit/src/log.test.ts index 1c75586746..fefac1a12f 100644 --- a/apps/sim/lib/audit/log.test.ts +++ b/packages/audit/src/log.test.ts @@ -21,7 +21,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 +356,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 +369,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 3176fcbee9..4ee040ba43 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 0000000000..7b64383489 --- /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/auth/package.json b/packages/auth/package.json new file mode 100644 index 0000000000..b44406abdf --- /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 0000000000..38fe06df32 --- /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 0000000000..1ffa3d2e84 --- /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/realtime-protocol/package.json b/packages/realtime-protocol/package.json new file mode 100644 index 0000000000..63590ce1b8 --- /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 fc48500ab6..19d76437ab 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 0000000000..1ffa3d2e84 --- /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 0000000000..d5a1dee553 --- /dev/null +++ b/packages/security/package.json @@ -0,0 +1,31 @@ +{ + "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" + } + }, + "scripts": { + "type-check": "tsc --noEmit", + "lint": "biome check --write --unsafe .", + "lint:check": "biome check .", + "format": "biome format --write .", + "format:check": "biome format ." + }, + "dependencies": {}, + "devDependencies": { + "@sim/tsconfig": "workspace:*", + "@types/node": "24.2.1", + "typescript": "^5.7.3" + } +} diff --git a/packages/security/src/compare.ts b/packages/security/src/compare.ts new file mode 100644 index 0000000000..3d68040e14 --- /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/tsconfig.json b/packages/security/tsconfig.json new file mode 100644 index 0000000000..1ffa3d2e84 --- /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/testing/src/mocks/audit.mock.ts b/packages/testing/src/mocks/audit.mock.ts index 4dadec2281..58671d6ba4 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 c3ab30078c..46c55c2e0a 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 0000000000..dea86a272d --- /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 0930d9b5dc..1c70412f01 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 0000000000..8bfd7b9ebe --- /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 0000000000..534adc1b66 --- /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 0000000000..1ffa3d2e84 --- /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 0000000000..10cc305db3 --- /dev/null +++ b/packages/workflow-persistence/package.json @@ -0,0 +1,52 @@ +{ + "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": { + "./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/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/load.ts b/packages/workflow-persistence/src/load.ts new file mode 100644 index 0000000000..7142f9ae7c --- /dev/null +++ b/packages/workflow-persistence/src/load.ts @@ -0,0 +1,175 @@ +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. + */ +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 0000000000..4b0eedcf0c --- /dev/null +++ b/packages/workflow-persistence/src/save.ts @@ -0,0 +1,107 @@ +import { db, workflowBlocks, workflowEdges, workflowSubflows } from '@sim/db' +import { createLogger } from '@sim/logger' +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: error instanceof Error ? error.message : 'Unknown error', + } + } +} diff --git a/apps/sim/lib/workflows/subblocks.ts b/packages/workflow-persistence/src/subblocks.ts similarity index 64% rename from apps/sim/lib/workflows/subblocks.ts rename to packages/workflow-persistence/src/subblocks.ts index 6f5bdb9222..66841cd35a 100644 --- a/apps/sim/lib/workflows/subblocks.ts +++ b/packages/workflow-persistence/src/subblocks.ts @@ -1,14 +1,7 @@ -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' -/** - * Merges subblock values into the provided subblock structures. - * Falls back to a default subblock shape when a value has no structure. - * @param subBlocks - Existing subblock definitions from the workflow - * @param values - Stored subblock values keyed by subblock id - * @returns Merged subblock structures with updated values - */ export function mergeSubBlockValues( subBlocks: Record | undefined, values: Record | undefined @@ -36,14 +29,6 @@ export function mergeSubBlockValues( return merged } -/** - * Merges workflow block states with explicit subblock values while maintaining block structure. - * Values that are null or undefined do not override existing subblock values. - * @param blocks - Block configurations from workflow state - * @param subBlockValues - Subblock values keyed by blockId -> subBlockId -> value - * @param blockId - Optional specific block ID to merge (merges all if not provided) - * @returns Merged block states with updated subblocks - */ export function mergeSubblockStateWithValues( blocks: Record, subBlockValues: Record> = {}, diff --git a/packages/workflow-persistence/src/subflow-helpers.ts b/packages/workflow-persistence/src/subflow-helpers.ts new file mode 100644 index 0000000000..b0f552f197 --- /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 0000000000..4cbc472858 --- /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 0000000000..1ffa3d2e84 --- /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 0000000000..dd8f6832c2 --- /dev/null +++ b/packages/workflow-types/package.json @@ -0,0 +1,36 @@ +{ + "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 ." + }, + "dependencies": { + "reactflow": "^11.11.4" + }, + "devDependencies": { + "@sim/tsconfig": "workspace:*", + "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 0000000000..4374dbfd58 --- /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 0000000000..fb3c35e51b --- /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 0000000000..1ffa3d2e84 --- /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 0000000000..c3623ee6fd --- /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 0000000000..c12fc9b764 --- /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) +})