diff --git a/apps/docs/content/docs/platform/connected-accounts.mdx b/apps/docs/content/docs/platform/connected-accounts.mdx index d53631ecc38..3127c214355 100644 --- a/apps/docs/content/docs/platform/connected-accounts.mdx +++ b/apps/docs/content/docs/platform/connected-accounts.mdx @@ -11,9 +11,9 @@ Each organization has at most one pool. Creating it does not give any workspace ## Availability -Connected accounts must be enabled for your organization. Sim Cloud also requires an active Enterprise plan. Organization owners and admins manage the pool, subject to the organization's permission settings. A workspace admin who is not an organization admin cannot change the pool or its workspace access. +Sim Cloud requires an active Enterprise plan. Organization owners and admins manage the pool, subject to the organization's permission settings. A workspace admin who is not an organization admin cannot change the pool or its workspace access. -For self-hosted deployments using environment-based feature flags, set `CREDENTIAL_GROUPS=true`. Availability is organization-scoped; personal workspaces cannot use an organization pool. +For self-hosted deployments, set `CREDENTIAL_GROUPS=true`, or `ENTERPRISE_ENABLED=true` to enable the whole enterprise suite. Availability is organization-scoped; personal workspaces cannot use an organization pool. Manage the pool under **Settings → Credential Groups**. It remains available independently of Search. Search administrators use **Settings → Sources**, while members connect personal Search accounts under **Integrations** in the main sidebar. These surfaces can share grants without sharing their purpose or resource settings. diff --git a/apps/docs/content/docs/platform/enterprise/self-hosted.mdx b/apps/docs/content/docs/platform/enterprise/self-hosted.mdx index 7de297e137a..bc8c776d41b 100644 --- a/apps/docs/content/docs/platform/enterprise/self-hosted.mdx +++ b/apps/docs/content/docs/platform/enterprise/self-hosted.mdx @@ -56,6 +56,7 @@ Three features do not need a flag at all: **custom branding**, **session policie | Session policies — on by default | `SESSION_POLICIES_ENABLED` | `NEXT_PUBLIC_SESSION_POLICIES_ENABLED` | | Data retention deletion | `DATA_RETENTION_ENABLED` | `NEXT_PUBLIC_DATA_RETENTION_ENABLED` | | Data drains | `DATA_DRAINS_ENABLED` | `NEXT_PUBLIC_DATA_DRAINS_ENABLED` | +| Credential Groups | `CREDENTIAL_GROUPS` | — | | Workspace forks | `FORKING_ENABLED` | `NEXT_PUBLIC_FORKING_ENABLED` | | Sim Mailer inbox — on by default | `INBOX_ENABLED` | `NEXT_PUBLIC_INBOX_ENABLED` | | Sandboxes | `SANDBOXES_ENABLED` | `NEXT_PUBLIC_SANDBOXES_ENABLED` | diff --git a/apps/sim/.env.example b/apps/sim/.env.example index 1353afdb19e..97ab2d23095 100644 --- a/apps/sim/.env.example +++ b/apps/sim/.env.example @@ -214,7 +214,6 @@ CRON_SECRET=your_cron_secret # Use `openssl rand -hex 32` to generate. Authentic # DATA_DRAINS_ENABLED= / NEXT_PUBLIC_DATA_DRAINS_ENABLED= # Export streams # FORKING_ENABLED= # Workspace forks # CREDENTIAL_GROUPS= # Enterprise managed OAuth collections -# TABLE_ROW_TTL= # Table TTL columns and expired-row cleanup # KNOWLEDGE_MEMBER_ACCESS= # Per-member knowledge connectors and hybrid-by-default retrieval # ORGANIZATIONS_ENABLED= / NEXT_PUBLIC_ORGANIZATIONS_ENABLED= # Organizations only diff --git a/apps/sim/app/api/cron/cleanup-table-row-ttl/route.test.ts b/apps/sim/app/api/cron/cleanup-table-row-ttl/route.test.ts index f7b0ceeabef..361e4130391 100644 --- a/apps/sim/app/api/cron/cleanup-table-row-ttl/route.test.ts +++ b/apps/sim/app/api/cron/cleanup-table-row-ttl/route.test.ts @@ -1,19 +1,13 @@ import { createMockRequest } from '@sim/testing' import { asyncJobsMock, asyncJobsMockFns } from '@sim/testing/mocks/async-jobs.mock' import { authInternalMock, authInternalMockFns } from '@sim/testing/mocks/auth-internal.mock' -import { - tableTtlAvailabilityMock, - tableTtlAvailabilityMockFns, -} from '@sim/testing/mocks/table-ttl-availability.mock' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' vi.mock('@/lib/auth/internal', () => authInternalMock) vi.mock('@/lib/core/async-jobs', () => asyncJobsMock) -vi.mock('@/lib/table/ttl-availability', () => tableTtlAvailabilityMock) import { GET } from '@/app/api/cron/cleanup-table-row-ttl/route' -const { mockIsTableRowTtlEnabled } = tableTtlAvailabilityMockFns const { mockVerifyCronAuth } = authInternalMockFns const mockEnqueue = asyncJobsMockFns.mockJobQueue.enqueue @@ -23,7 +17,6 @@ describe('table row TTL cleanup route', () => { vi.useFakeTimers() vi.setSystemTime(new Date('2026-08-22T17:01:00Z')) mockVerifyCronAuth.mockReturnValue(null) - mockIsTableRowTtlEnabled.mockResolvedValue(true) mockEnqueue.mockResolvedValue('job-ttl-1') }) diff --git a/apps/sim/app/api/cron/cleanup-table-row-ttl/route.ts b/apps/sim/app/api/cron/cleanup-table-row-ttl/route.ts index a7bdab822e5..095a2bc0340 100644 --- a/apps/sim/app/api/cron/cleanup-table-row-ttl/route.ts +++ b/apps/sim/app/api/cron/cleanup-table-row-ttl/route.ts @@ -3,7 +3,6 @@ import { type NextRequest, NextResponse } from 'next/server' import { verifyCronAuth } from '@/lib/auth/internal' import { getJobQueue } from '@/lib/core/async-jobs' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' -import { isTableRowTtlEnabled } from '@/lib/table/ttl-availability' export const dynamic = 'force-dynamic' @@ -15,11 +14,6 @@ export const GET = withRouteHandler(async (request: NextRequest) => { const authError = verifyCronAuth(request, 'table row TTL cleanup') if (authError) return authError - if (!(await isTableRowTtlEnabled())) { - logger.info('Table row TTL cleanup skipped because the feature is disabled') - return NextResponse.json({ triggered: false, reason: 'feature-disabled' }) - } - const queue = await getJobQueue() const scheduleWindow = Math.floor(Date.now() / TTL_CLEANUP_INTERVAL_MS) const jobId = await queue.enqueue( diff --git a/apps/sim/app/api/table/[tableId]/query/route.test.ts b/apps/sim/app/api/table/[tableId]/query/route.test.ts index 3b1392d2641..64085385739 100644 --- a/apps/sim/app/api/table/[tableId]/query/route.test.ts +++ b/apps/sim/app/api/table/[tableId]/query/route.test.ts @@ -1,6 +1,5 @@ import { tableApiMock, tableApiMockFns } from '@sim/testing/mocks/table-api.mock' import { - MockTableV2FeatureDisabledError, tableApplicationRowsMock, tableApplicationRowsMockFns, } from '@sim/testing/mocks/table-application-rows.mock' @@ -10,15 +9,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' vi.mock('@/lib/table/api', () => tableApiMock) vi.mock('@/lib/table/api/row-route-policies', () => ({ - internalTableV2QueryErrorPolicy: { - project: (error: unknown) => - error instanceof MockTableV2FeatureDisabledError - ? { - status: 403, - body: { error: error.message, code: 'tables_v2_disabled' }, - } - : null, - }, + internalTableV2QueryErrorPolicy: { project: () => null }, })) vi.mock('@/lib/table/application/rows', () => tableApplicationRowsMock) diff --git a/apps/sim/app/api/table/[tableId]/query/route.ts b/apps/sim/app/api/table/[tableId]/query/route.ts index 0826de5b7f2..b0b2ed6483a 100644 --- a/apps/sim/app/api/table/[tableId]/query/route.ts +++ b/apps/sim/app/api/table/[tableId]/query/route.ts @@ -30,7 +30,6 @@ export const POST = defineInternalJsonRoute({ includeTotal: !body.cursor, includeRunState: false, allowExpandedLimit: true, - requireV2Feature: true, includePersistedSecretProvenance: negotiateTableRowsProvenance( request, principal.kind === 'delegated' diff --git a/apps/sim/app/api/table/utils.ts b/apps/sim/app/api/table/utils.ts index 62d27b5e86e..626cc311ace 100644 --- a/apps/sim/app/api/table/utils.ts +++ b/apps/sim/app/api/table/utils.ts @@ -19,39 +19,10 @@ import type { ColumnDefinition, Filter, TableDefinition, TablePredicate } from ' import { buildFilterClause, getTableById, TableQueryValidationError } from '@/lib/table' import { USER_TABLE_ROWS_SQL_NAME } from '@/lib/table/constants' import { TableLockedError } from '@/lib/table/mutation-locks' -import { - getTableQueryAvailability, - TABLE_QUERY_UNAVAILABLE_REASON, -} from '@/lib/table/query-availability' import { isTablePredicate } from '@/lib/table/query-builder/converters' import { validateStoragePredicate } from '@/lib/table/query-builder/validate' import type { TableLockKind } from '@/lib/table/types' import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils' -import { getWorkspaceOrganizationId } from '@/lib/workspaces/utils' - -/** - * Gate for the internal predicate-grammar table query route (`tables-v2-api` - * flag). Runs AFTER authorization, so the caller has already proven read - * access to the table — hiding the gate behind a bare 404 at that point - * serves nobody and reads as data loss (live incident: the table_v2 block - * hard-"Not found"-ing on every query while the copilot gateway, which - * bypasses HTTP, found the rows). Authorized callers get an honest 403 - * naming the gate instead. - */ -export async function tablesV2GateError( - userId: string, - workspaceId: string -): Promise { - const orgId = await getWorkspaceOrganizationId(workspaceId) - if ((await getTableQueryAvailability({ userId, orgId })).enabled) return null - return NextResponse.json( - { - error: TABLE_QUERY_UNAVAILABLE_REASON, - code: 'tables_v2_disabled', - }, - { status: 403 } - ) -} /** * Maps a {@link TableLockedError} thrown by the service layer to a 423 response diff --git a/apps/sim/app/api/v1/logs/projection.test.ts b/apps/sim/app/api/v1/logs/projection.test.ts index d335f37588f..560ad6d3236 100644 --- a/apps/sim/app/api/v1/logs/projection.test.ts +++ b/apps/sim/app/api/v1/logs/projection.test.ts @@ -67,7 +67,6 @@ const { mockGetWorkspaceBillingSettings } = workspacesUtilsMockFns workspacesUtilsMockFns.mockGetWorkspaceBilledAccountUserId.mockImplementation( async () => 'billed-user' ) -workspacesUtilsMockFns.mockGetWorkspaceOrganizationId.mockImplementation(async () => null) const { mockMaterializeExecutionDataForDisplay: mockMaterialize } = traceStoreMockFns const mockGetUserEntityPermissions = permissionsMockFns.mockGetUserEntityPermissions diff --git a/apps/sim/app/api/v1/tables/[tableId]/route.test.ts b/apps/sim/app/api/v1/tables/[tableId]/route.test.ts index c4e541be0a9..8a4c4a835f9 100644 --- a/apps/sim/app/api/v1/tables/[tableId]/route.test.ts +++ b/apps/sim/app/api/v1/tables/[tableId]/route.test.ts @@ -12,10 +12,7 @@ import { permissionsMock, permissionsMockFns } from '@sim/testing/mocks/permissi import { createMockRequest } from '@sim/testing/mocks/request.mock' import { tableMock, tableMockFns } from '@sim/testing/mocks/table.mock' import { v1MiddlewareMock, v1MiddlewareMockFns } from '@sim/testing/mocks/v1-middleware.mock' -import { - workspacesUtilsMock, - workspacesUtilsMockFns, -} from '@sim/testing/mocks/workspaces-utils.mock' +import { workspacesUtilsMock } from '@sim/testing/mocks/workspaces-utils.mock' import { beforeEach, describe, expect, it, vi } from 'vitest' const { mockPerformDeleteTable } = vi.hoisted(() => ({ @@ -48,7 +45,6 @@ vi.mock('@/lib/table/orchestration', () => ({ import { DELETE } from '@/app/api/v1/tables/[tableId]/route' -workspacesUtilsMockFns.mockGetWorkspaceOrganizationId.mockResolvedValue(null) const { mockGetTableById } = tableMockFns const { mockCheckRateLimit, mockCheckWorkspaceScope, mockResolveWorkspaceRequestActor } = diff --git a/apps/sim/app/api/v1/tables/capability-gate.test.ts b/apps/sim/app/api/v1/tables/capability-gate.test.ts index 67250bca1de..128e60e0bc9 100644 --- a/apps/sim/app/api/v1/tables/capability-gate.test.ts +++ b/apps/sim/app/api/v1/tables/capability-gate.test.ts @@ -69,7 +69,6 @@ const { mockGetWorkspaceBillingSettings } = workspacesUtilsMockFns workspacesUtilsMockFns.mockGetWorkspaceBilledAccountUserId.mockImplementation( async () => 'billed-user' ) -workspacesUtilsMockFns.mockGetWorkspaceOrganizationId.mockImplementation(async () => null) const { mockGetTableById } = tableMockFns const mockCheckWorkspaceAccess = permissionsMockFns.mockCheckWorkspaceAccess diff --git a/apps/sim/app/o/[organizationId]/home/components/composer/composer.test.tsx b/apps/sim/app/o/[organizationId]/home/components/composer/composer.test.tsx index aa1466b9499..059c28598f2 100644 --- a/apps/sim/app/o/[organizationId]/home/components/composer/composer.test.tsx +++ b/apps/sim/app/o/[organizationId]/home/components/composer/composer.test.tsx @@ -232,7 +232,6 @@ async function render( { tableTtlAvailabilityMock) vi.mock('@/app/workspace/providers/socket-provider', () => ({ SocketProvider: ({ children }: { children: import('react').ReactNode }) => children, })) diff --git a/apps/sim/app/o/[organizationId]/layout.tsx b/apps/sim/app/o/[organizationId]/layout.tsx index 3ad21c07b64..78e1ace7284 100644 --- a/apps/sim/app/o/[organizationId]/layout.tsx +++ b/apps/sim/app/o/[organizationId]/layout.tsx @@ -8,7 +8,6 @@ import { isDashboardsEnabled } from '@/lib/dashboards/feature-flag' import { isMothershipModelSelectorEnabled, isPlanModeEnabled } from '@/lib/mothership/feature-flags' import { organizationRoutes, WORKSPACE_SETTINGS_PATH } from '@/lib/navigation/paths' import { getOrganizationSurfaceContext } from '@/lib/organizations/surface' -import { isTableRowTtlEnabled } from '@/lib/table/ttl-availability' import { getQueryClient } from '@/app/_shell/providers/get-query-client' import { buildAuthCrossLink } from '@/app/(auth)/auth-redirect' import { OrganizationAccessDenied } from '@/app/o/[organizationId]/components/organization-access-denied' @@ -56,19 +55,17 @@ export default async function OrganizationLayout({ if (!context.mothershipAvailable && !context.searchAccess.memberScoped) redirect(WORKSPACE_SETTINGS_PATH) - const [, tableRowTtlEnabled, modelSelectorEnabled, planModeEnabled, dashboardsEnabled] = - await Promise.all([ - prefetchOrganizationSidebar( - queryClient, - organizationId, - { kind: 'session', userId: session.user.id, sessionId: session.session.id }, - getActiveOrganizationId(session) - ), - isTableRowTtlEnabled(), - isMothershipModelSelectorEnabled(), - isPlanModeEnabled(), - isDashboardsEnabled(organizationId), - ]) + const [, modelSelectorEnabled, planModeEnabled, dashboardsEnabled] = await Promise.all([ + prefetchOrganizationSidebar( + queryClient, + organizationId, + { kind: 'session', userId: session.user.id, sessionId: session.session.id }, + getActiveOrganizationId(session) + ), + isMothershipModelSelectorEnabled(), + isPlanModeEnabled(), + isDashboardsEnabled(organizationId), + ]) const initialSidebarCollapsed = cookieStore.get('sidebar_collapsed')?.value === '1' return ( @@ -76,7 +73,6 @@ export default async function OrganizationLayout({ Type option.type !== 'workflow') .map((option) => ({ label: option.label, diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/column-config-sidebar/column-types.ts b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/column-config-sidebar/column-types.ts index 85ef1ad1045..e5a0fe6adc1 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/column-config-sidebar/column-types.ts +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/column-config-sidebar/column-types.ts @@ -18,10 +18,6 @@ export interface ColumnTypeOption { disabledReason?: string } -interface ColumnTypeAvailability { - tableRowTtlEnabled: boolean -} - /** * Real column types come from the registry — adding one there makes it appear * in every picker automatically. `workflow` is appended because it is a UI @@ -51,13 +47,9 @@ function columnTypeLimitMessage(label: string, maxPerTable: number): string { /** Picker entries with unavailable cardinality-limited types marked as disabled. */ export function columnTypeOptionsForTable( columns: readonly ColumnDefinition[], - currentColumn: ColumnDefinition | null | undefined, - availability: ColumnTypeAvailability + currentColumn: ColumnDefinition | null | undefined ): ColumnTypeOption[] { - return COLUMN_TYPE_OPTIONS.filter( - (option) => - option.type !== 'ttl' || availability.tableRowTtlEnabled || currentColumn?.type === 'ttl' - ).map((option) => { + return COLUMN_TYPE_OPTIONS.map((option) => { if (option.type === 'workflow') return option if (currentColumn?.type === option.type) return option if (option.maxPerTable === undefined) return option diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/column-dropdown/column-dropdown.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/column-dropdown/column-dropdown.tsx index 4fc8addc7ee..2f15a7095bf 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/column-dropdown/column-dropdown.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/column-dropdown/column-dropdown.tsx @@ -25,7 +25,6 @@ const CELL_HEADER = interface ColumnDropdownProps { columns: readonly ColumnDefinition[] - tableRowTtlEnabled: boolean /** `'header'` renders the page-header trigger (subtle Button); `'inline-header'` renders * the in-table column-header `` trigger. Same dropdown content either way. */ trigger: 'header' | 'inline-header' @@ -78,7 +77,6 @@ function ColumnTypeMenuItem({ option, onSelect }: ColumnTypeMenuItemProps) { */ export function ColumnDropdown({ columns, - tableRowTtlEnabled, trigger, disabled, onPickType, @@ -132,7 +130,7 @@ export function ColumnDropdown({ {triggerButton} - {columnTypeOptionsForTable(columns, undefined, { tableRowTtlEnabled }).map((option) => { + {columnTypeOptionsForTable(columns, undefined).map((option) => { const onSelect = option.type === 'workflow' ? onPickWorkflow diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/table-grid.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/table-grid.tsx index 0cabf2f1666..8136fb81d92 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/table-grid.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/table-grid.tsx @@ -175,7 +175,6 @@ interface TableGridProps { workspaceId?: string tableId?: string embedded?: boolean - tableRowTtlEnabled: boolean /** Remote collaborators' cell selections, rendered as presence overlays. */ remoteSelections: RemoteTableSelection[] /** Broadcast the local viewer's cell selection to the table presence room. */ @@ -450,7 +449,6 @@ export function TableGrid({ workspaceId: propWorkspaceId, tableId: propTableId, embedded, - tableRowTtlEnabled, remoteSelections, emitCellSelection, locks, @@ -4968,7 +4966,6 @@ export function TableGrid({ {userPermissions.canEdit && ( ({ TABLE_LIMITS: { DELETE_SNAPSHOT_BATCH_MAX_BYTES: 32 * 1024 * 1024 }, })) vi.mock('@/lib/table/service', () => ({ withLockedTable: mockWithLockedTable })) -vi.mock('@/lib/table/ttl-availability', () => ({ isTableRowTtlEnabled: vi.fn(async () => true) })) vi.mock('@/lib/table/trigger', () => ({ fireTableTrigger: vi.fn() })) import { runCleanupTableRowTtl } from '@/background/cleanup-table-row-ttl' diff --git a/apps/sim/background/cleanup-table-row-ttl.integration.ts b/apps/sim/background/cleanup-table-row-ttl.integration.ts index 6e5ca47e380..2f8519e70b3 100644 --- a/apps/sim/background/cleanup-table-row-ttl.integration.ts +++ b/apps/sim/background/cleanup-table-row-ttl.integration.ts @@ -14,12 +14,10 @@ import { sql } from 'drizzle-orm' import postgres from 'postgres' import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' -const { enabled, signalChanged, fireTrigger } = vi.hoisted(() => ({ - enabled: vi.fn(), +const { signalChanged, fireTrigger } = vi.hoisted(() => ({ signalChanged: vi.fn(), fireTrigger: vi.fn(), })) -vi.mock('@/lib/table/ttl-availability', () => ({ isTableRowTtlEnabled: enabled })) vi.mock('@/lib/table/events', () => ({ signalTableRowsChanged: signalChanged })) vi.mock('@/lib/table/trigger', () => ({ fireTableTrigger: fireTrigger })) @@ -115,7 +113,6 @@ describe.skipIf(!migrated)('Expiration with real PostgreSQL transactions', () => }) beforeEach(async () => { - enabled.mockResolvedValue(true) fireTrigger.mockResolvedValue(undefined) await control`DELETE FROM user_table_definitions WHERE workspace_id = ${workspaceId}` }) @@ -180,12 +177,9 @@ describe.skipIf(!migrated)('Expiration with real PostgreSQL transactions', () => expect(fireTrigger.mock.calls[0][4]).toHaveLength(4) }) - it('respects feature disablement, delete locks, and archival, then catches up when restored', async () => { + it('respects delete locks and archival, then catches up when restored', async () => { const table = await createTable() await seedRows(table, 1) - enabled.mockResolvedValue(false) - expect((await runCleanupTableRowTtl()).deleted).toBe(0) - enabled.mockResolvedValue(true) await control`UPDATE user_table_definitions SET delete_locked = true WHERE id = ${table}` expect((await runCleanupTableRowTtl()).deleted).toBe(0) await control`UPDATE user_table_definitions SET delete_locked = false, archived_at = now() WHERE id = ${table}` diff --git a/apps/sim/background/cleanup-table-row-ttl.test.ts b/apps/sim/background/cleanup-table-row-ttl.test.ts index 76353f830ce..5e3975bc043 100644 --- a/apps/sim/background/cleanup-table-row-ttl.test.ts +++ b/apps/sim/background/cleanup-table-row-ttl.test.ts @@ -3,10 +3,6 @@ import { getMockLogger } from '@sim/testing/mocks/logger.mock' import { tableEventsMock, tableEventsMockFns } from '@sim/testing/mocks/table-events.mock' import { tableServiceMock, tableServiceMockFns } from '@sim/testing/mocks/table-service.mock' import { tableTriggerMock, tableTriggerMockFns } from '@sim/testing/mocks/table-trigger.mock' -import { - tableTtlAvailabilityMock, - tableTtlAvailabilityMockFns, -} from '@sim/testing/mocks/table-ttl-availability.mock' import type { SQL } from 'drizzle-orm' import { PgDialect } from 'drizzle-orm/pg-core' import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest' @@ -22,7 +18,6 @@ vi.mock('@/lib/table/constants', () => ({ TABLE_LIMITS: { DELETE_SNAPSHOT_BATCH_MAX_BYTES: 32 * 1024 * 1024 }, })) vi.mock('@/lib/table/service', () => tableServiceMock) -vi.mock('@/lib/table/ttl-availability', () => tableTtlAvailabilityMock) vi.mock('@/lib/table/trigger', () => tableTriggerMock) import { runCleanupTableRowTtl } from '@/background/cleanup-table-row-ttl' @@ -31,7 +26,6 @@ const mockListExecute = dbChainMockFns.execute as Mock const { info: mockLoggerInfo, error: mockLoggerError } = getMockLogger('CleanupTableRowTtl') const { mockSignalTableRowsChanged } = tableEventsMockFns const { mockWithLockedTable } = tableServiceMockFns -const { mockIsTableRowTtlEnabled } = tableTtlAvailabilityMockFns const { mockFireTableTrigger } = tableTriggerMockFns const dialect = new PgDialect() @@ -61,7 +55,6 @@ function returnedRows(count: number, start = 1, createdAt = '2026-01-01T00:00:00 describe('table row TTL cleanup', () => { beforeEach(() => { - mockIsTableRowTtlEnabled.mockResolvedValue(true) mockListExecute.mockResolvedValue([{ id: table.id, workspaceId: table.workspaceId }]) mockWithLockedTable.mockImplementation( async ( diff --git a/apps/sim/background/cleanup-table-row-ttl.ts b/apps/sim/background/cleanup-table-row-ttl.ts index b43f6d5e4a9..baa51c6da21 100644 --- a/apps/sim/background/cleanup-table-row-ttl.ts +++ b/apps/sim/background/cleanup-table-row-ttl.ts @@ -13,7 +13,6 @@ import type { DbTransaction } from '@/lib/table/planner' import type { DeletedTableRow } from '@/lib/table/rows/ordering' import { withLockedTable } from '@/lib/table/service' import { fireTableTrigger } from '@/lib/table/trigger' -import { isTableRowTtlEnabled } from '@/lib/table/ttl-availability' import { TTL_TIMESTAMP_VALIDATION } from '@/lib/table/ttl-values' import type { RowData, TableSchema } from '@/lib/table/types' @@ -266,10 +265,6 @@ export async function runCleanupTableRowTtl( signal?: AbortSignal ): Promise { if (signal?.aborted) return { batches: 0, deleted: 0, limitReached: false } - if (!(await isTableRowTtlEnabled())) { - logger.info('Table row TTL cleanup skipped because the feature is disabled') - return { batches: 0, deleted: 0, limitReached: false } - } const nowUtc = new Date(Date.now()).toISOString() const batchSize = getDeleteSnapshotBatchSize() diff --git a/apps/sim/lib/core/config/enterprise-entitlements.ts b/apps/sim/lib/core/config/enterprise-entitlements.ts index 55635084fc7..2080b594ff9 100644 --- a/apps/sim/lib/core/config/enterprise-entitlements.ts +++ b/apps/sim/lib/core/config/enterprise-entitlements.ts @@ -28,6 +28,7 @@ export type EnterpriseFeature = | 'accessControl' | 'auditLogs' + | 'credentialGroups' | 'customBlocks' | 'dataDrains' | 'dataRetention' @@ -76,6 +77,7 @@ export type EnterpriseFeature = export const ENTERPRISE_FEATURE_LEGACY_DEFAULTS: Readonly> = { accessControl: false, auditLogs: false, + credentialGroups: false, customBlocks: false, dataDrains: false, dataRetention: false, diff --git a/apps/sim/lib/core/config/env-flags.ts b/apps/sim/lib/core/config/env-flags.ts index c50e34abb48..634c9ab0197 100644 --- a/apps/sim/lib/core/config/env-flags.ts +++ b/apps/sim/lib/core/config/env-flags.ts @@ -296,13 +296,15 @@ export const isEnterpriseEnabled = /** * Reads a feature's own flag as a tri-state, picking the server var or its * browser twin for the current runtime. `undefined` means the operator left it - * unset, which is what lets the master switch and legacy default apply. + * unset, which is what lets the master switch and legacy default apply. A + * server-only feature passes a `null` twin. */ function explicitEnterpriseFlag( serverValue: boolean | string | undefined, - clientKey: string + clientKey: string | null ): boolean | undefined { - return typeof window === 'undefined' ? envBoolean(serverValue) : envBoolean(getEnv(clientKey)) + if (typeof window === 'undefined') return envBoolean(serverValue) + return clientKey ? envBoolean(getEnv(clientKey)) : undefined } /** @@ -317,7 +319,7 @@ function explicitEnterpriseFlag( function enterpriseFeatureEnabled( feature: EnterpriseFeature, serverValue: boolean | string | undefined, - clientKey: string + clientKey: string | null ): boolean { const explicit = explicitEnterpriseFlag(serverValue, clientKey) if (isBillingEnabled) return explicit ?? false @@ -472,6 +474,17 @@ export const isSessionPoliciesEnabled = enterpriseFeatureEnabled( 'NEXT_PUBLIC_SESSION_POLICIES_ENABLED' ) +/** + * Are Credential Groups (managed connected accounts) enabled on a deployment + * without billing. Server-only: the browser learns availability from the + * credential-groups routes, so there is no `NEXT_PUBLIC_` twin. + */ +export const isCredentialGroupsEnabled = enterpriseFeatureEnabled( + 'credentialGroups', + env.CREDENTIAL_GROUPS, + null +) + /** * Is workspace forking enabled */ diff --git a/apps/sim/lib/core/config/env.ts b/apps/sim/lib/core/config/env.ts index 89568981831..261f530f93a 100644 --- a/apps/sim/lib/core/config/env.ts +++ b/apps/sim/lib/core/config/env.ts @@ -155,8 +155,6 @@ export const env = createEnv({ COPILOT_DEV_URL: z.string().url().optional(), // Sim agent API URL for the dev mothership environment COPILOT_STAGING_URL: z.string().url().optional(), // Sim agent API URL for the staging mothership environment COPILOT_PROD_URL: z.string().url().optional(), // Sim agent API URL for the production mothership environment - AGENT_INDEXER_URL: z.string().url().optional(), // URL for agent training data indexer - AGENT_INDEXER_API_KEY: z.string().min(1).optional(), // API key for agent indexer authentication COPILOT_STREAM_TTL_SECONDS: z.number().optional(), // Redis TTL for copilot SSE buffer COPILOT_STREAM_EVENT_LIMIT: z.number().optional(), // Max events retained per stream @@ -174,13 +172,10 @@ export const env = createEnv({ STRIPE_FREE_PRICE_ID: z.string().min(1).optional(), // Stripe price ID for free tier FREE_TIER_COST_LIMIT: z.number().optional(), // Cost limit for free tier users FREE_STORAGE_LIMIT_GB: z.number().optional(), // Free-tier storage limit in GB (default 5). With billing disabled, setting it explicitly opts into storage enforcement - STRIPE_PRO_PRICE_ID: z.string().min(1).optional(), // Stripe price ID for pro tier PRO_TIER_COST_LIMIT: z.number().optional(), // Cost limit for pro tier users PRO_STORAGE_LIMIT_GB: z.number().optional().default(50), // Storage limit in GB for pro tier users - STRIPE_TEAM_PRICE_ID: z.string().min(1).optional(), // Stripe price ID for team tier TEAM_TIER_COST_LIMIT: z.number().optional(), // Cost limit for team tier users TEAM_STORAGE_LIMIT_GB: z.number().optional().default(500), // Storage limit in GB for team tier organizations (pooled) - STRIPE_ENTERPRISE_PRICE_ID: z.string().min(1).optional(), // Stripe price ID for enterprise tier ENTERPRISE_TIER_COST_LIMIT: z.number().optional(), // Cost limit for enterprise tier users ENTERPRISE_STORAGE_LIMIT_GB: z.number().optional().default(500), // Default storage limit in GB for enterprise tier (can be overridden per org) BILLING_CONCURRENCY_LIMIT_FREE: z.string().optional(), // In-flight executions per free billing account @@ -342,7 +337,6 @@ export const env = createEnv({ TRIGGER_SECRET_KEY: z.string().min(1).optional(), // Trigger.dev secret key for background jobs TRIGGER_DEV_ENABLED: z.boolean().optional(), // Toggle to enable/disable Trigger.dev for async jobs CRON_SECRET: z.string().optional(), // Secret for authenticating cron job requests - JOB_RETENTION_DAYS: z.string().optional().default('1'), // Days to retain job logs/data SCHEDULE_EXECUTION_CONCURRENCY_LIMIT: z.string().optional().default('30'), WORKFLOW_EXECUTION_CONCURRENCY_LIMIT: z.string().optional().default('75'), WEBHOOK_EXECUTION_CONCURRENCY_LIMIT: z.string().optional().default('75'), @@ -361,7 +355,6 @@ export const env = createEnv({ AWS_ACCESS_KEY_ID: z.string().optional(), // AWS access key ID AWS_SECRET_ACCESS_KEY: z.string().optional(), // AWS secret access key S3_BUCKET_NAME: z.string().optional(), // S3 bucket for general file storage - S3_LOGS_BUCKET_NAME: z.string().optional(), // S3 bucket for storing logs S3_KB_BUCKET_NAME: z.string().optional(), // S3 bucket for knowledge base files S3_EXECUTION_FILES_BUCKET_NAME: z.string().optional(), // S3 bucket for workflow execution files S3_CHAT_BUCKET_NAME: z.string().optional(), // S3 bucket for chat logos @@ -485,7 +478,6 @@ export const env = createEnv({ KB_CONFIG_DOCUMENT_BATCH_SIZE: z.number().optional().default(10), // Documents per batch in the in-process (non-Trigger) path KB_CONFIG_DELAY_BETWEEN_BATCHES: z.number().optional().default(0), // Delay between batches in ms (0 for max speed) KB_CONFIG_DELAY_BETWEEN_DOCUMENTS: z.number().optional().default(50), // Delay between documents in ms - KB_CONFIG_CHUNK_CONCURRENCY: z.number().optional().default(10), // Concurrent PDF chunk OCR processing // Real-time Communication SOCKET_SERVER_URL: z.string().url().optional(), // WebSocket server URL for real-time features @@ -527,15 +519,10 @@ export const env = createEnv({ AIRTABLE_CLIENT_SECRET: z.string().optional(), // Airtable OAuth client secret BITBUCKET_CLIENT_ID: z.string().optional(), // Bitbucket OAuth consumer key BITBUCKET_CLIENT_SECRET: z.string().optional(), // Bitbucket OAuth consumer secret - APOLLO_API_KEY: z.string().optional(), // Apollo API key (optional system-wide config) - SUPABASE_CLIENT_ID: z.string().optional(), // Supabase OAuth client ID - SUPABASE_CLIENT_SECRET: z.string().optional(), // Supabase OAuth client secret NOTION_CLIENT_ID: z.string().optional(), // Notion OAuth client ID NOTION_CLIENT_SECRET: z.string().optional(), // Notion OAuth client secret MONDAY_CLIENT_ID: z.string().optional(), // Monday.com OAuth client ID MONDAY_CLIENT_SECRET: z.string().optional(), // Monday.com OAuth client secret - DISCORD_CLIENT_ID: z.string().optional(), // Discord OAuth client ID - DISCORD_CLIENT_SECRET: z.string().optional(), // Discord OAuth client secret DOCUSIGN_CLIENT_ID: z.string().optional(), // DocuSign OAuth client ID DOCUSIGN_CLIENT_SECRET: z.string().optional(), // DocuSign OAuth client secret DOCUSIGN_AUTH_HOST: z.string().optional(), // DocuSign auth host: account-d.docusign.com (demo, default) or account.docusign.com (production) @@ -643,10 +630,8 @@ export const env = createEnv({ DATA_DRAINS_ENABLED: z.boolean().optional(), // Enable data drains on self-hosted (bypasses hosted requirements) SESSION_POLICIES_ENABLED: z.boolean().optional(), // Enable org session policies on self-hosted (bypasses hosted requirements) FORKING_ENABLED: z.boolean().optional(), // Enable workspace forking on self-hosted (bypasses hosted requirements) - TABLES_V2_API: z.boolean().optional(), // Enable the v2 tables HTTP API (public /api/v2/tables + internal /api/table/[tableId]/query predicate-grammar route) - TABLE_ROW_TTL: z.boolean().optional(), AGENT_MEMORY_HISTORY: z.boolean().optional(), - CREDENTIAL_GROUPS: z.boolean().optional(), // Enable enterprise Credential Groups globally + CREDENTIAL_GROUPS: z.boolean().optional(), // Enable Credential Groups on self-hosted (bypasses hosted requirements) KNOWLEDGE_MEMBER_ACCESS: z.boolean().optional(), // Enable per-member knowledge connectors and hybrid-by-default retrieval globally // Organizations - for self-hosted deployments @@ -766,7 +751,6 @@ export const env = createEnv({ NEXT_PUBLIC_DATA_DRAINS_ENABLED: z.boolean().optional(), // Enable data drains on self-hosted (bypasses hosted requirements) NEXT_PUBLIC_SESSION_POLICIES_ENABLED: z.boolean().optional(), // Enable org session policies on self-hosted (bypasses hosted requirements) NEXT_PUBLIC_FORKING_ENABLED: z.boolean().optional(), // Enable workspace forking on self-hosted (bypasses hosted requirements) - NEXT_PUBLIC_WORKFLOW_COLUMNS_ENABLED: z.boolean().optional(), // Show the "Workflow" column type in user tables (defaults to false) NEXT_PUBLIC_ORGANIZATIONS_ENABLED: z.boolean().optional(), // Enable organizations on self-hosted (bypasses plan requirements) NEXT_PUBLIC_DISABLE_INVITATIONS: z.boolean().optional(), // Disable workspace invitations globally (for self-hosted deployments) NEXT_PUBLIC_DISABLE_PUBLIC_API: z.boolean().optional(), // Disable public API access UI toggle globally @@ -812,7 +796,6 @@ export const env = createEnv({ NEXT_PUBLIC_DATA_DRAINS_ENABLED: process.env.NEXT_PUBLIC_DATA_DRAINS_ENABLED, NEXT_PUBLIC_SESSION_POLICIES_ENABLED: process.env.NEXT_PUBLIC_SESSION_POLICIES_ENABLED, NEXT_PUBLIC_FORKING_ENABLED: process.env.NEXT_PUBLIC_FORKING_ENABLED, - NEXT_PUBLIC_WORKFLOW_COLUMNS_ENABLED: process.env.NEXT_PUBLIC_WORKFLOW_COLUMNS_ENABLED, NEXT_PUBLIC_ENTERPRISE_ENABLED: process.env.NEXT_PUBLIC_ENTERPRISE_ENABLED, NEXT_PUBLIC_ORGANIZATIONS_ENABLED: process.env.NEXT_PUBLIC_ORGANIZATIONS_ENABLED, NEXT_PUBLIC_DISABLE_INVITATIONS: process.env.NEXT_PUBLIC_DISABLE_INVITATIONS, diff --git a/apps/sim/lib/core/config/feature-flags.test.ts b/apps/sim/lib/core/config/feature-flags.test.ts index ad1f84dde1d..3b7c47e1fc0 100644 --- a/apps/sim/lib/core/config/feature-flags.test.ts +++ b/apps/sim/lib/core/config/feature-flags.test.ts @@ -40,12 +40,9 @@ setEnv({ APPCONFIG_APPLICATION: 'sim-staging', APPCONFIG_ENVIRONMENT: 'staging', DASHBOARDS: undefined, - TABLES_V2_API: undefined, - TABLE_ROW_TTL: undefined, MSHIP_MODEL_SELECTOR: undefined, MSHIP_PLAN_MODE: undefined, AGENT_MEMORY_HISTORY: undefined, - CREDENTIAL_GROUPS: undefined, KNOWLEDGE_MEMBER_ACCESS: undefined, SLACK_SEARCH_SHARED_APP: undefined, }) @@ -110,9 +107,6 @@ describe('getFeatureFlags', () => { const flags = await getFeatureFlags() // All registered flags should be present, disabled (env vars unset in test env) expect(flags['trigger-eu-region']).toEqual({ enabled: false }) - expect(flags['tables-v2-api']).toEqual({ enabled: false }) - expect(flags['table-row-ttl']).toEqual({ enabled: false }) - expect(flags['credential-groups']).toEqual({ enabled: false }) expect(mockFetch).not.toHaveBeenCalled() }) @@ -138,9 +132,6 @@ describe('getFeatureFlags', () => { mockFetch.mockResolvedValue(null) const flags = await getFeatureFlags() expect(flags['trigger-eu-region']).toEqual({ enabled: false }) - expect(flags['tables-v2-api']).toEqual({ enabled: false }) - expect(flags['table-row-ttl']).toEqual({ enabled: false }) - expect(flags['credential-groups']).toEqual({ enabled: false }) }) it('degrades gracefully on a malformed document', async () => { @@ -154,7 +145,6 @@ describe('getFeatureFlags', () => { describe('isFeatureEnabled', () => { beforeEach(() => { setEnvFlags({ isAppConfigEnabled: false }) - envRef.CREDENTIAL_GROUPS = undefined envRef.KNOWLEDGE_MEMBER_ACCESS = undefined envRef.SLACK_SEARCH_SHARED_APP = undefined }) @@ -225,32 +215,6 @@ describe('isFeatureEnabled', () => { }) }) - describe('credential-groups flag', () => { - it('uses a global fallback switch off AppConfig', async () => { - expect(await isFeatureEnabled('credential-groups')).toBe(false) - - envRef.CREDENTIAL_GROUPS = true - expect(await isFeatureEnabled('credential-groups')).toBe(true) - }) - - it('uses the global AppConfig clause', async () => { - withAppConfig({ 'credential-groups': { enabled: true } }) - expect(await isFeatureEnabled('credential-groups')).toBe(true) - }) - - it('opens for an allowlisted organization only', async () => { - withAppConfig({ 'credential-groups': { orgIds: ['org-1'] } }) - expect(await isFeatureEnabled('credential-groups', { orgId: 'org-1' })).toBe(true) - expect(await isFeatureEnabled('credential-groups', { orgId: 'org-2' })).toBe(false) - expect(await isFeatureEnabled('credential-groups')).toBe(false) - }) - - it('a legacy workspace allowlist does not enable the organization gate', async () => { - withAppConfig({ 'credential-groups': { workspaceIds: ['ws-1'] } }) - expect(await isFeatureEnabled('credential-groups', { orgId: 'org-1' })).toBe(false) - }) - }) - it('matches the workspaceIds clause', async () => { withAppConfig({ f: { workspaceIds: ['ws-1'] } }) expect(await enabled('f', { workspaceId: 'ws-1' })).toBe(true) @@ -321,50 +285,6 @@ describe('isFeatureEnabled', () => { }) }) -describe('tables-v2-api flag', () => { - beforeEach(() => { - setEnvFlags({ isAppConfigEnabled: false }) - envRef.TABLES_V2_API = undefined - }) - - it('is off by default off-AppConfig, on when the fallback secret is set', async () => { - expect(await isFeatureEnabled('tables-v2-api')).toBe(false) - envRef.TABLES_V2_API = true - expect(await isFeatureEnabled('tables-v2-api')).toBe(true) - }) - - it('gates by org cohort via AppConfig', async () => { - withAppConfig({ 'tables-v2-api': { orgIds: ['org-1'] } }) - expect(await isFeatureEnabled('tables-v2-api', { orgId: 'org-1' })).toBe(true) - expect(await isFeatureEnabled('tables-v2-api', { orgId: 'org-2' })).toBe(false) - expect(await isFeatureEnabled('tables-v2-api', { userId: 'u1' })).toBe(false) - }) - - it('global enabled turns it on for everyone', async () => { - withAppConfig({ 'tables-v2-api': { enabled: true } }) - expect(await isFeatureEnabled('tables-v2-api')).toBe(true) - }) -}) - -describe('table-row-ttl flag', () => { - beforeEach(() => { - setEnvFlags({ isAppConfigEnabled: false }) - envRef.TABLE_ROW_TTL = undefined - }) - - it('uses a global fallback switch off AppConfig', async () => { - expect(await isFeatureEnabled('table-row-ttl')).toBe(false) - - envRef.TABLE_ROW_TTL = true - expect(await isFeatureEnabled('table-row-ttl')).toBe(true) - }) - - it('uses the global AppConfig clause', async () => { - withAppConfig({ 'table-row-ttl': { enabled: true } }) - expect(await isFeatureEnabled('table-row-ttl')).toBe(true) - }) -}) - describe('Mothership model and Plan flags', () => { beforeEach(() => { setEnvFlags({ isAppConfigEnabled: false }) diff --git a/apps/sim/lib/core/config/feature-flags.ts b/apps/sim/lib/core/config/feature-flags.ts index 566d3481832..13849510a84 100644 --- a/apps/sim/lib/core/config/feature-flags.ts +++ b/apps/sim/lib/core/config/feature-flags.ts @@ -87,29 +87,6 @@ const FEATURE_FLAGS = { 'resolveTriggerRegion, so the whole deployment switches regions together.', fallback: 'TRIGGER_EU_REGION', }, - 'tables-v2-api': { - description: - 'Gate the internal predicate-grammar table query route (POST /api/table/[tableId]/query), ' + - 'its only caller. When off, that route returns 403 naming the gate (post-authz, so the ' + - 'masquerade 404 served nobody and broke the table_v2 block confusingly). Despite the ' + - 'name it does NOT gate any /api/v2/tables route. Gated by userId/orgId/admins via ' + - 'AppConfig; off-AppConfig falls back to TABLES_V2_API.', - fallback: 'TABLES_V2_API', - }, - 'table-row-ttl': { - description: - 'Enable TTL columns and the scheduled cleanup that removes expired table rows. ' + - 'Global on/off only; existing TTL data remains readable when disabled.', - fallback: 'TABLE_ROW_TTL', - }, - 'credential-groups': { - description: - 'Managed connected accounts, including organization account pools and their settings UI. ' + - 'Uses orgId targeting only; workspace callers resolve their canonical organization. Hosted ' + - 'owners also require an active Enterprise subscription. Organization Search additionally ' + - 'requires knowledge-member-access. Off-AppConfig falls back to CREDENTIAL_GROUPS.', - fallback: 'CREDENTIAL_GROUPS', - }, projects: { description: 'Expose the Project APIs once the membership backfill has validated. Global on/off only; ' + @@ -120,9 +97,9 @@ const FEATURE_FLAGS = { 'knowledge-member-access': { description: 'Organization Search (live) and the permission-aware workspace connector modes: members ' + - '(per-member sync, which also requires credential-groups) and admin (source ACL ' + + '(per-member sync, which also requires the Credential Groups entitlement) and admin (source ACL ' + 'mirroring, independent of managed identities). Organization Search UI, MCP, and ' + - 'search APIs require this flag and credential-groups for the canonical orgId; ' + + 'search APIs require this flag and the Credential Groups entitlement for the canonical orgId; ' + 'user/admin/workspace targeting cannot enable another organization. Workspace connector ' + 'modes use workspaceId; workspace retrieval defaults may additionally use user/admin ' + 'targeting. Off-AppConfig falls back to KNOWLEDGE_MEMBER_ACCESS.', diff --git a/apps/sim/lib/credential-groups/README.md b/apps/sim/lib/credential-groups/README.md index 4f9eba0719c..e586cf2e8d1 100644 --- a/apps/sim/lib/credential-groups/README.md +++ b/apps/sim/lib/credential-groups/README.md @@ -39,43 +39,41 @@ The Providers tab lists only added providers. **Add provider** opens a searchabl Search availability and mode are managed through **Organization settings → Sources**. Member mode has no resource filters; service mode uses the configured source’s resource boundary. Live search/read adapters are registered under `lib/sim-search/live/`; they resolve only the acting member’s current grants. OAuth completion may invoke the shared dispatch helper, but live Search sources are rejected by queue and worker guards before indexing. Ordinary workspace KB sources retain ingestion behavior. -Search requires both `CREDENTIAL_GROUPS` and `KNOWLEDGE_MEMBER_ACCESS` locally; Credential Groups alone requires only its own flag. Hosted deployments additionally enforce the routed org's feature rules and Enterprise availability. Owners and admins manage Search sources; existing Knowledge permission-group rules still apply. Managed MCP account connections remain available for live tool calls only and have no indexing switch. Separate API-key KB connectors for Fireflies, Granola, and Databricks do not consume these managed MCP connections. +Credential Groups is an enterprise entitlement: Sim Cloud requires the routed organization's Enterprise plan, and self-hosted deployments enable it with `CREDENTIAL_GROUPS` (or `ENTERPRISE_ENABLED`). Search additionally requires `knowledge-member-access`. Owners and admins manage Search sources; existing Knowledge permission-group rules still apply. Managed MCP account connections remain available for live tool calls only and have no indexing switch. Separate API-key KB connectors for Fireflies, Granola, and Databricks do not consume these managed MCP connections. ### Feature gates -| Surface or behavior | Required organization flags | +| Surface or behavior | Required for the organization | | --- | --- | -| Credential Groups settings page | `credential-groups` enabled, independently of Search | -| Provider setup APIs, personal contributions, workspace access to the pool | `credential-groups` | -| Organization Home/Assistant and chat pages, Sources, member Integrations, Search MCP settings | `credential-groups` and `knowledge-member-access` | - -| Organization Search MCP endpoint and organization knowledge search through internal/public APIs or trusted tools | `credential-groups` and `knowledge-member-access`, checked after current authorization | +| Credential Groups settings page | Credential Groups entitlement, independently of Search | +| Provider setup APIs, personal contributions, workspace access to the pool | Credential Groups entitlement | +| Organization Home/Assistant and chat pages, Sources, member Integrations, Search MCP settings | Credential Groups entitlement and `knowledge-member-access` | +| Organization Search MCP endpoint and organization knowledge search through internal/public APIs or trusted tools | Credential Groups entitlement and `knowledge-member-access`, checked after current authorization | The Search gate uses the persisted knowledge base owner or the authenticated route's target organization. User, platform-admin, and workspace targeting cannot opt a different organization into Search. Disabled organizations receive `403 Search is not enabled for this organization` before index lookup or model execution; hiding navigation is not the authorization boundary. Organization Home, Search, and chat URLs open full settings in the viewer's most recent accessible workspace when Search is disabled. Default app entry uses that same destination, and Home, Integrations, chat history, and Assistant loading UI are hidden. Connected accounts settings and Workspaces remain available. Settings and source-setup URLs also enforce their gates. Ordinary workspace knowledge search keeps its existing behavior. The legacy indexed surface retains its pause controls when that backend is selected; live Sources does not expose indexing controls. -For a targeted hosted rollout, configure both existing flags in AppConfig's `feature-flags` document: +For a targeted hosted Search rollout, add the organization to `knowledge-member-access` in AppConfig's `feature-flags` document: ```json { - "credential-groups": { "enabled": false, "orgIds": ["org-to-enable"] }, "knowledge-member-access": { "enabled": false, "orgIds": ["org-to-enable"] } } ``` -`enabled: true` enables a flag globally; it is not needed alongside an org allowlist. Off AppConfig, `CREDENTIAL_GROUPS=true` and `KNOWLEDGE_MEMBER_ACCESS=true` are deployment-wide switches and cannot target individual organizations. Both flag checks still apply the organization's hosted Enterprise/billing requirements and normal membership, permission-group, and document access checks. These examples document configuration only; this change does not update a deployed AppConfig document. +`enabled: true` enables the flag globally; it is not needed alongside an org allowlist. Off AppConfig, `KNOWLEDGE_MEMBER_ACCESS=true` is a deployment-wide switch and cannot target individual organizations. Normal membership, permission-group, and document access checks still apply. -Credential-groups rollout never evaluates `workspaceIds`. Existing workspace-scoped callers resolve their owning organization and use its `orgId`; personal workspaces cannot enable connected accounts. This flag rollout is separate from the organization's workspace access allowlist, which still controls which workflows may use the pool. Normal settings no longer prefetch the legacy workspace-owned account container. +Workspace-scoped callers resolve their owning organization's entitlement; personal workspaces cannot enable connected accounts. This entitlement is separate from the organization's workspace access allowlist, which still controls which workflows may use the pool. Normal settings no longer prefetch the legacy workspace-owned account container. Current bounds: 100 entries per discovery page, 1,000 workspace allowlist entries, and 1,000 deployed event subscriptions per organization. Event delivery is synchronous after enrollment commits; a delivery failure surfaces as an error and does not roll back the saved connection. An outbox/retry mechanism is not included. ## Rollout -OAuth attempt state changes at this release boundary (OAuth v5 and managed MCP v3). Older attempts lack a verified Sim user binding; older MCP attempts also lack the configuration version. They are deliberately rejected before token exchange, with an explicit instruction to reopen the invitation and connect again. Existing saved credentials are not invalidated by the state version change. Mixed application versions cannot complete each other's in-flight attempts: pause enrollment starts, allow the ten-minute state lifetime to drain, replace the application instances together, and only then reopen enrollment and enable the org rollout. Do not run enrollment OAuth across mixed versions or roll back with active attempts. +OAuth attempt state changes at this release boundary (OAuth v5 and managed MCP v3). Older attempts lack a verified Sim user binding; older MCP attempts also lack the configuration version. They are deliberately rejected before token exchange, with an explicit instruction to reopen the invitation and connect again. Existing saved credentials are not invalidated by the state version change. Mixed application versions cannot complete each other's in-flight attempts: pause enrollment starts, allow the ten-minute state lifetime to drain, replace the application instances together, and only then reopen enrollment. Do not run enrollment OAuth across mixed versions or roll back with active attempts. 1. Apply `0328_organization_connected_accounts.sql` before deploying code that reads the new columns. It expands ownership columns and checks, adds stable enrollment identity and MCP configuration versions, and builds indexes concurrently. No grants, enrollments, or Search data are moved or deleted. Constraints are added `NOT VALID` to avoid scanning existing tables while holding the DDL lock; validate them separately after auditing existing rows. 2. Inventory existing groups and their Search dependencies before enabling the feature. The queries below read IDs/counts only. Review archived/deleted sources too because a reset must account for retained documents and cleanup work. 3. Existing org groups without the new v2 workspace policy stop with a migration-review error. Do not insert a v2 policy over legacy contributions. Resolve Search dependencies explicitly, retire the old group through an audited maintenance procedure, create a fresh org pool, and invite people to reconnect. No reset command is supplied or run by this change. -4. Enable the existing `credential-groups` feature flag for the target org (`orgIds`), then set up providers and allow specific same-org workspaces. A previous workspace-only feature-flag allowlist does not enable the org surface. Sim Cloud also requires an active Enterprise entitlement. +4. Confirm the target org has the Credential Groups entitlement (an active Enterprise plan on Sim Cloud), then set up providers and allow specific same-org workspaces. 5. Replace legacy workflow blocks, reconfigure credential references, and redeploy event subscribers. Verify one manual run, one deployed run, and one revocation before widening the workspace allowlist. ```sql diff --git a/apps/sim/lib/credential-groups/availability.test.ts b/apps/sim/lib/credential-groups/availability.test.ts index 78be9d27449..720588718c1 100644 --- a/apps/sim/lib/credential-groups/availability.test.ts +++ b/apps/sim/lib/credential-groups/availability.test.ts @@ -1,66 +1,51 @@ import { resetEnvFlagsMock, setEnvFlags } from '@sim/testing/mocks/env-flags.mock' -import { featureFlagsMock, featureFlagsMockFns } from '@sim/testing/mocks/feature-flags.mock' -import { afterAll, describe, expect, it, vi } from 'vitest' - -vi.mock('@/lib/core/config/feature-flags', () => featureFlagsMock) - +import { afterEach, describe, expect, it } from 'vitest' import { resolveCredentialGroupsAvailability } from '@/lib/credential-groups/availability' -const mockIsFeatureEnabled = featureFlagsMockFns.mockIsFeatureEnabled - -setEnvFlags({ isHosted: true }) -afterAll(resetEnvFlagsMock) +afterEach(resetEnvFlagsMock) describe('resolveCredentialGroupsAvailability', () => { - it('does not expose organization accounts in a personal workspace even with the global flag enabled', async () => { - mockIsFeatureEnabled.mockResolvedValue(true) + it('does not expose organization accounts in a personal workspace', async () => { + setEnvFlags({ isHosted: true }) await expect( resolveCredentialGroupsAvailability({ organizationId: null, ownerBilling: { isEnterprise: true }, }) ).resolves.toEqual({ available: false, reason: 'feature_disabled' }) - expect(mockIsFeatureEnabled).not.toHaveBeenCalled() }) - it('attributes a disabled feature flag before considering the plan', async () => { - mockIsFeatureEnabled.mockResolvedValue(false) - + it('requires Enterprise on Sim Cloud', async () => { + setEnvFlags({ isHosted: true }) await expect( resolveCredentialGroupsAvailability({ organizationId: 'org-1', ownerBilling: { isEnterprise: false }, }) - ).resolves.toEqual({ - available: false, - reason: 'feature_disabled', - }) - }) - - it('requires Enterprise when the hosted feature is enabled', async () => { - mockIsFeatureEnabled.mockResolvedValue(true) - + ).resolves.toEqual({ available: false, reason: 'enterprise_plan_required' }) await expect( resolveCredentialGroupsAvailability({ organizationId: 'org-1', - ownerBilling: { isEnterprise: false }, + ownerBilling: { isEnterprise: true }, }) - ).resolves.toEqual({ - available: false, - reason: 'enterprise_plan_required', - }) + ).resolves.toEqual({ available: true }) }) - it('allows Enterprise organizations when the hosted feature is enabled', async () => { - mockIsFeatureEnabled.mockResolvedValue(true) - + it('stays off on self-hosted until the entitlement is enabled', async () => { + setEnvFlags({ isHosted: false, isCredentialGroupsEnabled: false }) await expect( resolveCredentialGroupsAvailability({ organizationId: 'org-1', ownerBilling: { isEnterprise: true }, }) - ).resolves.toEqual({ - available: true, - }) + ).resolves.toEqual({ available: false, reason: 'feature_disabled' }) + + setEnvFlags({ isHosted: false, isCredentialGroupsEnabled: true }) + await expect( + resolveCredentialGroupsAvailability({ + organizationId: 'org-1', + ownerBilling: { isEnterprise: false }, + }) + ).resolves.toEqual({ available: true }) }) }) diff --git a/apps/sim/lib/credential-groups/availability.ts b/apps/sim/lib/credential-groups/availability.ts index 2010afe622b..8122ab26dba 100644 --- a/apps/sim/lib/credential-groups/availability.ts +++ b/apps/sim/lib/credential-groups/availability.ts @@ -1,5 +1,4 @@ -import { isHosted } from '@/lib/core/config/env-flags' -import { isFeatureEnabled } from '@/lib/core/config/feature-flags' +import { isCredentialGroupsEnabled, isHosted } from '@/lib/core/config/env-flags' export type CredentialGroupsAvailability = | { available: true } @@ -18,21 +17,20 @@ export async function resolveCredentialGroupsAvailability({ organizationId, ownerBilling, }: CredentialGroupsAvailabilityInput): Promise { - if ( - !organizationId || - !(await isFeatureEnabled('credential-groups', { orgId: organizationId })) - ) { - return { available: false, reason: 'feature_disabled' } + if (!organizationId) return { available: false, reason: 'feature_disabled' } + if (isHosted) { + return ownerBilling.isEnterprise + ? { available: true } + : { available: false, reason: 'enterprise_plan_required' } } - if (isHosted && !ownerBilling.isEnterprise) { - return { available: false, reason: 'enterprise_plan_required' } - } - return { available: true } + return isCredentialGroupsEnabled + ? { available: true } + : { available: false, reason: 'feature_disabled' } } /** - * Credential Groups use organization rollout targeting and require an active - * Enterprise entitlement on Sim Cloud. Workspace flag targeting is not consulted. + * Credential Groups require an active Enterprise subscription on Sim Cloud and + * the `credentialGroups` enterprise entitlement on self-hosted deployments. */ export async function isCredentialGroupsAvailable( input: CredentialGroupsAvailabilityInput diff --git a/apps/sim/lib/credential-groups/enrollments.test.ts b/apps/sim/lib/credential-groups/enrollments.test.ts index 95e82372725..be3077a0df5 100644 --- a/apps/sim/lib/credential-groups/enrollments.test.ts +++ b/apps/sim/lib/credential-groups/enrollments.test.ts @@ -12,7 +12,7 @@ import { credentialGroupsProvidersMockFns, } from '@sim/testing/mocks/credential-groups-providers.mock' import { emailMailerMock } from '@sim/testing/mocks/email-mailer.mock' -import { featureFlagsMock, featureFlagsMockFns } from '@sim/testing/mocks/feature-flags.mock' +import { setEnvFlags } from '@sim/testing/mocks/env-flags.mock' import { eq, ilike, inArray, isNull } from 'drizzle-orm' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' @@ -30,7 +30,6 @@ vi.mock('@/components/emails/credential-groups/render', () => ({ vi.mock('@/lib/messaging/email/mailer', () => emailMailerMock) vi.mock('@/lib/billing/core/subscription', () => billingSubscriptionMock) -vi.mock('@/lib/core/config/feature-flags', () => featureFlagsMock) vi.mock('@/lib/billing/core/workspace-access', () => billingWorkspaceAccessMock) @@ -41,7 +40,6 @@ vi.mock('@/lib/credential-groups/availability', () => ({ vi.mock('@/lib/credential-groups/provider-registry', () => credentialGroupsProvidersMock) import { renderCredentialGroupInvitationEmail } from '@/components/emails/credential-groups/render' -import { isFeatureEnabled } from '@/lib/core/config/feature-flags' import { bindCredentialGroupEnrollmentUser, completeCredentialGroupEnrollment, @@ -61,7 +59,7 @@ import { sendEmail } from '@/lib/messaging/email/mailer' const mockGetOrganizationSubscriptionUsable = billingSubscriptionMockFns.mockGetOrganizationSubscriptionUsable mockGetOrganizationSubscriptionUsable.mockResolvedValue({ plan: 'enterprise' }) -featureFlagsMockFns.mockIsFeatureEnabled.mockResolvedValue(true) +setEnvFlags({ isCredentialGroupsEnabled: true }) billingWorkspaceAccessMockFns.mockGetWorkspaceOwnerSubscriptionAccess.mockResolvedValue({}) credentialGroupsProvidersMockFns.mockGetCredentialGroupProviderAdapter.mockReturnValue(adapter) @@ -736,7 +734,6 @@ describe('organization enrollment bound identity', () => { beforeEach(() => { resetDbChainMock() mockGetOrganizationSubscriptionUsable.mockResolvedValue({ plan: 'enterprise' }) - vi.mocked(isFeatureEnabled).mockResolvedValue(true) queueTableRows(schemaMock.credentialGroupEnrollment, [row]) }) it('accepts a verified organization member without any workspace', async () => { diff --git a/apps/sim/lib/credential-groups/scoped-availability.test.ts b/apps/sim/lib/credential-groups/scoped-availability.test.ts index 52942f52747..47f2d021783 100644 --- a/apps/sim/lib/credential-groups/scoped-availability.test.ts +++ b/apps/sim/lib/credential-groups/scoped-availability.test.ts @@ -8,13 +8,11 @@ import { billingWorkspaceAccessMockFns, } from '@sim/testing/mocks/billing-workspace-access.mock' import { resetEnvFlagsMock, setEnvFlags } from '@sim/testing/mocks/env-flags.mock' -import { featureFlagsMock, featureFlagsMockFns } from '@sim/testing/mocks/feature-flags.mock' import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest' const hoisted = vi.hoisted(() => ({ workspaceAvailable: vi.fn(), })) -vi.mock('@/lib/core/config/feature-flags', () => featureFlagsMock) vi.mock('@/lib/billing/core/subscription', () => billingSubscriptionMock) vi.mock('@/lib/billing/core/access', () => billingAccessMock) vi.mock('@/lib/billing/core/workspace-access', () => billingWorkspaceAccessMock) @@ -26,7 +24,6 @@ import { isScopedCredentialGroupsAvailable } from '@/lib/credential-groups/scope const mocks = { ...hoisted, - feature: featureFlagsMockFns.mockIsFeatureEnabled, blocked: billingAccessMockFns.mockIsOrganizationBillingBlocked, workspace: billingWorkspaceAccessMockFns.mockGetWorkspaceOwnerSubscriptionAccess, } @@ -38,15 +35,13 @@ afterAll(resetEnvFlagsMock) describe('owner-scoped connected accounts availability', () => { beforeEach(() => { - mocks.feature.mockResolvedValue(true) mockSubscription.mockResolvedValue({ plan: 'enterprise', status: 'active' }) mocks.blocked.mockResolvedValue(false) }) - it('uses the exact organization payer and feature context without a workspace', async () => { + it('uses the exact organization payer without a workspace', async () => { await expect( isScopedCredentialGroupsAvailable({ kind: 'organization', organizationId: 'org-1' }) ).resolves.toBe(true) - expect(mocks.feature).toHaveBeenCalledWith('credential-groups', { orgId: 'org-1' }) expect(mockSubscription).toHaveBeenCalledWith('org-1', { onError: 'throw' }) expect(mocks.blocked).toHaveBeenCalledWith('org-1') expect(mocks.workspace).not.toHaveBeenCalled() diff --git a/apps/sim/lib/credential-groups/scoped-availability.ts b/apps/sim/lib/credential-groups/scoped-availability.ts index 8eb4649287a..b262dff786c 100644 --- a/apps/sim/lib/credential-groups/scoped-availability.ts +++ b/apps/sim/lib/credential-groups/scoped-availability.ts @@ -3,12 +3,11 @@ import { getOrganizationSubscriptionUsable } from '@/lib/billing/core/subscripti import { getWorkspaceOwnerSubscriptionAccess } from '@/lib/billing/core/workspace-access' import { isEnterprise } from '@/lib/billing/plan-helpers' import { hasPaidSubscriptionStatus } from '@/lib/billing/subscriptions/utils' -import { isHosted } from '@/lib/core/config/env-flags' -import { isFeatureEnabled } from '@/lib/core/config/feature-flags' +import { isCredentialGroupsEnabled, isHosted } from '@/lib/core/config/env-flags' import type { ResourceScope } from '@/lib/core/resource-scope' import { isCredentialGroupsAvailable } from '@/lib/credential-groups/availability' -/** Workspace callers inherit their canonical organization's rollout; authorization remains separate. */ +/** Workspace callers inherit their canonical organization's entitlement; authorization remains separate. */ export async function isScopedCredentialGroupsAvailable(scope: ResourceScope): Promise { if (scope.kind === 'workspace') { const ownerBilling = await getWorkspaceOwnerSubscriptionAccess(scope.workspaceId) @@ -17,8 +16,7 @@ export async function isScopedCredentialGroupsAvailable(scope: ResourceScope): P ownerBilling, }) } - if (!(await isFeatureEnabled('credential-groups', { orgId: scope.organizationId }))) return false - if (!isHosted) return true + if (!isHosted) return isCredentialGroupsEnabled const [subscription, blocked] = await Promise.all([ getOrganizationSubscriptionUsable(scope.organizationId, { onError: 'throw' }), isOrganizationBillingBlocked(scope.organizationId), diff --git a/apps/sim/lib/internal/table/operations.ts b/apps/sim/lib/internal/table/operations.ts index e55bab8afa5..9b95031290d 100644 --- a/apps/sim/lib/internal/table/operations.ts +++ b/apps/sim/lib/internal/table/operations.ts @@ -275,7 +275,6 @@ export async function executeTableQueryRowsV2( includeTotal: !body.cursor, includeRunState: false, allowExpandedLimit: true, - requireV2Feature: true, includePersistedSecretProvenance, requestId: context.requestId, }, diff --git a/apps/sim/lib/mothership/agent-cli/curation.test.ts b/apps/sim/lib/mothership/agent-cli/curation.test.ts index e333ebb1bd8..4d4b5b9d583 100644 --- a/apps/sim/lib/mothership/agent-cli/curation.test.ts +++ b/apps/sim/lib/mothership/agent-cli/curation.test.ts @@ -1,9 +1,5 @@ import { permissionCheckMock } from '@sim/testing/mocks/permission-check.mock' import { permissionGroupsResolveMockFns } from '@sim/testing/mocks/permission-groups-resolve.mock' -import { - workspaceContextMock, - workspaceContextMockFns, -} from '@sim/testing/mocks/workspace-context.mock' import { beforeEach, describe, expect, it, vi } from 'vitest' import { mothershipBlockDetailSchema } from '@/lib/api/contracts/mothership-catalog' import { type V2BlockDetail, v2BlockDetailSchema } from '@/lib/api/contracts/v2/catalog' @@ -11,7 +7,6 @@ import { curateBlockDetail } from '@/lib/mothership/agent-cli/curation' import { inputFormatValueSchema } from '@/lib/workflows/input-format-schema' import { PROVIDER_DEFINITIONS } from '@/providers/models' -const workspaceContext = workspaceContextMockFns.mockResolveActiveWorkspaceApplicationContext permissionGroupsResolveMockFns.mockGetUserPermissionConfig.mockImplementation( async () => permissionConfig.current ) @@ -34,12 +29,6 @@ vi.mock('@/lib/integrations/tool-projection', () => ({ const viewer = { workspaceId: 'ws', userId: 'user' } -const queryAvailability = vi.hoisted(() => - vi.fn(async () => ({ enabled: false, reason: 'Not enabled' })) -) -vi.mock('@/lib/table/query-availability', () => ({ getTableQueryAvailability: queryAvailability })) -vi.mock('@/lib/workspaces/application/workspace-context', () => workspaceContextMock) - function blockDetail(): V2BlockDetail { return { id: 'slack', @@ -82,10 +71,6 @@ function ok(stdout: string) { describe('curateBlockDetail', () => { beforeEach(() => { - queryAvailability.mockClear() - workspaceContext - .mockReset() - .mockResolvedValue({ workspaceId: 'ws', workspaceOrganizationId: 'canonical-target-org' }) permissionConfig.current = null denied.current = { needsProjection: new Map(), fullyDenied: new Set() } }) @@ -142,53 +127,6 @@ describe('curateBlockDetail', () => { } ) - it('projects rollout eligibility only for permitted operations using the target organization', async () => { - const original = { ...blockDetail(), toolIds: ['table_query_rows_v2'] } - const staleChatContext = { ...viewer, organizationId: 'unrelated-chat-org' } - const result = await curateBlockDetail(ok(JSON.stringify(original)), staleChatContext) - expect(JSON.parse(result.stdout).operationAvailability).toEqual({ - table_query_rows_v2: { enabled: false, reason: 'Not enabled' }, - }) - expect(queryAvailability).toHaveBeenLastCalledWith({ - userId: 'user', - orgId: 'canonical-target-org', - }) - expect(workspaceContext).toHaveBeenCalledWith('ws') - workspaceContext.mockClear() - queryAvailability.mockClear() - const legacy = await curateBlockDetail( - ok(JSON.stringify({ ...original, toolIds: ['table_query_rows'] })), - viewer - ) - expect(JSON.parse(legacy.stdout)).not.toHaveProperty('operationAvailability') - expect(queryAvailability).not.toHaveBeenCalled() - expect(workspaceContext).not.toHaveBeenCalled() - }) - - it('uses the canonical organization when the workspace viewer has no organization context', async () => { - await curateBlockDetail( - ok(JSON.stringify({ ...blockDetail(), toolIds: ['table_query_rows_v2'] })), - viewer - ) - expect(queryAvailability).toHaveBeenCalledWith({ - userId: 'user', - orgId: 'canonical-target-org', - }) - }) - - it('does not evaluate rollout for a denied typed query operation', async () => { - permissionConfig.current = { deniedTools: ['table_query_rows_v2'] } - denied.current = { - fullyDenied: new Set(), - needsProjection: new Map([['slack', new Set(['query'])]]), - } - const original = { ...blockDetail(), toolIds: ['table_query_rows_v2', 'slack_send'] } - const result = await curateBlockDetail(ok(JSON.stringify(original)), viewer) - expect(JSON.parse(result.stdout)).not.toHaveProperty('operationAvailability') - expect(workspaceContext).not.toHaveBeenCalled() - expect(queryAvailability).not.toHaveBeenCalled() - }) - it('drops denied operations and their tools from a partially denied block', async () => { permissionConfig.current = { deniedTools: ['slack_canvas'] } denied.current = { diff --git a/apps/sim/lib/mothership/agent-cli/curation.ts b/apps/sim/lib/mothership/agent-cli/curation.ts index a0fe3b41c89..4d56d8eb6f6 100644 --- a/apps/sim/lib/mothership/agent-cli/curation.ts +++ b/apps/sim/lib/mothership/agent-cli/curation.ts @@ -16,10 +16,8 @@ import { withToolBindingHints } from '@/lib/mothership/agent-cli/tool-binding-hi import { agentCliFail } from '@/lib/mothership/agent-cli/types' import type { AgentCliRawResult } from '@/lib/mothership/generated/agent-cli' import { createToolAccessGate } from '@/lib/permission-groups/operation-access' -import { getTableQueryAvailability } from '@/lib/table/query-availability' import { getBlockOutputs } from '@/lib/workflows/blocks/block-outputs' import { inputFormatValueSchema } from '@/lib/workflows/input-format-schema' -import { resolveActiveWorkspaceApplicationContext } from '@/lib/workspaces/application/workspace-context' import { getUserPermissionConfig } from '@/ee/access-control/utils/permission-check' export interface CurationViewer { @@ -97,18 +95,6 @@ export async function curateBlockDetail( : detail ) enriched = withToolBindingHints(enriched) - if (detail.toolIds.includes('table_query_rows_v2')) { - const workspace = await resolveActiveWorkspaceApplicationContext(viewer.workspaceId) - enriched = { - ...enriched, - operationAvailability: { - table_query_rows_v2: await getTableQueryAvailability({ - userId: viewer.userId, - orgId: workspace.workspaceOrganizationId, - }), - }, - } - } if (enriched === detail) return permitted return { ...permitted, stdout: JSON.stringify(mothershipBlockDetailSchema.parse(enriched)) } } diff --git a/apps/sim/lib/navigation/organization-rollout.test.ts b/apps/sim/lib/navigation/organization-rollout.test.ts index 267874e0be5..64290a72439 100644 --- a/apps/sim/lib/navigation/organization-rollout.test.ts +++ b/apps/sim/lib/navigation/organization-rollout.test.ts @@ -45,46 +45,39 @@ describe('organization rollout during impersonation', () => { mocks.platformAdmin.mockImplementation(async (userId: string) => userId === 'platform-admin') }) - it.each([ - { knowledge: false, groups: false }, - { knowledge: false, groups: true }, - { knowledge: true, groups: false }, - { knowledge: true, groups: true }, - ])('uses the customer organization for both gates: %j', async ({ knowledge, groups }) => { - const flags: FeatureFlagsConfig = { - 'knowledge-member-access': { - orgIds: ['admin-org', ...(knowledge ? ['customer-org'] : [])], - userIds: ['platform-admin'], - adminEnabled: true, - }, - 'credential-groups': { - orgIds: ['admin-org', ...(groups ? ['customer-org'] : [])], - userIds: ['platform-admin'], - adminEnabled: true, - }, - } - mocks.appConfig.mockResolvedValue(flags) + it.each([false, true])( + 'uses the customer organization for the Search gate (enabled: %s)', + async (knowledge) => { + const flags: FeatureFlagsConfig = { + 'knowledge-member-access': { + orgIds: ['admin-org', ...(knowledge ? ['customer-org'] : [])], + userIds: ['platform-admin'], + adminEnabled: true, + }, + } + mocks.appConfig.mockResolvedValue(flags) - await expect(resolveAppEntryPath({ user: { id: 'platform-admin' } })).resolves.toBe( - '/o/admin-org/home' - ) + await expect(resolveAppEntryPath({ user: { id: 'platform-admin' } })).resolves.toBe( + '/o/admin-org/home' + ) - const impersonatedSession = { - user: { id: 'customer-member' }, - session: { impersonatedBy: 'platform-admin', activeOrganizationId: 'customer-org' }, - } - await expect(resolveAppEntryPath(impersonatedSession)).resolves.toBe( - knowledge && groups ? '/o/customer-org/home' : '/workspace' - ) - expect(mocks.landing).toHaveBeenLastCalledWith('customer-member', 'customer-org') - expect(mocks.platformAdmin).not.toHaveBeenCalled() + const impersonatedSession = { + user: { id: 'customer-member' }, + session: { impersonatedBy: 'platform-admin', activeOrganizationId: 'customer-org' }, + } + await expect(resolveAppEntryPath(impersonatedSession)).resolves.toBe( + knowledge ? '/o/customer-org/home' : '/workspace' + ) + expect(mocks.landing).toHaveBeenLastCalledWith('customer-member', 'customer-org') + expect(mocks.platformAdmin).not.toHaveBeenCalled() - if (knowledge && groups) { - await expect(requireOrganizationSearchAvailable('customer-org')).resolves.toBeUndefined() - } else { - await expect(requireOrganizationSearchAvailable('customer-org')).rejects.toMatchObject({ - code: 'forbidden', - }) + if (knowledge) { + await expect(requireOrganizationSearchAvailable('customer-org')).resolves.toBeUndefined() + } else { + await expect(requireOrganizationSearchAvailable('customer-org')).rejects.toMatchObject({ + code: 'forbidden', + }) + } } - }) + ) }) diff --git a/apps/sim/lib/table/api/route-policies.test.ts b/apps/sim/lib/table/api/route-policies.test.ts index ae3c4953449..1881f05a165 100644 --- a/apps/sim/lib/table/api/route-policies.test.ts +++ b/apps/sim/lib/table/api/route-policies.test.ts @@ -18,7 +18,6 @@ import { generateInternalDelegationToken, generateInternalToken } from '@/lib/au import { OrchestrationError } from '@/lib/core/orchestration/types' import { internalTableSessionOrExecutorAuth } from '@/lib/table/api' import { v2TableErrorPolicies } from '@/lib/table/api/route-policies' -import { TableRowTtlDisabledError } from '@/lib/table/errors' const mockGetSession = authMockFns.mockGetSession const mockBindDelegation = authInternalDelegationMockFns.mockBindInternalExecutorDelegation @@ -146,24 +145,4 @@ describe('internal Table route authentication', () => { error: { code: 'BAD_REQUEST', message: 'Invalid workflow ID' }, }) }) - - it.each([false, true])( - 'preserves the TTL-disabled reason code (wrapped: %s)', - async (wrapped) => { - const error = new TableRowTtlDisabledError() - error.message = 'TTL support is turned off' - const response = v2TableErrorPolicies.default.render( - wrapped ? new Error('operation failed', { cause: error }) : error - ) - - expect(response.status).toBe(400) - await expect(response.json()).resolves.toEqual({ - error: { - code: 'BAD_REQUEST', - message: 'TTL support is turned off', - details: { code: 'TABLE_ROW_TTL_DISABLED' }, - }, - }) - } - ) }) diff --git a/apps/sim/lib/table/api/route-policies.ts b/apps/sim/lib/table/api/route-policies.ts index f0715d24536..bd8402a0809 100644 --- a/apps/sim/lib/table/api/route-policies.ts +++ b/apps/sim/lib/table/api/route-policies.ts @@ -7,10 +7,8 @@ import { internalOrchestrationErrorPolicy, type V2ErrorPolicy, } from '@/lib/api/server/routes' -import { asOrchestrationError } from '@/lib/core/orchestration/types' import { TABLE_DELEGATION_AUDIENCE } from '@/lib/table/application/authorization' import { TableOperationError } from '@/lib/table/application/errors' -import { TableRowTtlDisabledError } from '@/lib/table/errors' import { TableLockedError } from '@/lib/table/mutation-locks' import { v2CaughtOrchestrationError, @@ -27,12 +25,6 @@ export const internalTableSessionOrExecutorAuth = createInternalSessionOrExecuto }) function renderTableError(error: unknown) { - const classified = asOrchestrationError(error) - if (classified instanceof TableRowTtlDisabledError) { - return v2Error('BAD_REQUEST', classified.message, { - details: { code: classified.detailCode }, - }) - } if (error instanceof TableOperationError) { return v2ErrorForOrchestration( error.code, diff --git a/apps/sim/lib/table/api/row-route-policies.ts b/apps/sim/lib/table/api/row-route-policies.ts index 8eb5107aaf0..ef98bde448b 100644 --- a/apps/sim/lib/table/api/row-route-policies.ts +++ b/apps/sim/lib/table/api/row-route-policies.ts @@ -5,7 +5,7 @@ import { } from '@/lib/api/server/routes' import { internalTableErrorPolicies, v2TableErrorPolicies } from '@/lib/table/api/route-policies' import { TableRowProvenanceError } from '@/lib/table/application/row-secret-provenance' -import { TableRowsValidationError, TableV2FeatureDisabledError } from '@/lib/table/application/rows' +import { TableRowsValidationError } from '@/lib/table/application/rows' import { v2Error } from '@/app/api/v2/lib/response' export const v2TableRowsErrorPolicy = { @@ -39,12 +39,6 @@ export const internalTableRowsErrorPolicy = extendInternalErrorPolicy( export const internalTableV2QueryErrorPolicy = extendInternalErrorPolicy( internalTableRowsErrorPolicy, (error) => { - if (error instanceof TableV2FeatureDisabledError) { - return internalErrorResponse(403, { - error: error.message, - code: 'tables_v2_disabled', - }) - } if ( error instanceof TableRowsValidationError && typeof error.details === 'object' && diff --git a/apps/sim/lib/table/application/rows.test.ts b/apps/sim/lib/table/application/rows.test.ts index 24782791ef9..c79d6171a44 100644 --- a/apps/sim/lib/table/application/rows.test.ts +++ b/apps/sim/lib/table/application/rows.test.ts @@ -3,7 +3,6 @@ import { createSessionPrincipal, } from '@sim/testing/factories/principal.factory' import { auditMock, auditMockFns } from '@sim/testing/mocks/audit.mock' -import { featureFlagsMock, featureFlagsMockFns } from '@sim/testing/mocks/feature-flags.mock' import { tableMock, tableMockFns } from '@sim/testing/mocks/table.mock' import { tableApplicationContextMock, @@ -20,10 +19,6 @@ import { tableRowsServiceMockFns, } from '@sim/testing/mocks/table-rows-service.mock' import { workspaceAuthzMock, workspaceAuthzMockFns } from '@sim/testing/mocks/workspace-authz.mock' -import { - workspacesUtilsMock, - workspacesUtilsMockFns, -} from '@sim/testing/mocks/workspaces-utils.mock' import { beforeEach, describe, expect, it, vi } from 'vitest' import type { TableDefinition } from '@/lib/table/types' @@ -35,10 +30,6 @@ const { mockIsScopeCompatible, mockLoadExecutionsForRow, mockLoadEnrichmentDetai }) ) -vi.mock('@/lib/core/config/feature-flags', () => featureFlagsMock) - -vi.mock('@/lib/workspaces/utils', () => workspacesUtilsMock) - vi.mock('@sim/audit', () => auditMock) vi.mock('@sim/platform-authz/workspace', () => workspaceAuthzMock) @@ -121,8 +112,6 @@ const { } = tableRowsSecretProvenanceMockFns const mockReplaceRowsWithTx = tableRowsServiceMockFns.mockReplaceTableRowsWithTx const mockResolveContext = tableApplicationContextMockFns.mockResolveActiveTableContext -const mockIsFeatureEnabled = featureFlagsMockFns.mockIsFeatureEnabled -const mockGetWorkspaceOrganizationId = workspacesUtilsMockFns.mockGetWorkspaceOrganizationId const mockRecordAudit = auditMockFns.mockRecordAudit const mockResolvePermission = workspaceAuthzMockFns.mockResolveEffectiveWorkspacePermission const mockSignalRowsChanged = tableEventsMockFns.mockSignalTableRowsChanged @@ -406,8 +395,6 @@ describe('row query and upsert application semantics', () => { beforeEach(() => { mockResolvePermission.mockResolvedValue('write') mockResolveContext.mockResolvedValue(contextFor()) - mockIsFeatureEnabled.mockResolvedValue(true) - mockGetWorkspaceOrganizationId.mockResolvedValue('organization-1') }) it('rejects a malformed POST query cursor before querying storage', async () => { diff --git a/apps/sim/lib/table/application/rows.ts b/apps/sim/lib/table/application/rows.ts index a113effb4be..9f65e75baf4 100644 --- a/apps/sim/lib/table/application/rows.ts +++ b/apps/sim/lib/table/application/rows.ts @@ -1,10 +1,6 @@ import { isDeepStrictEqual } from 'node:util' import { AuditAction, AuditResourceType } from '@sim/audit' -import { - type Principal, - resolvePrincipalAttribution, - resolvePrincipalSubjectUserId, -} from '@sim/auth/principal' +import { type Principal, resolvePrincipalAttribution } from '@sim/auth/principal' import { db } from '@sim/db' import { getRequestContext } from '@sim/logger' import { generateId } from '@sim/utils/id' @@ -77,10 +73,6 @@ import { columnTypeOf } from '@/lib/table/column-types' import { TableQueryValidationError } from '@/lib/table/errors' import { signalTableRowsChanged, signalTableRowsChangedByActor } from '@/lib/table/events' import { CSV_MAX_BATCH_SIZE } from '@/lib/table/import' -import { - getTableQueryAvailability, - TABLE_QUERY_UNAVAILABLE_REASON, -} from '@/lib/table/query-availability' import { isTablePredicate, predicateToFilter } from '@/lib/table/query-builder/converters' import { validatePredicate, @@ -100,7 +92,6 @@ import type { FindRowMatch, RowWriteOptions } from '@/lib/table/rows/service' import { replaceTableRowsWithTx } from '@/lib/table/rows/service' import { predicateToStorage, resolveFilterSelectValues } from '@/lib/table/select-values' import { coerceRowValues } from '@/lib/table/validation' -import { getWorkspaceOrganizationId } from '@/lib/workspaces/utils' import type { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry' export class TableRowsValidationError extends OrchestrationError { @@ -113,13 +104,6 @@ export class TableRowsValidationError extends OrchestrationError { } } -export class TableV2FeatureDisabledError extends OrchestrationError { - constructor() { - super('forbidden', TABLE_QUERY_UNAVAILABLE_REASON) - this.name = 'TableV2FeatureDisabledError' - } -} - interface TableScopedInput { tableId: string assertedWorkspaceId?: string @@ -535,7 +519,6 @@ export interface QueryTableRowsInput extends TableScopedInput, RunStateReadInput columns?: string[] includeTotal?: boolean allowExpandedLimit?: boolean - requireV2Feature?: boolean includePersistedSecretProvenance?: boolean } @@ -559,22 +542,6 @@ export const queryTableRows = defineAuthorizedTableUseCase({ input.includePersistedSecretProvenance ) try { - if (input.requireV2Feature) { - const orgId = await getWorkspaceOrganizationId(context.workspaceId) - if ( - !( - await getTableQueryAvailability({ - // An actorless run has no user to match a per-user rule against, and a - // missing one resolves the admin clause to `false` without a query — so - // the gate only ever narrows here, never widens. - userId: resolvePrincipalSubjectUserId(principal), - orgId, - }) - ).enabled - ) { - throw new TableV2FeatureDisabledError() - } - } if (input.limit !== undefined && !input.allowExpandedLimit) { requireIntegerInRange(input.limit, 1, TABLE_LIMITS.MAX_QUERY_LIMIT, 'Limit') } else if ( diff --git a/apps/sim/lib/table/billing.ts b/apps/sim/lib/table/billing.ts index 3602737aba4..7572247d8b2 100644 --- a/apps/sim/lib/table/billing.ts +++ b/apps/sim/lib/table/billing.ts @@ -193,7 +193,7 @@ function cacheLimits(workspaceId: string, limits: TablePlanLimits): void { * answers 400 and the workspace table ceiling answers 403 * (`WORKSPACE_RESOURCE_LIMIT_REACHED`), where 409 arguably fits both. Both are * left as shipped — this error is also reachable from the internal surface, - * which is not behind the v2 flag, so unifying them is a deliberate + * which is not part of the v2 surface, so unifying them is a deliberate * cross-surface change rather than part of a v2-only pass. */ export class TableRowLimitError extends OrchestrationError { diff --git a/apps/sim/lib/table/columns/service.ts b/apps/sim/lib/table/columns/service.ts index 12d918ae86b..6c70903dc6f 100644 --- a/apps/sim/lib/table/columns/service.ts +++ b/apps/sim/lib/table/columns/service.ts @@ -43,7 +43,6 @@ import { updateTableRowsWithDerivedSecretProvenance } from '@/lib/table/rows/sec import { assertValidSchema } from '@/lib/table/schema-invariants' import { selectValueToNames } from '@/lib/table/select-values' import { withLockedTable } from '@/lib/table/service' -import { assertTableRowTtlEnabled } from '@/lib/table/ttl-availability' import { scaledStatementTimeoutMs, setTableTxTimeouts } from '@/lib/table/tx' import type { ColumnDefinition, @@ -132,8 +131,6 @@ export async function addTableColumn( requestId: string, options?: ColumnMutationOptions ): Promise { - if (column.type === 'ttl') await assertTableRowTtlEnabled() - return withLockedTable( tableId, async (table, trx) => { @@ -749,8 +746,6 @@ export async function updateColumnType( requestId: string, options?: ColumnMutationOptions ): Promise { - if (data.newType === 'ttl') await assertTableRowTtlEnabled() - return withLockedTable( data.tableId, async (table, trx) => { diff --git a/apps/sim/lib/table/columns/ttl-limit.test.ts b/apps/sim/lib/table/columns/ttl-limit.test.ts index 4639b7463f0..8a77aa25dfc 100644 --- a/apps/sim/lib/table/columns/ttl-limit.test.ts +++ b/apps/sim/lib/table/columns/ttl-limit.test.ts @@ -1,8 +1,4 @@ import { tableServiceMock, tableServiceMockFns } from '@sim/testing/mocks/table-service.mock' -import { - tableTtlAvailabilityMock, - tableTtlAvailabilityMockFns, -} from '@sim/testing/mocks/table-ttl-availability.mock' import { beforeEach, describe, expect, it, vi } from 'vitest' import type { TableDefinition, TableLocks } from '@/lib/table/types' @@ -11,12 +7,10 @@ const { mockTimeoutExecute } = vi.hoisted(() => ({ })) vi.mock('@/lib/table/service', () => tableServiceMock) -vi.mock('@/lib/table/ttl-availability', () => tableTtlAvailabilityMock) import { addTableColumn } from '@/lib/table/columns/service' const mockWithLockedTable = tableServiceMockFns.mockWithLockedTable -const mockAssertTableRowTtlEnabled = tableTtlAvailabilityMockFns.mockAssertTableRowTtlEnabled const UNLOCKED: TableLocks = { schemaLocked: false, @@ -57,22 +51,12 @@ const transaction = new Proxy( describe('TTL column mutation limit', () => { beforeEach(() => { - mockAssertTableRowTtlEnabled.mockResolvedValue(undefined) mockTimeoutExecute.mockResolvedValue([]) mockWithLockedTable.mockImplementation(async (_tableId, mutate) => mutate(makeTable(), transaction) ) }) - it('rejects adding a TTL column before locking when the feature is disabled', async () => { - mockAssertTableRowTtlEnabled.mockRejectedValue(new Error('Expiration columns are not enabled')) - - await expect( - addTableColumn('table-1', { name: 'expiry', type: 'ttl' }, 'request-1') - ).rejects.toThrow('Expiration columns are not enabled') - expect(mockWithLockedTable).not.toHaveBeenCalled() - }) - it('rejects adding a second TTL column before persistence', async () => { await expect( addTableColumn('table-1', { name: 'another_expiry', type: 'ttl' }, 'request-1') diff --git a/apps/sim/lib/table/errors.ts b/apps/sim/lib/table/errors.ts index a9ce6aa78f1..d54cf0b4a69 100644 --- a/apps/sim/lib/table/errors.ts +++ b/apps/sim/lib/table/errors.ts @@ -1,15 +1,3 @@ -import { OrchestrationError } from '@/lib/core/orchestration/types' - -/** A disabled TTL feature, distinct from malformed column input. */ -export class TableRowTtlDisabledError extends OrchestrationError { - readonly detailCode = 'TABLE_ROW_TTL_DISABLED' - - constructor() { - super('validation', 'Expiration columns are not enabled') - this.name = 'TableRowTtlDisabledError' - } -} - /** * Stable, machine-readable codes for table query failures. SDKs and clients * branch on these instead of string-matching human-facing messages. diff --git a/apps/sim/lib/table/lock-order.test.ts b/apps/sim/lib/table/lock-order.test.ts index 181b6672059..5191779da93 100644 --- a/apps/sim/lib/table/lock-order.test.ts +++ b/apps/sim/lib/table/lock-order.test.ts @@ -7,13 +7,10 @@ */ import { userTableDefinitions } from '@sim/db/schema' import { dbChainMockFns, queueTableRows, resetDbChainMock } from '@sim/testing' -import { featureFlagsMock } from '@sim/testing/mocks/feature-flags.mock' import { beforeEach, describe, expect, it, vi } from 'vitest' import { importAppendRows } from '@/lib/table/import-data' import type { TableDefinition } from '@/lib/table/types' -vi.mock('@/lib/core/config/feature-flags', () => featureFlagsMock) - vi.mock('@/lib/table/validation', () => ({ validateRowSize: vi.fn(() => ({ valid: true, errors: [] })), validateRowAgainstSchema: vi.fn(() => ({ valid: true, errors: [] })), diff --git a/apps/sim/lib/table/query-availability.ts b/apps/sim/lib/table/query-availability.ts deleted file mode 100644 index 569c868cbd6..00000000000 --- a/apps/sim/lib/table/query-availability.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { isFeatureEnabled } from '@/lib/core/config/feature-flags' - -export const TABLE_QUERY_UNAVAILABLE_REASON = - 'The v2 table query API is not enabled for this workspace' - -/** Deployment eligibility for typed predicate queries; resource authorization remains separate. */ -export async function getTableQueryAvailability(actor: { userId?: string; orgId?: string | null }) { - const enabled = await isFeatureEnabled('tables-v2-api', actor) - return enabled ? { enabled } : { enabled, reason: TABLE_QUERY_UNAVAILABLE_REASON } -} diff --git a/apps/sim/lib/table/service.test.ts b/apps/sim/lib/table/service.test.ts index 12882114794..0c7252d7637 100644 --- a/apps/sim/lib/table/service.test.ts +++ b/apps/sim/lib/table/service.test.ts @@ -7,10 +7,6 @@ import { } from '@sim/testing' import { realtimeNotifyMock } from '@sim/testing/mocks/realtime-notify.mock' import { tableBillingMock } from '@sim/testing/mocks/table-billing.mock' -import { - tableTtlAvailabilityMock, - tableTtlAvailabilityMockFns, -} from '@sim/testing/mocks/table-ttl-availability.mock' import { beforeEach, describe, expect, it, vi } from 'vitest' import type { TableSchema } from '@/lib/table/types' @@ -18,12 +14,8 @@ vi.mock('@/lib/realtime/notify', () => realtimeNotifyMock) vi.mock('@/lib/table/billing', () => tableBillingMock) -vi.mock('@/lib/table/ttl-availability', () => tableTtlAvailabilityMock) - import { createTable, getTableById } from '@/lib/table/service' -const mockAssertTableRowTtlEnabled = tableTtlAvailabilityMockFns.mockAssertTableRowTtlEnabled - const WORKSPACE_ID = '6fc7631d-88cd-46f8-9f0a-d4764daef7f8' function create(schema: TableSchema) { @@ -36,16 +28,6 @@ function create(schema: TableSchema) { describe('createTable schema invariants', () => { beforeEach(() => { resetDbChainMock() - mockAssertTableRowTtlEnabled.mockResolvedValue(undefined) - }) - - it('rejects a TTL schema before persistence when the feature is disabled', async () => { - mockAssertTableRowTtlEnabled.mockRejectedValue(new Error('Expiration columns are not enabled')) - - await expect( - create({ columns: [{ name: 'expires_at', type: 'ttl' }] } as TableSchema) - ).rejects.toThrow('Expiration columns are not enabled') - expect(dbChainMockFns.insert).not.toHaveBeenCalled() }) /** diff --git a/apps/sim/lib/table/service.ts b/apps/sim/lib/table/service.ts index 29cea8ee77f..0f0978e7e46 100644 --- a/apps/sim/lib/table/service.ts +++ b/apps/sim/lib/table/service.ts @@ -60,7 +60,6 @@ import { mutateTableRowsWithSecretProvenance, } from '@/lib/table/rows/secret-provenance' import { assertValidSchema } from '@/lib/table/schema-invariants' -import { assertTableRowTtlEnabled } from '@/lib/table/ttl-availability' import { setTableTxTimeouts } from '@/lib/table/tx' import { type CreateTableData, @@ -562,10 +561,6 @@ export async function createTable( ) } - if (data.schema.columns.some((column) => column.type === 'ttl')) { - await assertTableRowTtlEnabled() - } - const tableId = generateTableId() const now = new Date() diff --git a/apps/sim/lib/table/ttl-availability.test.ts b/apps/sim/lib/table/ttl-availability.test.ts deleted file mode 100644 index 609edc3c63f..00000000000 --- a/apps/sim/lib/table/ttl-availability.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { featureFlagsMock, featureFlagsMockFns } from '@sim/testing/mocks/feature-flags.mock' -import { describe, expect, it, vi } from 'vitest' - -vi.mock('@/lib/core/config/feature-flags', () => featureFlagsMock) - -import { assertTableRowTtlEnabled } from '@/lib/table/ttl-availability' - -const mockIsFeatureEnabled = featureFlagsMockFns.mockIsFeatureEnabled - -describe('table row TTL availability', () => { - it('rejects TTL column creation while the flag is disabled', async () => { - mockIsFeatureEnabled.mockResolvedValue(false) - - await expect(assertTableRowTtlEnabled()).rejects.toMatchObject({ - code: 'validation', - detailCode: 'TABLE_ROW_TTL_DISABLED', - }) - }) - - it('propagates flag lookup failures instead of reporting the feature as disabled', async () => { - const error = new Error('flag service unavailable') - mockIsFeatureEnabled.mockRejectedValue(error) - - await expect(assertTableRowTtlEnabled()).rejects.toBe(error) - }) -}) diff --git a/apps/sim/lib/table/ttl-availability.ts b/apps/sim/lib/table/ttl-availability.ts deleted file mode 100644 index a04002f12c4..00000000000 --- a/apps/sim/lib/table/ttl-availability.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { isFeatureEnabled } from '@/lib/core/config/feature-flags' -import { TableRowTtlDisabledError } from '@/lib/table/errors' - -/** Whether TTL columns and their cleanup behavior are enabled globally. */ -export function isTableRowTtlEnabled(): Promise { - return isFeatureEnabled('table-row-ttl') -} - -/** Rejects attempts to introduce a TTL column while the feature is disabled. */ -export async function assertTableRowTtlEnabled(): Promise { - if (await isTableRowTtlEnabled()) return - throw new TableRowTtlDisabledError() -} diff --git a/apps/sim/lib/table/workflow-groups/service.test.ts b/apps/sim/lib/table/workflow-groups/service.test.ts index 54a8a861918..caded69e4aa 100644 --- a/apps/sim/lib/table/workflow-groups/service.test.ts +++ b/apps/sim/lib/table/workflow-groups/service.test.ts @@ -1,11 +1,7 @@ import { tableRowsSecretProvenanceMock } from '@sim/testing/mocks/table-rows-secret-provenance.mock' import { tableServiceMock, tableServiceMockFns } from '@sim/testing/mocks/table-service.mock' -import { - tableTtlAvailabilityMock, - tableTtlAvailabilityMockFns, -} from '@sim/testing/mocks/table-ttl-availability.mock' import { tableWorkflowColumnsMock } from '@sim/testing/mocks/table-workflow-columns.mock' -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { describe, expect, it, vi } from 'vitest' import type { TableDefinition, TableMetadata, TableSchema, WorkflowGroup } from '@/lib/table/types' vi.mock('@/lib/table/service', () => tableServiceMock) @@ -14,7 +10,6 @@ vi.mock('@/lib/table/mutation-locks', () => ({ assertSchemaMutable: vi.fn(), })) vi.mock('@/lib/table/rows/secret-provenance', () => tableRowsSecretProvenanceMock) -vi.mock('@/lib/table/ttl-availability', () => tableTtlAvailabilityMock) vi.mock('@/lib/table/workflow-columns', () => tableWorkflowColumnsMock) /** * These ceiling fixtures declare groups whose output columns are not in the @@ -26,9 +21,7 @@ vi.mock('@/lib/table/schema-invariants', () => ({ })) import { TABLE_LIMITS } from '@/lib/table/constants' -import { addWorkflowGroup, updateWorkflowGroup } from '@/lib/table/workflow-groups/service' - -const mockAssertTableRowTtlEnabled = tableTtlAvailabilityMockFns.mockAssertTableRowTtlEnabled +import { addWorkflowGroup } from '@/lib/table/workflow-groups/service' const mockWithLockedTable = tableServiceMockFns.mockWithLockedTable @@ -67,10 +60,6 @@ function tableWithGroups(count: number): TableDefinition { * columns are capped) does not survive an update path that adds none. */ describe('addWorkflowGroup group ceiling', () => { - beforeEach(() => { - mockAssertTableRowTtlEnabled.mockResolvedValue(undefined) - }) - function add(existingGroups: number) { const table = tableWithGroups(existingGroups) mockWithLockedTable.mockImplementation( @@ -100,44 +89,6 @@ describe('addWorkflowGroup group ceiling', () => { }) }) -describe('workflow group TTL availability', () => { - beforeEach(() => { - mockAssertTableRowTtlEnabled.mockRejectedValue(new Error('Expiration columns are not enabled')) - }) - - it.each([ - [ - 'group creation', - () => - addWorkflowGroup( - { - tableId: 'table-1', - workspaceId: 'workspace-1', - group: groupAt(1), - outputColumns: [{ name: 'expires_at', type: 'ttl' }], - } as Parameters[0], - 'request-1' - ), - ], - [ - 'group update', - () => - updateWorkflowGroup( - { - tableId: 'table-1', - workspaceId: 'workspace-1', - groupId: 'group-1', - newOutputColumns: [{ name: 'expires_at', type: 'ttl' }], - } as Parameters[0], - 'request-1' - ), - ], - ])('rejects TTL introduction through %s while disabled', async (_label, introduceTtl) => { - await expect(introduceTtl()).rejects.toThrow('Expiration columns are not enabled') - expect(mockWithLockedTable).not.toHaveBeenCalled() - }) -}) - /** * A group may take over columns the table already has. Before this, any * `outputColumns` name matching an existing column was refused as a duplicate, @@ -186,10 +137,6 @@ describe('addWorkflowGroup attaching existing columns', () => { ) } - beforeEach(() => { - mockAssertTableRowTtlEnabled.mockResolvedValue(undefined) - }) - it('attaches an existing column named in outputColumns instead of creating a duplicate', async () => { const written = arrangeWrite() @@ -271,10 +218,6 @@ describe('addWorkflowGroup attaching existing columns', () => { * while the cell runner had been loading the deployment all along. */ describe('addWorkflowGroup deployment mode', () => { - beforeEach(() => { - mockAssertTableRowTtlEnabled.mockResolvedValue(undefined) - }) - function add(group: WorkflowGroup) { const set = vi.fn(() => ({ where: () => Promise.resolve() })) mockWithLockedTable.mockImplementation( diff --git a/apps/sim/lib/table/workflow-groups/service.ts b/apps/sim/lib/table/workflow-groups/service.ts index 5bd853a6cb7..1107717c54a 100644 --- a/apps/sim/lib/table/workflow-groups/service.ts +++ b/apps/sim/lib/table/workflow-groups/service.ts @@ -25,7 +25,6 @@ import { stripGroupExecutions } from '@/lib/table/rows/executions' import { updateTableRowsWithDerivedSecretProvenance } from '@/lib/table/rows/secret-provenance' import { assertValidSchema } from '@/lib/table/schema-invariants' import { withLockedTable } from '@/lib/table/service' -import { assertTableRowTtlEnabled } from '@/lib/table/ttl-availability' import { setTableTxTimeouts } from '@/lib/table/tx' import type { AddWorkflowGroupData, @@ -133,10 +132,6 @@ export async function addWorkflowGroup( data: AddWorkflowGroupData, requestId: string ): Promise { - if (data.outputColumns.some((column) => column.type === 'ttl')) { - await assertTableRowTtlEnabled() - } - const updatedTable = await withLockedTable( data.tableId, async (table, trx) => { @@ -323,11 +318,6 @@ export async function updateWorkflowGroup( requestId: string ): Promise { const mappingUpdates = data.mappingUpdates ?? [] - const introducesTtl = - data.newOutputColumns?.some((column) => column.type === 'ttl') === true || - data.resolvedMappingTypes?.columns.some((column) => column.type === 'ttl') === true - if (introducesTtl) await assertTableRowTtlEnabled() - // Phase 1 (no lock): consume the output types resolved and authorized by the // application command. Resolution stays outside the advisory-lock critical // section so concurrent group edits do not hold the schema lock during the diff --git a/apps/sim/lib/workspaces/application/list-organization-workspaces.test.ts b/apps/sim/lib/workspaces/application/list-organization-workspaces.test.ts index 11ad3623ca1..af21a6eab39 100644 --- a/apps/sim/lib/workspaces/application/list-organization-workspaces.test.ts +++ b/apps/sim/lib/workspaces/application/list-organization-workspaces.test.ts @@ -1,5 +1,4 @@ import { createSessionPrincipal } from '@sim/testing/factories/principal.factory' -import { featureFlagsMock, featureFlagsMockFns } from '@sim/testing/mocks/feature-flags.mock' import { organizationAuthorizationMock, organizationAuthorizationMockFns, @@ -14,7 +13,6 @@ import { } from '@sim/testing/mocks/workspaces-utils.mock' import { beforeEach, describe, expect, it, vi } from 'vitest' -vi.mock('@/lib/core/config/feature-flags', () => featureFlagsMock) vi.mock('@/lib/core/application/organization-authorization', () => organizationAuthorizationMock) vi.mock('@/lib/workspaces/utils', () => workspacesUtilsMock) vi.mock('@/lib/permission-groups/config-scope.server', () => permissionGroupScopeMock) @@ -23,7 +21,6 @@ import { DEFAULT_PERMISSION_GROUP_CONFIG } from '@/lib/permission-groups/fields' import { listOrganizationWorkspaces } from '@/lib/workspaces/application/list-organization-workspaces' const mocks = { - feature: featureFlagsMockFns.mockIsFeatureEnabled, rows: workspacesUtilsMockFns.mockListAccessibleWorkspaceRowsForUser, authorize: organizationAuthorizationMockFns.mockAuthorizeOrganizationOperation, config: permissionGroupScopeMockFns.mockResolvePermissionGroupConfig, @@ -37,7 +34,6 @@ const row = (id: string, organizationId = 'org', role: 'read' | 'write' | 'admin beforeEach(() => { mocks.authorize.mockResolvedValue({ userId: 'user', organizationId: 'org' }) mocks.config.mockResolvedValue(DEFAULT_PERMISSION_GROUP_CONFIG) - mocks.feature.mockResolvedValue(false) mocks.rows.mockResolvedValue([row('b', 'org', 'write'), row('a'), row('outside', 'other')]) }) describe('organization workspace inventory', () => { @@ -50,7 +46,6 @@ describe('organization workspace inventory', () => { }) ).workspaces ).toEqual([]) - expect(mocks.feature).not.toHaveBeenCalled() mocks.authorize.mockRejectedValue(new Error('Membership revoked')) await expect( listOrganizationWorkspaces.execute({ principal, input: { organizationId: 'org', limit: 1 } }) @@ -74,16 +69,3 @@ describe('organization workspace inventory', () => { }) }) }) - -it('does not disclose rollout detail for a denied operation', async () => { - mocks.config.mockResolvedValue({ - ...DEFAULT_PERMISSION_GROUP_CONFIG, - deniedTools: ['table_query_rows_v2'], - }) - const result = await listOrganizationWorkspaces.execute({ - principal, - input: { organizationId: 'org', workspaceId: 'a', limit: 1 }, - }) - expect(result.workspaces[0]).toMatchObject({ operationAvailability: {} }) - expect(mocks.feature).not.toHaveBeenCalled() -}) diff --git a/apps/sim/lib/workspaces/application/list-organization-workspaces.ts b/apps/sim/lib/workspaces/application/list-organization-workspaces.ts index dc7ff86505c..92fcadaf385 100644 --- a/apps/sim/lib/workspaces/application/list-organization-workspaces.ts +++ b/apps/sim/lib/workspaces/application/list-organization-workspaces.ts @@ -7,8 +7,6 @@ import { } from '@/lib/permission-groups/capabilities' import { capabilityDeniedBy } from '@/lib/permission-groups/capability-assertions' import { resolvePermissionGroupConfig } from '@/lib/permission-groups/config-scope.server' -import { createToolAccessGate } from '@/lib/permission-groups/operation-access' -import { getTableQueryAvailability } from '@/lib/table/query-availability' import { organizationWorkspaceOperations } from '@/lib/workspaces/application/organization-operations' import { listAccessibleWorkspaceRowsForUser } from '@/lib/workspaces/utils' @@ -69,16 +67,6 @@ export const listOrganizationWorkspaces = { ? { capabilityDetail: 'full' as const, capabilities, - operationAvailability: createToolAccessGate(config?.deniedTools)( - 'table_query_rows_v2' - ) - ? { - table_query_rows_v2: await getTableQueryAvailability({ - userId: context.userId, - orgId: context.organizationId, - }), - } - : {}, } : { capabilityDetail: 'restrictions' as const, diff --git a/apps/sim/lib/workspaces/utils.ts b/apps/sim/lib/workspaces/utils.ts index 40dcd050f33..ce6e57dac79 100644 --- a/apps/sim/lib/workspaces/utils.ts +++ b/apps/sim/lib/workspaces/utils.ts @@ -48,21 +48,6 @@ export async function getWorkspaceBilledAccountUserId(workspaceId: string): Prom return settings?.billedAccountUserId ?? null } -/** - * The organization that owns a workspace (null for personal workspaces). Used to - * gate features by org cohort — the flag model allowlists org ids, and API routes - * only carry a `workspaceId`, so this resolves the one from the other. - */ -export async function getWorkspaceOrganizationId(workspaceId: string): Promise { - if (!workspaceId) return null - const rows = await db - .select({ organizationId: workspaceTable.organizationId }) - .from(workspaceTable) - .where(eq(workspaceTable.id, workspaceId)) - .limit(1) - return rows[0]?.organizationId ?? null -} - /** * Workspaces the user administers purely through organization owner/admin role, * with no explicit permission row required. Empty when the user is not an org diff --git a/helm/sim/Chart.yaml b/helm/sim/Chart.yaml index 1b0be5399dd..51d97537731 100644 --- a/helm/sim/Chart.yaml +++ b/helm/sim/Chart.yaml @@ -2,7 +2,7 @@ apiVersion: v2 name: sim description: A Helm chart for Sim - the open-source AI workspace where teams build, deploy, and manage AI agents type: application -version: 1.11.7 +version: 1.11.8 appVersion: "v0.8.26" kubeVersion: ">=1.25.0-0" home: https://sim.ai diff --git a/helm/sim/values.yaml b/helm/sim/values.yaml index d8e46e8503c..7817b60cb1b 100644 --- a/helm/sim/values.yaml +++ b/helm/sim/values.yaml @@ -113,7 +113,6 @@ app: # Optional: Scheduled Jobs Authentication # Generate using: openssl rand -hex 32 CRON_SECRET: "" # OPTIONAL - required only if cronjobs.enabled=true, authenticates scheduled job requests - TABLE_ROW_TTL: "" # Enable TTL columns and expired-row cleanup when AppConfig is unavailable KNOWLEDGE_MEMBER_ACCESS: "" # Enable per-member knowledge connectors when AppConfig is unavailable # Optional: API Key Encryption (RECOMMENDED for production) diff --git a/packages/testing/src/mocks/env-flags.mock.ts b/packages/testing/src/mocks/env-flags.mock.ts index cba6ac6fc4c..e58ce718feb 100644 --- a/packages/testing/src/mocks/env-flags.mock.ts +++ b/packages/testing/src/mocks/env-flags.mock.ts @@ -39,6 +39,7 @@ interface EnvFlagsMockState { isWhitelabelingEnabled: boolean isAuditLogsEnabled: boolean isCustomBlocksEnabled: boolean + isCredentialGroupsEnabled: boolean isDataRetentionEnabled: boolean isDataDrainsEnabled: boolean isSessionPoliciesEnabled: boolean @@ -97,6 +98,7 @@ const defaultEnvFlagsState: EnvFlagsMockState = { isSessionPoliciesEnabled: true, isAuditLogsEnabled: false, isCustomBlocksEnabled: false, + isCredentialGroupsEnabled: false, isDataRetentionEnabled: false, isDataDrainsEnabled: false, isForkingEnabled: false, diff --git a/packages/testing/src/mocks/index.ts b/packages/testing/src/mocks/index.ts index f8dd5d0d97a..ffbbc317d54 100644 --- a/packages/testing/src/mocks/index.ts +++ b/packages/testing/src/mocks/index.ts @@ -759,7 +759,6 @@ export { export { MockCsvImportValidationError, MockTableQueryValidationError, - MockTableRowTtlDisabledError, MockTableViewValidationError, tableMock, tableMockFns, @@ -775,7 +774,6 @@ export { export { MockProjectedWireRowsValidationError, MockTableRowsValidationError, - MockTableV2FeatureDisabledError, tableApplicationRowsMock, tableApplicationRowsMockFns, } from './table-application-rows.mock' @@ -826,10 +824,6 @@ export { tableTriggerMock, tableTriggerMockFns, } from './table-trigger.mock' -export { - tableTtlAvailabilityMock, - tableTtlAvailabilityMockFns, -} from './table-ttl-availability.mock' export { tableWireMock, tableWireMockFns, diff --git a/packages/testing/src/mocks/table-application-rows.mock.ts b/packages/testing/src/mocks/table-application-rows.mock.ts index b1e6446308f..0f6ac8a9285 100644 --- a/packages/testing/src/mocks/table-application-rows.mock.ts +++ b/packages/testing/src/mocks/table-application-rows.mock.ts @@ -18,19 +18,6 @@ export class MockTableRowsValidationError extends Error { } } -/** - * Stand-in for `TableV2FeatureDisabledError`: same `name`, `code: 'forbidden'` and the real - * `TABLE_QUERY_UNAVAILABLE_REASON` message. Not an `OrchestrationError` subclass. - */ -export class MockTableV2FeatureDisabledError extends Error { - readonly code = 'forbidden' as const - - constructor() { - super('The v2 table query API is not enabled for this workspace') - this.name = 'TableV2FeatureDisabledError' - } -} - /** * Stand-in for `ProjectedWireRowsValidationError`: subclass of * {@link MockTableRowsValidationError} with the real `name`. @@ -99,7 +86,6 @@ const fns = tableApplicationRowsMockFns */ export const tableApplicationRowsMock = { TableRowsValidationError: MockTableRowsValidationError, - TableV2FeatureDisabledError: MockTableV2FeatureDisabledError, ProjectedWireRowsValidationError: MockProjectedWireRowsValidationError, tablePredicateNamesToFilter: fns.mockTablePredicateNamesToFilter, listTableRows: { diff --git a/packages/testing/src/mocks/table-route-utils.mock.ts b/packages/testing/src/mocks/table-route-utils.mock.ts index ae611388a3f..ec937c956bc 100644 --- a/packages/testing/src/mocks/table-route-utils.mock.ts +++ b/packages/testing/src/mocks/table-route-utils.mock.ts @@ -75,8 +75,7 @@ function errorResponse(message: string, status: number, details?: unknown): Resp * `rootErrorMessage`, `csvProxyBodyCapResponse`, `multipartErrorResponse`, * `capabilityGovernedUserId`, `accessError`, `errorResponse`, `badRequestResponse`, * `unauthorizedResponse`, `forbiddenResponse`, `notFoundResponse`. - * `tablesV2GateError` resolves `null` (the gate is open); `tableFilterError` returns `null` (the - * filter is valid). `checkAccess` is a bare `vi.fn()`. + * `tableFilterError` returns `null` (the filter is valid). `checkAccess` is a bare `vi.fn()`. * * @example * ```ts @@ -86,9 +85,6 @@ function errorResponse(message: string, status: number, details?: unknown): Resp * ``` */ export const tableRouteUtilsMockFns = { - mockTablesV2GateError: vi.fn( - async (_userId: string, _workspaceId: string): Promise => null - ), mockTableLockErrorResponse: vi.fn((error: unknown): Response | null => lockResponse(error)), mockTableFilterError: vi.fn((_filter: unknown, _columns: unknown): Response | null => null), mockRootErrorMessage: vi.fn((error: unknown): string => { @@ -183,7 +179,6 @@ export const tableRouteUtilsMockFns = { */ export const tableRouteUtilsMock = { CSV_IMPORT_PROXY_BODY_CAP_BYTES: 10 * 1024 * 1024, - tablesV2GateError: tableRouteUtilsMockFns.mockTablesV2GateError, tableLockErrorResponse: tableRouteUtilsMockFns.mockTableLockErrorResponse, tableFilterError: tableRouteUtilsMockFns.mockTableFilterError, rootErrorMessage: tableRouteUtilsMockFns.mockRootErrorMessage, diff --git a/packages/testing/src/mocks/table-ttl-availability.mock.ts b/packages/testing/src/mocks/table-ttl-availability.mock.ts deleted file mode 100644 index c94365242c9..00000000000 --- a/packages/testing/src/mocks/table-ttl-availability.mock.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { vi } from 'vitest' - -/** - * Controllable mock functions for `@/lib/table/ttl-availability`. - * - * Defaults: `isTableRowTtlEnabled` resolves `false` (the flag is off), `assertTableRowTtlEnabled` - * resolves `undefined` (the gate passes). The two are independent: flipping one does not affect - * the other. - * - * @example - * ```ts - * import { tableTtlAvailabilityMockFns } from '@sim/testing/mocks/table-ttl-availability.mock' - * - * tableTtlAvailabilityMockFns.mockIsTableRowTtlEnabled.mockResolvedValue(true) - * ``` - */ -export const tableTtlAvailabilityMockFns = { - mockIsTableRowTtlEnabled: vi.fn(async (): Promise => false), - mockAssertTableRowTtlEnabled: vi.fn(async (): Promise => {}), -} - -/** - * Static mock module for `@/lib/table/ttl-availability`. - * - * @example - * ```ts - * vi.mock('@/lib/table/ttl-availability', () => tableTtlAvailabilityMock) - * ``` - */ -export const tableTtlAvailabilityMock = { - isTableRowTtlEnabled: tableTtlAvailabilityMockFns.mockIsTableRowTtlEnabled, - assertTableRowTtlEnabled: tableTtlAvailabilityMockFns.mockAssertTableRowTtlEnabled, -} diff --git a/packages/testing/src/mocks/table.mock.ts b/packages/testing/src/mocks/table.mock.ts index 2b5a5bda6c4..34d22de51d2 100644 --- a/packages/testing/src/mocks/table.mock.ts +++ b/packages/testing/src/mocks/table.mock.ts @@ -39,22 +39,6 @@ export class MockTableQueryValidationError extends Error { } } -/** - * Stand-in for `TableRowTtlDisabledError` from `@/lib/table/errors`: same `name`, - * `code: 'validation'`, `detailCode` and message. It extends `Error`, not the real - * `OrchestrationError`, so code under test that checks `instanceof OrchestrationError` will not - * match it; classifiers reading `.code` do. - */ -export class MockTableRowTtlDisabledError extends Error { - readonly code = 'validation' as const - readonly detailCode = 'TABLE_ROW_TTL_DISABLED' - - constructor() { - super('Expiration columns are not enabled') - this.name = 'TableRowTtlDisabledError' - } -} - /** * Stand-in for `CsvImportValidationError` from `@/lib/table/import`: same `name`, * `code: 'CSV_IMPORT_VALIDATION'`, message and `details`. @@ -279,7 +263,6 @@ export const tableMock = { ...tableRowsServiceMock, ...tableServiceMock, TableQueryValidationError: MockTableQueryValidationError, - TableRowTtlDisabledError: MockTableRowTtlDisabledError, CsvImportValidationError: MockCsvImportValidationError, TableViewValidationError: MockTableViewValidationError, DEFAULT_CURRENCY_CODE: 'USD', diff --git a/packages/testing/src/mocks/workspaces-utils.mock.ts b/packages/testing/src/mocks/workspaces-utils.mock.ts index 4c2da675994..cc057a8f592 100644 --- a/packages/testing/src/mocks/workspaces-utils.mock.ts +++ b/packages/testing/src/mocks/workspaces-utils.mock.ts @@ -33,7 +33,6 @@ export class MockWorkspaceBillingAccountRemovalError extends Error { export const workspacesUtilsMockFns = { mockGetWorkspaceBillingSettings: vi.fn(), mockGetWorkspaceBilledAccountUserId: vi.fn(), - mockGetWorkspaceOrganizationId: vi.fn(), mockListAccessibleWorkspaceRowsForUser: vi.fn(), mockTransferWorkspaceOwnershipToBilledAccountForMemberRemovalTx: vi.fn(), mockReassignWorkflowOwnershipForWorkspaceMemberRemovalTx: vi.fn(), @@ -72,7 +71,6 @@ export const workspacesUtilsMock = { WorkspaceBillingAccountRemovalError: MockWorkspaceBillingAccountRemovalError, getWorkspaceBillingSettings: workspacesUtilsMockFns.mockGetWorkspaceBillingSettings, getWorkspaceBilledAccountUserId: workspacesUtilsMockFns.mockGetWorkspaceBilledAccountUserId, - getWorkspaceOrganizationId: workspacesUtilsMockFns.mockGetWorkspaceOrganizationId, listAccessibleWorkspaceRowsForUser: workspacesUtilsMockFns.mockListAccessibleWorkspaceRowsForUser, transferWorkspaceOwnershipToBilledAccountForMemberRemovalTx: workspacesUtilsMockFns.mockTransferWorkspaceOwnershipToBilledAccountForMemberRemovalTx, diff --git a/scripts/check-unused-exports.baseline.json b/scripts/check-unused-exports.baseline.json index ff4ded5f495..f8755347171 100644 --- a/scripts/check-unused-exports.baseline.json +++ b/scripts/check-unused-exports.baseline.json @@ -270,7 +270,6 @@ "apps/sim/app/api/table/utils.ts#errorResponse", "apps/sim/app/api/table/utils.ts#forbiddenResponse", "apps/sim/app/api/table/utils.ts#notFoundResponse", - "apps/sim/app/api/table/utils.ts#tablesV2GateError", "apps/sim/app/api/table/utils.ts#unauthorizedResponse", "apps/sim/app/api/v1/admin/responses.ts#errorResponse", "apps/sim/app/api/v1/admin/types.ts#DEFAULT_LIMIT",