diff --git a/.claude/commands/add-feature-flag.md b/.claude/commands/add-feature-flag.md new file mode 100644 index 0000000000..74670ede56 --- /dev/null +++ b/.claude/commands/add-feature-flag.md @@ -0,0 +1,72 @@ +--- +description: Add a runtime gated feature flag (AppConfig-backed on prod, secret fallback off-prod), gated by org id, user id, or admin +argument-hint: +--- + +# Add Feature Flag Skill + +You add a **runtime, gated feature flag** to Sim — one that can be turned on for specific orgs, users, or admins and changed on prod with no redeploy (AWS AppConfig). When AppConfig isn't the source of truth, the flag falls back to a single **secret** (on/off only). + +## When to use this vs `env-flags.ts` + +- **Feature flag** (`@/lib/core/config/feature-flags.ts`): per-request, gated by `userId`/`orgId`/admin, changeable at runtime. This skill. +- **Env flag** (`@/lib/core/config/env-flags.ts`): deploy-time capability/environment detection (`isProd`, `isHosted`, `isBillingEnabled`). A module-load boolean. **Do not add gated flags here.** + +If the user wants a fixed per-deployment toggle, send them to `env-flags.ts` instead. + +## The flag model + +A flag's **gating rule lives only in the hosted AppConfig document**. It is ON for a context when any clause matches: + +```ts +interface FeatureFlagRule { + enabled?: boolean // global default for everyone + orgIds?: string[] // allowlisted organization ids + userIds?: string[] // allowlisted user ids + admins?: boolean // platform admins (user.role === 'admin') +} +``` + +Critically, **none of this is expressible in code** — gating (especially `admins`) can only be set through AppConfig, so no environment can grant access from a code literal. Off-AppConfig (self-hosted/OSS/local), a flag is simply on or off, derived from its fallback secret. + +## Steps + +1. **Define the flag.** Add one entry to the `FEATURE_FLAGS` registry in `apps/sim/lib/core/config/feature-flags.ts`. Each entry is the flag's whole definition — name (kebab-case key), `description`, and the `fallback` secret consulted when AppConfig isn't the source of truth (truthy ⇒ on globally): + + ```ts + const FEATURE_FLAGS = { + '': { + description: '', + fallback: '', + }, + } + ``` + + `fallback` is the env/secret key (typed as `keyof typeof env`), so add `` to `apps/sim/lib/core/config/env.ts` first (and the deployment's secret store) — it won't typecheck otherwise. Do **not** add org/user/admin defaults here — that gating exists only in AppConfig. Adding the entry makes `` a valid `FeatureFlagName`. + +2. **Gate the call site.** Call `isFeatureEnabled` with whatever ids you have — admin status is resolved internally, so callers never pass it: + + ```ts + import { isFeatureEnabled } from '@/lib/core/config/feature-flags' + + if (await isFeatureEnabled('', { userId, orgId })) { + // gated behavior + } + ``` + + - Missing ids are fine — a clause with no matching id is skipped; with no `userId`, the admin clause resolves to `false` without a DB read. + - Admin routes that already know the caller is an admin may pass `{ userId, isAdmin: true }` to skip the role lookup. + - **Client/UI flags:** resolve server-side (in a server component, route, or loader) and pass the boolean down as a prop. There is no client AppConfig. + +3. **(Prod) configure in AppConfig.** The infra `feature-flags` profile schema is permissive, so a new flag needs **no infra change**. Operators add the flag under `flags` in the hosted `feature-flags` document — including any `orgIds`/`userIds`/`admins` gating — and start a `sim--fast` deployment (see the AppConfig runbook in the infra README — same flow as `access-control`). The fallback secret only applies when AppConfig is disabled. + +4. **Test.** Add a case to `apps/sim/lib/core/config/feature-flags.test.ts`: use `withAppConfig({ flags: { ... } })` to cover the gating rule (mock `isPlatformAdmin` for the `admins` clause), and toggle the fallback secret to cover the off-AppConfig path. + +5. **Clean up after rollout.** When the feature ships to everyone, delete the flag's entry from `FEATURE_FLAGS`, the `` env entry, the AppConfig document, the call sites, and the test. Leaving dead flags around is the main failure mode of flag systems. + +## Notes + +- Flag keys are `kebab-case`. +- Never read flags via raw `fetch` or a new AppConfig client — always go through `isFeatureEnabled` / `getFeatureFlags`. +- Never bake gating into code. The fallback is a single boolean secret; org/user/admin scoping is AppConfig-only. +- The admin check reads the DB **replica** (`dbReplica`) and is resolved lazily, so an admin-gated flag adds at most one cheap replica read, and only when `admins` is the deciding clause. diff --git a/.cursor/commands/add-feature-flag.md b/.cursor/commands/add-feature-flag.md new file mode 100644 index 0000000000..fc3dba41e4 --- /dev/null +++ b/.cursor/commands/add-feature-flag.md @@ -0,0 +1,67 @@ +# Add Feature Flag Skill + +You add a **runtime, gated feature flag** to Sim — one that can be turned on for specific orgs, users, or admins and changed on prod with no redeploy (AWS AppConfig). When AppConfig isn't the source of truth, the flag falls back to a single **secret** (on/off only). + +## When to use this vs `env-flags.ts` + +- **Feature flag** (`@/lib/core/config/feature-flags.ts`): per-request, gated by `userId`/`orgId`/admin, changeable at runtime. This skill. +- **Env flag** (`@/lib/core/config/env-flags.ts`): deploy-time capability/environment detection (`isProd`, `isHosted`, `isBillingEnabled`). A module-load boolean. **Do not add gated flags here.** + +If the user wants a fixed per-deployment toggle, send them to `env-flags.ts` instead. + +## The flag model + +A flag's **gating rule lives only in the hosted AppConfig document**. It is ON for a context when any clause matches: + +```ts +interface FeatureFlagRule { + enabled?: boolean // global default for everyone + orgIds?: string[] // allowlisted organization ids + userIds?: string[] // allowlisted user ids + admins?: boolean // platform admins (user.role === 'admin') +} +``` + +Critically, **none of this is expressible in code** — gating (especially `admins`) can only be set through AppConfig, so no environment can grant access from a code literal. Off-AppConfig (self-hosted/OSS/local), a flag is simply on or off, derived from its fallback secret. + +## Steps + +1. **Define the flag.** Add one entry to the `FEATURE_FLAGS` registry in `apps/sim/lib/core/config/feature-flags.ts`. Each entry is the flag's whole definition — name (kebab-case key), `description`, and the `fallback` secret consulted when AppConfig isn't the source of truth (truthy ⇒ on globally): + + ```ts + const FEATURE_FLAGS = { + '': { + description: '', + fallback: '', + }, + } + ``` + + `fallback` is the env/secret key (typed as `keyof typeof env`), so add `` to `apps/sim/lib/core/config/env.ts` first (and the deployment's secret store) — it won't typecheck otherwise. Do **not** add org/user/admin defaults here — that gating exists only in AppConfig. Adding the entry makes `` a valid `FeatureFlagName`. + +2. **Gate the call site.** Call `isFeatureEnabled` with whatever ids you have — admin status is resolved internally, so callers never pass it: + + ```ts + import { isFeatureEnabled } from '@/lib/core/config/feature-flags' + + if (await isFeatureEnabled('', { userId, orgId })) { + // gated behavior + } + ``` + + - Missing ids are fine — a clause with no matching id is skipped; with no `userId`, the admin clause resolves to `false` without a DB read. + - Admin routes that already know the caller is an admin may pass `{ userId, isAdmin: true }` to skip the role lookup. + - **Client/UI flags:** resolve server-side (in a server component, route, or loader) and pass the boolean down as a prop. There is no client AppConfig. + +3. **(Prod) configure in AppConfig.** The infra `feature-flags` profile schema is permissive, so a new flag needs **no infra change**. Operators add the flag under `flags` in the hosted `feature-flags` document — including any `orgIds`/`userIds`/`admins` gating — and start a `sim--fast` deployment (see the AppConfig runbook in the infra README — same flow as `access-control`). The fallback secret only applies when AppConfig is disabled. + +4. **Test.** Add a case to `apps/sim/lib/core/config/feature-flags.test.ts`: use `withAppConfig({ flags: { ... } })` to cover the gating rule (mock `isPlatformAdmin` for the `admins` clause), and toggle the fallback secret to cover the off-AppConfig path. + +5. **Clean up after rollout.** When the feature ships to everyone, delete the flag's entry from `FEATURE_FLAGS`, the `` env entry, the AppConfig document, the call sites, and the test. Leaving dead flags around is the main failure mode of flag systems. + +## Notes + +- Flag keys are `kebab-case`. +- Never read flags via raw `fetch` or a new AppConfig client — always go through `isFeatureEnabled` / `getFeatureFlags`. +- Never bake gating into code. The fallback is a single boolean secret; org/user/admin scoping is AppConfig-only. +- The admin check reads the DB **replica** (`dbReplica`) and is resolved lazily, so an admin-gated flag adds at most one cheap replica read, and only when `admins` is the deciding clause. diff --git a/.cursor/rules/sim-testing.mdc b/.cursor/rules/sim-testing.mdc index ffb2f85701..ca3ceb1e94 100644 --- a/.cursor/rules/sim-testing.mdc +++ b/.cursor/rules/sim-testing.mdc @@ -150,7 +150,7 @@ vi.useFakeTimers() | `@/lib/auth/hybrid` | `hybridAuthMock`, `hybridAuthMockFns` | `vi.mock('@/lib/auth/hybrid', () => hybridAuthMock)` | | `@/lib/copilot/request/http` | `copilotHttpMock`, `copilotHttpMockFns` | `vi.mock('@/lib/copilot/request/http', () => copilotHttpMock)` | | `@/lib/core/config/env` | `envMock`, `createEnvMock(overrides)` | `vi.mock('@/lib/core/config/env', () => envMock)` | -| `@/lib/core/config/feature-flags` | `featureFlagsMock` | `vi.mock('@/lib/core/config/feature-flags', () => featureFlagsMock)` | +| `@/lib/core/config/env-flags` | `featureFlagsMock` | `vi.mock('@/lib/core/config/env-flags', () => featureFlagsMock)` | | `@/lib/core/config/redis` | `redisConfigMock`, `redisConfigMockFns` | `vi.mock('@/lib/core/config/redis', () => redisConfigMock)` | | `@/lib/core/security/encryption` | `encryptionMock`, `encryptionMockFns` | `vi.mock('@/lib/core/security/encryption', () => encryptionMock)` | | `@/lib/core/security/input-validation.server` | `inputValidationMock`, `inputValidationMockFns` | `vi.mock('@/lib/core/security/input-validation.server', () => inputValidationMock)` | diff --git a/.github/workflows/test-build.yml b/.github/workflows/test-build.yml index f263bd7b78..a272f713f9 100644 --- a/.github/workflows/test-build.yml +++ b/.github/workflows/test-build.yml @@ -55,12 +55,12 @@ jobs: - name: Install dependencies run: bun install --frozen-lockfile - - name: Validate feature flags + - name: Validate env flags run: | - FILE="apps/sim/lib/core/config/feature-flags.ts" + FILE="apps/sim/lib/core/config/env-flags.ts" ERRORS="" - echo "Checking for hardcoded boolean feature flags..." + echo "Checking for hardcoded boolean env flags..." # Use perl for multiline matching to catch both: # export const isHosted = true @@ -69,17 +69,17 @@ jobs: HARDCODED=$(perl -0777 -ne 'while (/export const (is[A-Za-z]+)\s*=\s*\n?\s*(true|false)\b/g) { print " $1 = $2\n" }' "$FILE") if [ -n "$HARDCODED" ]; then - ERRORS="${ERRORS}\n❌ Feature flags must not be hardcoded to boolean literals!\n\nFound hardcoded flags:\n${HARDCODED}\n\nFeature flags should derive their values from environment variables.\n" + ERRORS="${ERRORS}\n❌ Env flags must not be hardcoded to boolean literals!\n\nFound hardcoded flags:\n${HARDCODED}\n\nEnv flags should derive their values from environment variables.\n" fi - echo "Checking feature flag naming conventions..." + echo "Checking env flag naming conventions..." # Check that all export const (except functions) start with 'is' # This finds exports like "export const someFlag" that don't start with "is" or "get" BAD_NAMES=$(grep -E "^export const [a-z]" "$FILE" | grep -vE "^export const (is|get)" | sed 's/export const \([a-zA-Z]*\).*/ \1/') if [ -n "$BAD_NAMES" ]; then - ERRORS="${ERRORS}\n❌ Feature flags must use 'is' prefix for boolean flags!\n\nFound incorrectly named flags:\n${BAD_NAMES}\n\nExample: 'hostedMode' should be 'isHostedMode'\n" + ERRORS="${ERRORS}\n❌ Env flags must use 'is' prefix for boolean flags!\n\nFound incorrectly named flags:\n${BAD_NAMES}\n\nExample: 'hostedMode' should be 'isHostedMode'\n" fi if [ -n "$ERRORS" ]; then @@ -88,7 +88,7 @@ jobs: exit 1 fi - echo "✅ All feature flags are properly configured" + echo "✅ All env flags are properly configured" - name: Check block registry invariants run: | diff --git a/apps/sim/app/(auth)/components/oauth-provider-checker.tsx b/apps/sim/app/(auth)/components/oauth-provider-checker.tsx index 218377f07d..ee2f4ede8a 100644 --- a/apps/sim/app/(auth)/components/oauth-provider-checker.tsx +++ b/apps/sim/app/(auth)/components/oauth-provider-checker.tsx @@ -4,7 +4,7 @@ import { isGoogleAuthDisabled, isMicrosoftAuthDisabled, isProd, -} from '@/lib/core/config/feature-flags' +} from '@/lib/core/config/env-flags' export async function getOAuthProviderStatus() { const githubAvailable = diff --git a/apps/sim/app/(auth)/signup/page.tsx b/apps/sim/app/(auth)/signup/page.tsx index b43f6ebad5..1fbd4cbfd2 100644 --- a/apps/sim/app/(auth)/signup/page.tsx +++ b/apps/sim/app/(auth)/signup/page.tsx @@ -1,5 +1,5 @@ import type { Metadata } from 'next' -import { isEmailSignupDisabled, isRegistrationDisabled } from '@/lib/core/config/feature-flags' +import { isEmailSignupDisabled, isRegistrationDisabled } from '@/lib/core/config/env-flags' import { getOAuthProviderStatus } from '@/app/(auth)/components/oauth-provider-checker' import SignupForm from '@/app/(auth)/signup/signup-form' diff --git a/apps/sim/app/(auth)/verify/page.tsx b/apps/sim/app/(auth)/verify/page.tsx index 70c5484144..c8825186d0 100644 --- a/apps/sim/app/(auth)/verify/page.tsx +++ b/apps/sim/app/(auth)/verify/page.tsx @@ -1,5 +1,5 @@ import type { Metadata } from 'next' -import { isEmailVerificationEnabled, isProd } from '@/lib/core/config/feature-flags' +import { isEmailVerificationEnabled, isProd } from '@/lib/core/config/env-flags' import { hasEmailService } from '@/lib/messaging/email/mailer' import { VerifyContent } from '@/app/(auth)/verify/verify-content' diff --git a/apps/sim/app/(landing)/components/legal-layout.tsx b/apps/sim/app/(landing)/components/legal-layout.tsx index 3f5ebdee8d..029bbff3fa 100644 --- a/apps/sim/app/(landing)/components/legal-layout.tsx +++ b/apps/sim/app/(landing)/components/legal-layout.tsx @@ -1,5 +1,5 @@ import { getNavBlogPosts } from '@/lib/blog/registry' -import { isHosted } from '@/lib/core/config/feature-flags' +import { isHosted } from '@/lib/core/config/env-flags' import Footer from '@/app/(landing)/components/footer/footer' import Navbar from '@/app/(landing)/components/navbar/navbar' diff --git a/apps/sim/app/(landing)/contact/page.tsx b/apps/sim/app/(landing)/contact/page.tsx index 6d2b00ef70..2cd9e4bcf1 100644 --- a/apps/sim/app/(landing)/contact/page.tsx +++ b/apps/sim/app/(landing)/contact/page.tsx @@ -1,6 +1,6 @@ import type { Metadata } from 'next' import { getNavBlogPosts } from '@/lib/blog/registry' -import { isHosted } from '@/lib/core/config/feature-flags' +import { isHosted } from '@/lib/core/config/env-flags' import { SITE_URL } from '@/lib/core/utils/urls' import { ContactForm } from '@/app/(landing)/components/contact/contact-form' import Footer from '@/app/(landing)/components/footer/footer' diff --git a/apps/sim/app/api/auth/[...all]/route.test.ts b/apps/sim/app/api/auth/[...all]/route.test.ts index f87f1a0167..0d82eac149 100644 --- a/apps/sim/app/api/auth/[...all]/route.test.ts +++ b/apps/sim/app/api/auth/[...all]/route.test.ts @@ -31,7 +31,7 @@ vi.mock('@/lib/auth/anonymous', () => ({ createAnonymousSession: handlerMocks.createAnonymousSession, })) -vi.mock('@/lib/core/config/feature-flags', () => ({ +vi.mock('@/lib/core/config/env-flags', () => ({ get isAuthDisabled() { return handlerMocks.isAuthDisabled }, diff --git a/apps/sim/app/api/auth/[...all]/route.ts b/apps/sim/app/api/auth/[...all]/route.ts index 6ff9bfd6db..8456afff4c 100644 --- a/apps/sim/app/api/auth/[...all]/route.ts +++ b/apps/sim/app/api/auth/[...all]/route.ts @@ -2,7 +2,7 @@ import { toNextJsHandler } from 'better-auth/next-js' import { type NextRequest, NextResponse } from 'next/server' import { auth } from '@/lib/auth' import { createAnonymousSession, ensureAnonymousUserExists } from '@/lib/auth/anonymous' -import { isAuthDisabled } from '@/lib/core/config/feature-flags' +import { isAuthDisabled } from '@/lib/core/config/env-flags' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' export const dynamic = 'force-dynamic' diff --git a/apps/sim/app/api/auth/providers/route.ts b/apps/sim/app/api/auth/providers/route.ts index 8828505600..015a59cdc1 100644 --- a/apps/sim/app/api/auth/providers/route.ts +++ b/apps/sim/app/api/auth/providers/route.ts @@ -2,7 +2,7 @@ import type { NextRequest } from 'next/server' import { NextResponse } from 'next/server' import { getAuthProvidersContract } from '@/lib/api/contracts/auth' import { parseRequest } from '@/lib/api/server' -import { isRegistrationDisabled } from '@/lib/core/config/feature-flags' +import { isRegistrationDisabled } from '@/lib/core/config/env-flags' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { getOAuthProviderStatus } from '@/app/(auth)/components/oauth-provider-checker' diff --git a/apps/sim/app/api/auth/socket-token/route.ts b/apps/sim/app/api/auth/socket-token/route.ts index c7b0dc618c..14fe571c1d 100644 --- a/apps/sim/app/api/auth/socket-token/route.ts +++ b/apps/sim/app/api/auth/socket-token/route.ts @@ -3,7 +3,7 @@ import { toError } from '@sim/utils/errors' import { headers } from 'next/headers' import { type NextRequest, NextResponse } from 'next/server' import { auth } from '@/lib/auth' -import { isAuthDisabled } from '@/lib/core/config/feature-flags' +import { isAuthDisabled } from '@/lib/core/config/env-flags' import { enforceIpRateLimit } from '@/lib/core/rate-limiter' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' diff --git a/apps/sim/app/api/billing/switch-plan/route.ts b/apps/sim/app/api/billing/switch-plan/route.ts index 33a9f40a08..c066afbedb 100644 --- a/apps/sim/app/api/billing/switch-plan/route.ts +++ b/apps/sim/app/api/billing/switch-plan/route.ts @@ -19,7 +19,7 @@ import { hasUsableSubscriptionStatus, isOrgScopedSubscription, } from '@/lib/billing/subscriptions/utils' -import { isBillingEnabled } from '@/lib/core/config/feature-flags' +import { isBillingEnabled } from '@/lib/core/config/env-flags' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { captureServerEvent } from '@/lib/posthog/server' diff --git a/apps/sim/app/api/billing/update-cost/route.test.ts b/apps/sim/app/api/billing/update-cost/route.test.ts index 1fb8f8c294..b2a2b1a37a 100644 --- a/apps/sim/app/api/billing/update-cost/route.test.ts +++ b/apps/sim/app/api/billing/update-cost/route.test.ts @@ -40,7 +40,7 @@ vi.mock('@/lib/billing/threshold-billing', () => ({ checkAndBillOverageThreshold: mockCheckAndBillOverageThreshold, })) -vi.mock('@/lib/core/config/feature-flags', () => ({ +vi.mock('@/lib/core/config/env-flags', () => ({ isBillingEnabled: true, })) diff --git a/apps/sim/app/api/billing/update-cost/route.ts b/apps/sim/app/api/billing/update-cost/route.ts index 92ccce1e8a..f2f65d482d 100644 --- a/apps/sim/app/api/billing/update-cost/route.ts +++ b/apps/sim/app/api/billing/update-cost/route.ts @@ -14,7 +14,7 @@ import { TraceAttr } from '@/lib/copilot/generated/trace-attributes-v1' import { TraceSpan } from '@/lib/copilot/generated/trace-spans-v1' import { checkInternalApiKey } from '@/lib/copilot/request/http' import { withIncomingGoSpan } from '@/lib/copilot/request/otel' -import { isBillingEnabled } from '@/lib/core/config/feature-flags' +import { isBillingEnabled } from '@/lib/core/config/env-flags' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' diff --git a/apps/sim/app/api/chat/manage/[id]/route.test.ts b/apps/sim/app/api/chat/manage/[id]/route.test.ts index e11b1b548e..38b2434816 100644 --- a/apps/sim/app/api/chat/manage/[id]/route.test.ts +++ b/apps/sim/app/api/chat/manage/[id]/route.test.ts @@ -33,7 +33,7 @@ const mockNotifySocketDeploymentChanged = workflowsOrchestrationMockFns.mockNotifySocketDeploymentChanged vi.mock('@sim/audit', () => auditMock) -vi.mock('@/lib/core/config/feature-flags', () => ({ +vi.mock('@/lib/core/config/env-flags', () => ({ isDev: true, isHosted: false, isProd: false, diff --git a/apps/sim/app/api/chat/manage/[id]/route.ts b/apps/sim/app/api/chat/manage/[id]/route.ts index 0115be7099..707051d7e9 100644 --- a/apps/sim/app/api/chat/manage/[id]/route.ts +++ b/apps/sim/app/api/chat/manage/[id]/route.ts @@ -8,7 +8,7 @@ import type { NextRequest } from 'next/server' import { chatIdParamsSchema, updateChatContract } from '@/lib/api/contracts/chats' import { getValidationErrorMessage, parseRequest } from '@/lib/api/server' import { getSession } from '@/lib/auth' -import { isDev } from '@/lib/core/config/feature-flags' +import { isDev } from '@/lib/core/config/env-flags' import { encryptSecret } from '@/lib/core/security/encryption' import { getEmailDomain } from '@/lib/core/utils/urls' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' diff --git a/apps/sim/app/api/chat/utils.test.ts b/apps/sim/app/api/chat/utils.test.ts index 539ba6c6e0..0f0a5b6406 100644 --- a/apps/sim/app/api/chat/utils.test.ts +++ b/apps/sim/app/api/chat/utils.test.ts @@ -78,7 +78,7 @@ vi.mock('@/lib/core/security/deployment', () => ({ isEmailAllowed: mockIsEmailAllowed, })) -vi.mock('@/lib/core/config/feature-flags', () => ({ +vi.mock('@/lib/core/config/env-flags', () => ({ isDev: true, isProd: false, get isBillingEnabled() { diff --git a/apps/sim/app/api/chat/utils.ts b/apps/sim/app/api/chat/utils.ts index 6d01d6fa67..49c5f17064 100644 --- a/apps/sim/app/api/chat/utils.ts +++ b/apps/sim/app/api/chat/utils.ts @@ -7,7 +7,7 @@ import { and, eq, isNull } from 'drizzle-orm' import type { NextRequest, NextResponse } from 'next/server' import { isWorkspaceApiExecutionEntitled } from '@/lib/billing/core/api-access' import { getEnv } from '@/lib/core/config/env' -import { isBillingEnabled, isFreeApiDeploymentGateEnabled } from '@/lib/core/config/feature-flags' +import { isBillingEnabled, isFreeApiDeploymentGateEnabled } from '@/lib/core/config/env-flags' import type { TokenBucketConfig } from '@/lib/core/rate-limiter' import { RateLimiter } from '@/lib/core/rate-limiter' import { diff --git a/apps/sim/app/api/copilot/api-keys/validate/route.test.ts b/apps/sim/app/api/copilot/api-keys/validate/route.test.ts index 0e410018d6..33eabf8645 100644 --- a/apps/sim/app/api/copilot/api-keys/validate/route.test.ts +++ b/apps/sim/app/api/copilot/api-keys/validate/route.test.ts @@ -42,7 +42,7 @@ vi.mock('@/lib/copilot/request/otel', () => ({ ) => fn({ setAttribute: vi.fn(), setAttributes: vi.fn() }), })) -vi.mock('@/lib/core/config/feature-flags', () => ({ +vi.mock('@/lib/core/config/env-flags', () => ({ get isHosted() { return mockFlags.isHosted }, diff --git a/apps/sim/app/api/copilot/api-keys/validate/route.ts b/apps/sim/app/api/copilot/api-keys/validate/route.ts index c011f663ed..b13d2b1177 100644 --- a/apps/sim/app/api/copilot/api-keys/validate/route.ts +++ b/apps/sim/app/api/copilot/api-keys/validate/route.ts @@ -14,7 +14,7 @@ import { TraceAttr } from '@/lib/copilot/generated/trace-attributes-v1' import { TraceSpan } from '@/lib/copilot/generated/trace-spans-v1' import { checkInternalApiKey } from '@/lib/copilot/request/http' import { withIncomingGoSpan } from '@/lib/copilot/request/otel' -import { isHosted } from '@/lib/core/config/feature-flags' +import { isHosted } from '@/lib/core/config/env-flags' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' const logger = createLogger('CopilotApiKeysValidate') diff --git a/apps/sim/app/api/cron/run-data-drains/route.ts b/apps/sim/app/api/cron/run-data-drains/route.ts index 939d75419a..61416bb8f6 100644 --- a/apps/sim/app/api/cron/run-data-drains/route.ts +++ b/apps/sim/app/api/cron/run-data-drains/route.ts @@ -2,7 +2,7 @@ import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' import { type NextRequest, NextResponse } from 'next/server' import { verifyCronAuth } from '@/lib/auth/internal' -import { isBillingEnabled, isDataDrainsEnabled } from '@/lib/core/config/feature-flags' +import { isBillingEnabled, isDataDrainsEnabled } from '@/lib/core/config/env-flags' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { dispatchDueDrains } from '@/lib/data-drains/dispatcher' diff --git a/apps/sim/app/api/files/serve/[...path]/route.ts b/apps/sim/app/api/files/serve/[...path]/route.ts index 03de2e41c4..3aea989d03 100644 --- a/apps/sim/app/api/files/serve/[...path]/route.ts +++ b/apps/sim/app/api/files/serve/[...path]/route.ts @@ -10,7 +10,7 @@ import { getE2BDocFormat, loadCompiledDocByExt, } from '@/lib/copilot/tools/server/files/doc-compile' -import { isE2BDocEnabled } from '@/lib/core/config/feature-flags' +import { isE2BDocEnabled } from '@/lib/core/config/env-flags' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { runSandboxTask } from '@/lib/execution/sandbox/run-task' import { CopilotFiles, isUsingCloudStorage } from '@/lib/uploads' diff --git a/apps/sim/app/api/function/execute/route.test.ts b/apps/sim/app/api/function/execute/route.test.ts index 958361bfb7..f14f6a1bfa 100644 --- a/apps/sim/app/api/function/execute/route.test.ts +++ b/apps/sim/app/api/function/execute/route.test.ts @@ -5,7 +5,7 @@ */ import { createMockRequest, - featureFlagsMock, + envFlagsMock, hybridAuthMockFns, workflowsUtilsMock, } from '@sim/testing' @@ -94,7 +94,7 @@ vi.mock('@/lib/uploads', () => ({ vi.mock('@/lib/workflows/utils', () => workflowsUtilsMock) -vi.mock('@/lib/core/config/feature-flags', () => featureFlagsMock) +vi.mock('@/lib/core/config/env-flags', () => envFlagsMock) import { validateProxyUrl } from '@/lib/core/security/input-validation' import { clearLargeValueCacheForTests } from '@/lib/execution/payloads/cache' @@ -105,7 +105,7 @@ import { POST } from '@/app/api/function/execute/route' describe('Function Execute API Route', () => { beforeEach(() => { vi.clearAllMocks() - featureFlagsMock.isE2bEnabled = false + envFlagsMock.isE2bEnabled = false hybridAuthMockFns.mockCheckInternalAuth.mockResolvedValue({ success: true, @@ -341,7 +341,7 @@ describe('Function Execute API Route', () => { }) it('exports multiple declared sandbox output files', async () => { - featureFlagsMock.isE2bEnabled = true + envFlagsMock.isE2bEnabled = true mockExecuteInE2B.mockResolvedValueOnce({ result: 'done', stdout: 'ok', @@ -409,7 +409,7 @@ describe('Function Execute API Route', () => { }) it('prevalidates all sandbox output destinations before writing any files', async () => { - featureFlagsMock.isE2bEnabled = true + envFlagsMock.isE2bEnabled = true mockExecuteInE2B.mockResolvedValueOnce({ result: 'done', stdout: 'ok', @@ -453,7 +453,7 @@ describe('Function Execute API Route', () => { }) it('rejects duplicate sandbox output destinations before writing files', async () => { - featureFlagsMock.isE2bEnabled = true + envFlagsMock.isE2bEnabled = true mockExecuteInE2B.mockResolvedValueOnce({ result: 'done', stdout: 'ok', @@ -498,7 +498,7 @@ describe('Function Execute API Route', () => { }) it('returns a targeted error when a declared sandbox output is missing', async () => { - featureFlagsMock.isE2bEnabled = true + envFlagsMock.isE2bEnabled = true mockExecuteInE2B.mockResolvedValueOnce({ result: 'done', stdout: 'ok', @@ -571,7 +571,7 @@ describe('Function Execute API Route', () => { }) it('rejects large refs in runtimes without ref-native helpers', async () => { - featureFlagsMock.isE2bEnabled = true + envFlagsMock.isE2bEnabled = true const req = createMockRequest('POST', { code: 'echo "$__blockRef_0"', language: 'shell', diff --git a/apps/sim/app/api/function/execute/route.ts b/apps/sim/app/api/function/execute/route.ts index 9ac6ab2da7..6f04cfaf07 100644 --- a/apps/sim/app/api/function/execute/route.ts +++ b/apps/sim/app/api/function/execute/route.ts @@ -14,7 +14,7 @@ import { validateWorkspaceFileWriteTarget, writeWorkspaceFileByPath, } from '@/lib/copilot/vfs/resource-writer' -import { isE2bEnabled } from '@/lib/core/config/feature-flags' +import { isE2bEnabled } from '@/lib/core/config/env-flags' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { executeInE2B, executeShellInE2B } from '@/lib/execution/e2b' diff --git a/apps/sim/app/api/mothership/execute/route.ts b/apps/sim/app/api/mothership/execute/route.ts index 6f022a7c0d..c97a394c18 100644 --- a/apps/sim/app/api/mothership/execute/route.ts +++ b/apps/sim/app/api/mothership/execute/route.ts @@ -15,7 +15,7 @@ import { import { runHeadlessCopilotLifecycle } from '@/lib/copilot/request/lifecycle/headless' import { requestExplicitStreamAbort } from '@/lib/copilot/request/session/explicit-abort' import type { StreamEvent } from '@/lib/copilot/request/types' -import { isE2BDocEnabled } from '@/lib/core/config/feature-flags' +import { isE2BDocEnabled } from '@/lib/core/config/env-flags' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { buildUserSkillTool } from '@/lib/mothership/skills' import { diff --git a/apps/sim/app/api/organizations/[id]/data-retention/route.ts b/apps/sim/app/api/organizations/[id]/data-retention/route.ts index ea2ae9c6ac..65e291a00d 100644 --- a/apps/sim/app/api/organizations/[id]/data-retention/route.ts +++ b/apps/sim/app/api/organizations/[id]/data-retention/route.ts @@ -12,7 +12,7 @@ import { type OrganizationRetentionSettings, } from '@/lib/billing/cleanup-dispatcher' import { isOrganizationOnEnterprisePlan } from '@/lib/billing/core/subscription' -import { isBillingEnabled } from '@/lib/core/config/feature-flags' +import { isBillingEnabled } from '@/lib/core/config/env-flags' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' const logger = createLogger('DataRetentionAPI') diff --git a/apps/sim/app/api/organizations/[id]/members/[memberId]/usage-limit/route.test.ts b/apps/sim/app/api/organizations/[id]/members/[memberId]/usage-limit/route.test.ts index ec4669e954..e92f8e1ce6 100644 --- a/apps/sim/app/api/organizations/[id]/members/[memberId]/usage-limit/route.test.ts +++ b/apps/sim/app/api/organizations/[id]/members/[memberId]/usage-limit/route.test.ts @@ -37,7 +37,7 @@ vi.mock('@/lib/billing/organizations/member-limits', () => ({ setOrgMemberUsageLimit: mockSetOrgMemberUsageLimit, })) -vi.mock('@/lib/core/config/feature-flags', () => ({ +vi.mock('@/lib/core/config/env-flags', () => ({ get isHosted() { return mockFlags.isHosted }, diff --git a/apps/sim/app/api/organizations/[id]/members/[memberId]/usage-limit/route.ts b/apps/sim/app/api/organizations/[id]/members/[memberId]/usage-limit/route.ts index ad04ae4bbf..ad85d7ef83 100644 --- a/apps/sim/app/api/organizations/[id]/members/[memberId]/usage-limit/route.ts +++ b/apps/sim/app/api/organizations/[id]/members/[memberId]/usage-limit/route.ts @@ -14,7 +14,7 @@ import { getOrgMemberWorkspaceUsage, setOrgMemberUsageLimit, } from '@/lib/billing/organizations/member-limits' -import { isHosted } from '@/lib/core/config/feature-flags' +import { isHosted } from '@/lib/core/config/env-flags' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' const logger = createLogger('OrgMemberUsageLimitAPI') diff --git a/apps/sim/app/api/schedules/execute/route.test.ts b/apps/sim/app/api/schedules/execute/route.test.ts index 2d0fa95ff5..7b61e3198a 100644 --- a/apps/sim/app/api/schedules/execute/route.test.ts +++ b/apps/sim/app/api/schedules/execute/route.test.ts @@ -61,7 +61,7 @@ vi.mock('@/background/schedule-execution', () => ({ }), })) -vi.mock('@/lib/core/config/feature-flags', () => mockFeatureFlags) +vi.mock('@/lib/core/config/env-flags', () => mockFeatureFlags) vi.mock('@/lib/core/async-jobs', () => ({ getJobQueue: vi.fn().mockResolvedValue({ diff --git a/apps/sim/app/api/settings/allowed-integrations/route.ts b/apps/sim/app/api/settings/allowed-integrations/route.ts index 7f4a45dada..19da51361c 100644 --- a/apps/sim/app/api/settings/allowed-integrations/route.ts +++ b/apps/sim/app/api/settings/allowed-integrations/route.ts @@ -1,6 +1,6 @@ import { NextResponse } from 'next/server' import { getSession } from '@/lib/auth' -import { getAllowedIntegrationsFromEnv } from '@/lib/core/config/feature-flags' +import { getAllowedIntegrationsFromEnv } from '@/lib/core/config/env-flags' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' export const GET = withRouteHandler(async () => { diff --git a/apps/sim/app/api/settings/allowed-mcp-domains/route.ts b/apps/sim/app/api/settings/allowed-mcp-domains/route.ts index 207eb70616..54f9887158 100644 --- a/apps/sim/app/api/settings/allowed-mcp-domains/route.ts +++ b/apps/sim/app/api/settings/allowed-mcp-domains/route.ts @@ -1,6 +1,6 @@ import { NextResponse } from 'next/server' import { getSession } from '@/lib/auth' -import { getAllowedMcpDomainsFromEnv } from '@/lib/core/config/feature-flags' +import { getAllowedMcpDomainsFromEnv } from '@/lib/core/config/env-flags' import { getBaseUrl } from '@/lib/core/utils/urls' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' diff --git a/apps/sim/app/api/settings/allowed-providers/route.ts b/apps/sim/app/api/settings/allowed-providers/route.ts index 81b0b66b11..7a4d5ce4d8 100644 --- a/apps/sim/app/api/settings/allowed-providers/route.ts +++ b/apps/sim/app/api/settings/allowed-providers/route.ts @@ -1,6 +1,6 @@ import { NextResponse } from 'next/server' import { getSession } from '@/lib/auth' -import { getBlacklistedProvidersFromEnv } from '@/lib/core/config/feature-flags' +import { getBlacklistedProvidersFromEnv } from '@/lib/core/config/env-flags' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' export const GET = withRouteHandler(async () => { diff --git a/apps/sim/app/api/speech/token/route.test.ts b/apps/sim/app/api/speech/token/route.test.ts index a318172983..8c00a1c778 100644 --- a/apps/sim/app/api/speech/token/route.test.ts +++ b/apps/sim/app/api/speech/token/route.test.ts @@ -51,7 +51,7 @@ vi.mock('@/app/api/workflows/utils', () => ({ vi.mock('@/lib/core/config/env', () => ({ env: { ELEVENLABS_API_KEY: 'test-key' } })) -vi.mock('@/lib/core/config/feature-flags', () => ({ +vi.mock('@/lib/core/config/env-flags', () => ({ isBillingEnabled: false, getCostMultiplier: () => 1, })) diff --git a/apps/sim/app/api/speech/token/route.ts b/apps/sim/app/api/speech/token/route.ts index 116cb5f822..e9e7dfa631 100644 --- a/apps/sim/app/api/speech/token/route.ts +++ b/apps/sim/app/api/speech/token/route.ts @@ -10,7 +10,7 @@ import { getSession } from '@/lib/auth' import { checkActorUsageLimits } from '@/lib/billing/calculations/usage-monitor' import { recordUsage } from '@/lib/billing/core/usage-log' import { env } from '@/lib/core/config/env' -import { getCostMultiplier, isBillingEnabled } from '@/lib/core/config/feature-flags' +import { getCostMultiplier, isBillingEnabled } from '@/lib/core/config/env-flags' import { RateLimiter } from '@/lib/core/rate-limiter' import { validateAuthToken } from '@/lib/core/security/deployment' import { getClientIp } from '@/lib/core/utils/request' diff --git a/apps/sim/app/api/table/[tableId]/delete-async/route.test.ts b/apps/sim/app/api/table/[tableId]/delete-async/route.test.ts index 3ebdd6a768..d6b0c17c4f 100644 --- a/apps/sim/app/api/table/[tableId]/delete-async/route.test.ts +++ b/apps/sim/app/api/table/[tableId]/delete-async/route.test.ts @@ -33,7 +33,7 @@ vi.mock('@/lib/table/jobs/service', () => ({ releaseJobClaim: mockReleaseJobClaim, })) vi.mock('@/lib/table/delete-runner', () => ({ runTableDelete: mockRunTableDelete })) -vi.mock('@/lib/core/config/feature-flags', () => ({ +vi.mock('@/lib/core/config/env-flags', () => ({ get isTriggerDevEnabled() { return flags.triggerDev }, diff --git a/apps/sim/app/api/table/[tableId]/delete-async/route.ts b/apps/sim/app/api/table/[tableId]/delete-async/route.ts index a7ab365fee..92d3106669 100644 --- a/apps/sim/app/api/table/[tableId]/delete-async/route.ts +++ b/apps/sim/app/api/table/[tableId]/delete-async/route.ts @@ -4,7 +4,7 @@ import { type NextRequest, NextResponse } from 'next/server' import { deleteTableRowsAsyncContract } from '@/lib/api/contracts/tables' import { parseRequest } from '@/lib/api/server' import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' -import { isTriggerDevEnabled } from '@/lib/core/config/feature-flags' +import { isTriggerDevEnabled } from '@/lib/core/config/env-flags' import { runDetached } from '@/lib/core/utils/background' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' diff --git a/apps/sim/app/api/table/[tableId]/export-async/route.ts b/apps/sim/app/api/table/[tableId]/export-async/route.ts index 1218af5c80..0403e62647 100644 --- a/apps/sim/app/api/table/[tableId]/export-async/route.ts +++ b/apps/sim/app/api/table/[tableId]/export-async/route.ts @@ -4,7 +4,7 @@ import { type NextRequest, NextResponse } from 'next/server' import { exportTableAsyncContract } from '@/lib/api/contracts/tables' import { parseRequest } from '@/lib/api/server' import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' -import { isTriggerDevEnabled } from '@/lib/core/config/feature-flags' +import { isTriggerDevEnabled } from '@/lib/core/config/env-flags' import { runDetached } from '@/lib/core/utils/background' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' diff --git a/apps/sim/app/api/table/[tableId]/import-async/route.ts b/apps/sim/app/api/table/[tableId]/import-async/route.ts index ba3787a4fb..72b0fa28fd 100644 --- a/apps/sim/app/api/table/[tableId]/import-async/route.ts +++ b/apps/sim/app/api/table/[tableId]/import-async/route.ts @@ -4,7 +4,7 @@ import { type NextRequest, NextResponse } from 'next/server' import { importIntoTableAsyncContract } from '@/lib/api/contracts/tables' import { parseRequest } from '@/lib/api/server' import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' -import { isTriggerDevEnabled } from '@/lib/core/config/feature-flags' +import { isTriggerDevEnabled } from '@/lib/core/config/env-flags' import { runDetached } from '@/lib/core/utils/background' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' diff --git a/apps/sim/app/api/table/import-async/route.ts b/apps/sim/app/api/table/import-async/route.ts index f10b822b6e..0d5b6a418a 100644 --- a/apps/sim/app/api/table/import-async/route.ts +++ b/apps/sim/app/api/table/import-async/route.ts @@ -4,7 +4,7 @@ import { type NextRequest, NextResponse } from 'next/server' import { importTableAsyncContract } from '@/lib/api/contracts/tables' import { parseRequest } from '@/lib/api/server' import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' -import { isTriggerDevEnabled } from '@/lib/core/config/feature-flags' +import { isTriggerDevEnabled } from '@/lib/core/config/env-flags' import { runDetached } from '@/lib/core/utils/background' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' diff --git a/apps/sim/app/api/telemetry/route.ts b/apps/sim/app/api/telemetry/route.ts index aed019188c..b1a80962c4 100644 --- a/apps/sim/app/api/telemetry/route.ts +++ b/apps/sim/app/api/telemetry/route.ts @@ -3,7 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server' import { telemetryContract } from '@/lib/api/contracts/telemetry' import { parseRequest } from '@/lib/api/server' import { env } from '@/lib/core/config/env' -import { isProd } from '@/lib/core/config/feature-flags' +import { isProd } from '@/lib/core/config/env-flags' import { enforceIpRateLimit } from '@/lib/core/rate-limiter' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' diff --git a/apps/sim/app/api/tools/onepassword/utils.test.ts b/apps/sim/app/api/tools/onepassword/utils.test.ts index 504ed46b05..4c07ede132 100644 --- a/apps/sim/app/api/tools/onepassword/utils.test.ts +++ b/apps/sim/app/api/tools/onepassword/utils.test.ts @@ -8,7 +8,7 @@ const { mockDnsLookup, hostedFlag } = vi.hoisted(() => ({ hostedFlag: { value: false }, })) -vi.mock('@/lib/core/config/feature-flags', () => ({ +vi.mock('@/lib/core/config/env-flags', () => ({ get isHosted() { return hostedFlag.value }, diff --git a/apps/sim/app/api/tools/onepassword/utils.ts b/apps/sim/app/api/tools/onepassword/utils.ts index 4dcee71696..87c5e090da 100644 --- a/apps/sim/app/api/tools/onepassword/utils.ts +++ b/apps/sim/app/api/tools/onepassword/utils.ts @@ -12,7 +12,7 @@ import type { import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' import * as ipaddr from 'ipaddr.js' -import { isHosted } from '@/lib/core/config/feature-flags' +import { isHosted } from '@/lib/core/config/env-flags' import { isPrivateOrReservedIP, secureFetchWithPinnedIP, diff --git a/apps/sim/app/api/v1/admin/organizations/[id]/billing/route.ts b/apps/sim/app/api/v1/admin/organizations/[id]/billing/route.ts index c19168b7a6..69b773accf 100644 --- a/apps/sim/app/api/v1/admin/organizations/[id]/billing/route.ts +++ b/apps/sim/app/api/v1/admin/organizations/[id]/billing/route.ts @@ -25,7 +25,7 @@ import { } from '@/lib/api/contracts/v1/admin' import { parseRequest } from '@/lib/api/server' import { getOrganizationBillingData } from '@/lib/billing/core/organization' -import { isBillingEnabled } from '@/lib/core/config/feature-flags' +import { isBillingEnabled } from '@/lib/core/config/env-flags' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { withAdminAuthParams } from '@/app/api/v1/admin/middleware' import { diff --git a/apps/sim/app/api/v1/admin/organizations/[id]/members/[memberId]/route.ts b/apps/sim/app/api/v1/admin/organizations/[id]/members/[memberId]/route.ts index 6bb367feb2..0f25618fc2 100644 --- a/apps/sim/app/api/v1/admin/organizations/[id]/members/[memberId]/route.ts +++ b/apps/sim/app/api/v1/admin/organizations/[id]/members/[memberId]/route.ts @@ -40,7 +40,7 @@ import { removeUserFromOrganization, WORKSPACE_BILLING_ACCOUNT_REMOVAL_ERROR, } from '@/lib/billing/organizations/membership' -import { isBillingEnabled } from '@/lib/core/config/feature-flags' +import { isBillingEnabled } from '@/lib/core/config/env-flags' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { withAdminAuthParams } from '@/app/api/v1/admin/middleware' import { diff --git a/apps/sim/app/api/v1/admin/organizations/[id]/members/route.ts b/apps/sim/app/api/v1/admin/organizations/[id]/members/route.ts index 5c4466d182..99e6d2c791 100644 --- a/apps/sim/app/api/v1/admin/organizations/[id]/members/route.ts +++ b/apps/sim/app/api/v1/admin/organizations/[id]/members/route.ts @@ -39,7 +39,7 @@ import { import { parseRequest } from '@/lib/api/server' import { getOrgMemberLedgerByUser } from '@/lib/billing/core/organization' import { addUserToOrganization } from '@/lib/billing/organizations/membership' -import { isBillingEnabled } from '@/lib/core/config/feature-flags' +import { isBillingEnabled } from '@/lib/core/config/env-flags' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { withAdminAuthParams } from '@/app/api/v1/admin/middleware' import { diff --git a/apps/sim/app/api/v1/auth.ts b/apps/sim/app/api/v1/auth.ts index ce288dd676..0f39188900 100644 --- a/apps/sim/app/api/v1/auth.ts +++ b/apps/sim/app/api/v1/auth.ts @@ -2,7 +2,7 @@ import { createLogger } from '@sim/logger' import type { NextRequest } from 'next/server' import { authenticateApiKeyFromHeader, updateApiKeyLastUsed } from '@/lib/api-key/service' import { ANONYMOUS_USER_ID } from '@/lib/auth/constants' -import { isAuthDisabled } from '@/lib/core/config/feature-flags' +import { isAuthDisabled } from '@/lib/core/config/env-flags' const logger = createLogger('V1Auth') diff --git a/apps/sim/app/api/wand/route.ts b/apps/sim/app/api/wand/route.ts index 160d55171e..fae011a480 100644 --- a/apps/sim/app/api/wand/route.ts +++ b/apps/sim/app/api/wand/route.ts @@ -14,7 +14,7 @@ import { import { recordUsage } from '@/lib/billing/core/usage-log' import { checkAndBillOverageThreshold } from '@/lib/billing/threshold-billing' import { env } from '@/lib/core/config/env' -import { getCostMultiplier, isBillingEnabled } from '@/lib/core/config/feature-flags' +import { getCostMultiplier, isBillingEnabled } from '@/lib/core/config/env-flags' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { enrichTableSchema } from '@/lib/table/llm/wand' diff --git a/apps/sim/app/api/webhooks/agentmail/route.ts b/apps/sim/app/api/webhooks/agentmail/route.ts index b7aeb093c7..a38cf549ba 100644 --- a/apps/sim/app/api/webhooks/agentmail/route.ts +++ b/apps/sim/app/api/webhooks/agentmail/route.ts @@ -19,7 +19,7 @@ import { agentMailMessageSchema, webhookSvixHeadersSchema, } from '@/lib/api/contracts/webhooks' -import { isTriggerDevEnabled } from '@/lib/core/config/feature-flags' +import { isTriggerDevEnabled } from '@/lib/core/config/env-flags' import { assertContentLengthWithinLimit, isPayloadSizeLimitError, diff --git a/apps/sim/app/api/workspaces/[id]/files/[fileId]/compiled-check/route.ts b/apps/sim/app/api/workspaces/[id]/files/[fileId]/compiled-check/route.ts index f5952ecfe7..c8f7d6299e 100644 --- a/apps/sim/app/api/workspaces/[id]/files/[fileId]/compiled-check/route.ts +++ b/apps/sim/app/api/workspaces/[id]/files/[fileId]/compiled-check/route.ts @@ -6,7 +6,7 @@ import { parseRequest } from '@/lib/api/server' import { getSession } from '@/lib/auth' import { getE2BDocFormat } from '@/lib/copilot/tools/server/files/doc-compile' import { runE2BCompiledCheck } from '@/lib/copilot/tools/server/files/doc-recalc' -import { isE2BDocEnabled } from '@/lib/core/config/feature-flags' +import { isE2BDocEnabled } from '@/lib/core/config/env-flags' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' import { BINARY_DOC_TASKS, MAX_DOCUMENT_PREVIEW_CODE_BYTES } from '@/lib/execution/constants' import { runSandboxTask, SandboxUserCodeError } from '@/lib/execution/sandbox/run-task' diff --git a/apps/sim/app/chat/components/message/components/file-download.test.tsx b/apps/sim/app/chat/components/message/components/file-download.test.tsx index 6d48be1e2b..a707bbd55b 100644 --- a/apps/sim/app/chat/components/message/components/file-download.test.tsx +++ b/apps/sim/app/chat/components/message/components/file-download.test.tsx @@ -19,7 +19,7 @@ vi.mock('@/lib/core/config/env', () => ({ getEnv: vi.fn(), })) -vi.mock('@/lib/core/config/feature-flags', () => ({ +vi.mock('@/lib/core/config/env-flags', () => ({ isProd: false, })) diff --git a/apps/sim/app/layout.tsx b/apps/sim/app/layout.tsx index 8c20a3e8b4..82e6f107b7 100644 --- a/apps/sim/app/layout.tsx +++ b/apps/sim/app/layout.tsx @@ -5,7 +5,7 @@ import { BrandedLayout } from '@/components/branded-layout' import { PostHogProvider } from '@/app/_shell/providers/posthog-provider' import { generateBrandedMetadata, generateThemeCSS } from '@/ee/whitelabeling' import '@/app/_styles/globals.css' -import { isHosted, isReactGrabEnabled, isReactScanEnabled } from '@/lib/core/config/feature-flags' +import { isHosted, isReactGrabEnabled, isReactScanEnabled } from '@/lib/core/config/env-flags' import { HydrationErrorHandler } from '@/app/_shell/hydration-error-handler' import { QueryProvider } from '@/app/_shell/providers/query-provider' import { SessionProvider } from '@/app/_shell/providers/session-provider' diff --git a/apps/sim/app/workspace/[workspaceId]/settings/[section]/page.tsx b/apps/sim/app/workspace/[workspaceId]/settings/[section]/page.tsx index 650391cf5d..88a3ec5f51 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/[section]/page.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/[section]/page.tsx @@ -2,7 +2,7 @@ import { Suspense } from 'react' import { dehydrate, HydrationBoundary } from '@tanstack/react-query' import type { Metadata } from 'next' import { redirect } from 'next/navigation' -import { isBillingEnabled } from '@/lib/core/config/feature-flags' +import { isBillingEnabled } from '@/lib/core/config/env-flags' import { getQueryClient } from '@/app/_shell/providers/get-query-client' import type { SettingsSection } from '@/app/workspace/[workspaceId]/settings/navigation' import { prefetchGeneralSettings, prefetchSubscriptionData, prefetchUserProfile } from './prefetch' diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/general/general.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/general/general.tsx index bce32a2995..6a824dfc39 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/general/general.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/general/general.tsx @@ -25,7 +25,7 @@ import { telemetryContract } from '@/lib/api/contracts/telemetry' import { signOut, useSession } from '@/lib/auth/auth-client' import { ANONYMOUS_USER_ID } from '@/lib/auth/constants' import { getEnv, isTruthy } from '@/lib/core/config/env' -import { isHosted } from '@/lib/core/config/feature-flags' +import { isHosted } from '@/lib/core/config/env-flags' import { handleKeyboardActivation } from '@/lib/core/utils/keyboard' import { getBrowserTimezone, getTimezoneOptions } from '@/lib/core/utils/timezone' import { getBaseUrl } from '@/lib/core/utils/urls' diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/new-column-dropdown/new-column-dropdown.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/new-column-dropdown/new-column-dropdown.tsx index 50c240a13c..7d5afba2de 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/new-column-dropdown/new-column-dropdown.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/new-column-dropdown/new-column-dropdown.tsx @@ -13,7 +13,7 @@ import { DropdownMenuTrigger, Plus, } from '@/components/emcn' -import { isWorkflowColumnsEnabledClient } from '@/lib/core/config/feature-flags' +import { isWorkflowColumnsEnabledClient } from '@/lib/core/config/env-flags' import type { ColumnDefinition } from '@/lib/table' import { COLUMN_TYPE_OPTIONS } from '../column-config-sidebar' diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-sidebar/settings-sidebar.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-sidebar/settings-sidebar.tsx index 13d86154e2..6c70c0bcf5 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-sidebar/settings-sidebar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-sidebar/settings-sidebar.tsx @@ -7,7 +7,7 @@ import { ChevronDown, ChipConfirmModal, chipVariants } from '@/components/emcn' import { useSession } from '@/lib/auth/auth-client' import { getSubscriptionAccessState } from '@/lib/billing/client' import { isEnterprise } from '@/lib/billing/plan-helpers' -import { isHosted } from '@/lib/core/config/feature-flags' +import { isHosted } from '@/lib/core/config/env-flags' import { cn } from '@/lib/core/utils/cn' import { getUserRole } from '@/lib/workspaces/organization' import type { SettingsSection } from '@/app/workspace/[workspaceId]/settings/navigation' diff --git a/apps/sim/blocks/utils.test.ts b/apps/sim/blocks/utils.test.ts index 3148e64673..85b2e27396 100644 --- a/apps/sim/blocks/utils.test.ts +++ b/apps/sim/blocks/utils.test.ts @@ -34,7 +34,7 @@ const { mockProviders } = vi.hoisted(() => ({ }, })) -vi.mock('@/lib/core/config/feature-flags', () => ({ +vi.mock('@/lib/core/config/env-flags', () => ({ get isHosted() { return mockIsHosted.value }, diff --git a/apps/sim/blocks/utils.ts b/apps/sim/blocks/utils.ts index 4b1d0b556e..861c0b12de 100644 --- a/apps/sim/blocks/utils.ts +++ b/apps/sim/blocks/utils.ts @@ -4,7 +4,7 @@ import { isCohereConfigured, isHosted, isOllamaConfigured, -} from '@/lib/core/config/feature-flags' +} from '@/lib/core/config/env-flags' import { getScopesForService } from '@/lib/oauth/utils' import { buildCanonicalIndex } from '@/lib/workflows/subblocks/visibility' import type { BlockOutput, OutputFieldDefinition, SubBlockConfig } from '@/blocks/types' diff --git a/apps/sim/components/emails/components/email-footer.tsx b/apps/sim/components/emails/components/email-footer.tsx index ee0a46209d..610072b4d9 100644 --- a/apps/sim/components/emails/components/email-footer.tsx +++ b/apps/sim/components/emails/components/email-footer.tsx @@ -1,6 +1,6 @@ import { Container, Img, Link, Section } from '@react-email/components' import { baseStyles, colors, spacing, typography } from '@/components/emails/_styles' -import { isHosted } from '@/lib/core/config/feature-flags' +import { isHosted } from '@/lib/core/config/env-flags' import { getBaseUrl } from '@/lib/core/utils/urls' import { getBrandConfig } from '@/ee/whitelabeling' diff --git a/apps/sim/connectors/s3/s3.ts b/apps/sim/connectors/s3/s3.ts index 677427e25d..89ea546ae3 100644 --- a/apps/sim/connectors/s3/s3.ts +++ b/apps/sim/connectors/s3/s3.ts @@ -2,7 +2,7 @@ import crypto from 'crypto' import { createLogger } from '@sim/logger' import { getErrorMessage, toError } from '@sim/utils/errors' import { S3Icon } from '@/components/icons' -import { isHosted } from '@/lib/core/config/feature-flags' +import { isHosted } from '@/lib/core/config/env-flags' import { secureFetchWithRetry } from '@/lib/knowledge/documents/secure-fetch.server' import { VALIDATE_RETRY_OPTIONS } from '@/lib/knowledge/documents/utils' import type { ConnectorConfig, ExternalDocument, ExternalDocumentList } from '@/connectors/types' diff --git a/apps/sim/ee/access-control/utils/permission-check.test.ts b/apps/sim/ee/access-control/utils/permission-check.test.ts index a28fd7ae41..7476110a89 100644 --- a/apps/sim/ee/access-control/utils/permission-check.test.ts +++ b/apps/sim/ee/access-control/utils/permission-check.test.ts @@ -90,7 +90,7 @@ vi.mock('@/lib/workspaces/permissions/utils', () => ({ getWorkspaceWithOwner: mockGetWorkspaceWithOwner, })) -vi.mock('@/lib/core/config/feature-flags', () => ({ +vi.mock('@/lib/core/config/env-flags', () => ({ getAllowedIntegrationsFromEnv: mockGetAllowedIntegrationsFromEnv, isAccessControlEnabled: true, isHosted: true, diff --git a/apps/sim/ee/access-control/utils/permission-check.ts b/apps/sim/ee/access-control/utils/permission-check.ts index 7d304ea42a..5d0cbbac60 100644 --- a/apps/sim/ee/access-control/utils/permission-check.ts +++ b/apps/sim/ee/access-control/utils/permission-check.ts @@ -9,7 +9,7 @@ import { isHosted, isInvitationsDisabled, isPublicApiDisabled, -} from '@/lib/core/config/feature-flags' +} from '@/lib/core/config/env-flags' import { isBlockTypeAccessControlExempt } from '@/lib/permission-groups/block-access' import { DEFAULT_PERMISSION_GROUP_CONFIG, diff --git a/apps/sim/ee/data-retention/components/data-retention-settings.tsx b/apps/sim/ee/data-retention/components/data-retention-settings.tsx index 4e3bff54a3..5ac649517f 100644 --- a/apps/sim/ee/data-retention/components/data-retention-settings.tsx +++ b/apps/sim/ee/data-retention/components/data-retention-settings.tsx @@ -5,7 +5,7 @@ import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' import { Chip, ChipSelect, toast } from '@/components/emcn' import { useSession } from '@/lib/auth/auth-client' -import { isBillingEnabled } from '@/lib/core/config/feature-flags' +import { isBillingEnabled } from '@/lib/core/config/env-flags' import { getUserRole } from '@/lib/workspaces/organization/utils' import { SettingsSection } from '@/app/workspace/[workspaceId]/settings/components/settings-section/settings-section' import { InfoNote } from '@/ee/components/info-note' diff --git a/apps/sim/ee/sso/components/sso-settings.tsx b/apps/sim/ee/sso/components/sso-settings.tsx index e3840b7492..048cd0a3c9 100644 --- a/apps/sim/ee/sso/components/sso-settings.tsx +++ b/apps/sim/ee/sso/components/sso-settings.tsx @@ -19,7 +19,7 @@ import { import type { SsoRegistrationBody } from '@/lib/api/contracts/auth' import { useSession } from '@/lib/auth/auth-client' import { getSubscriptionAccessState } from '@/lib/billing/client/utils' -import { isBillingEnabled } from '@/lib/core/config/feature-flags' +import { isBillingEnabled } from '@/lib/core/config/env-flags' import { cn } from '@/lib/core/utils/cn' import { getBaseUrl } from '@/lib/core/utils/urls' import { getUserRole } from '@/lib/workspaces/organization/utils' diff --git a/apps/sim/ee/whitelabeling/components/whitelabeling-settings.tsx b/apps/sim/ee/whitelabeling/components/whitelabeling-settings.tsx index f6cf33511d..c10ccb1e89 100644 --- a/apps/sim/ee/whitelabeling/components/whitelabeling-settings.tsx +++ b/apps/sim/ee/whitelabeling/components/whitelabeling-settings.tsx @@ -10,7 +10,7 @@ import { Button, ChipInput, Label, Loader, toast } from '@/components/emcn' import { useSession } from '@/lib/auth/auth-client' import { getSubscriptionAccessState } from '@/lib/billing/client/utils' import { HEX_COLOR_REGEX } from '@/lib/branding' -import { isBillingEnabled } from '@/lib/core/config/feature-flags' +import { isBillingEnabled } from '@/lib/core/config/env-flags' import { cn } from '@/lib/core/utils/cn' import { getUserRole } from '@/lib/workspaces/organization/utils' import { diff --git a/apps/sim/executor/handlers/agent/agent-handler.test.ts b/apps/sim/executor/handlers/agent/agent-handler.test.ts index 2d7a89c0a8..6990a719bf 100644 --- a/apps/sim/executor/handlers/agent/agent-handler.test.ts +++ b/apps/sim/executor/handlers/agent/agent-handler.test.ts @@ -11,7 +11,7 @@ import { executeTool } from '@/tools' process.env.NEXT_PUBLIC_APP_URL = 'http://localhost:3000' -vi.mock('@/lib/core/config/feature-flags', () => ({ +vi.mock('@/lib/core/config/env-flags', () => ({ isHosted: false, isProd: false, isDev: true, diff --git a/apps/sim/hooks/queries/copilot-keys.ts b/apps/sim/hooks/queries/copilot-keys.ts index 479cc022bc..3be208c2cd 100644 --- a/apps/sim/hooks/queries/copilot-keys.ts +++ b/apps/sim/hooks/queries/copilot-keys.ts @@ -8,7 +8,7 @@ import { generateCopilotApiKeyContract, listCopilotApiKeysContract, } from '@/lib/api/contracts' -import { isHosted } from '@/lib/core/config/feature-flags' +import { isHosted } from '@/lib/core/config/env-flags' const logger = createLogger('CopilotKeysQuery') diff --git a/apps/sim/lib/a2a/push-notifications.ts b/apps/sim/lib/a2a/push-notifications.ts index 4413b569fb..016e993c4a 100644 --- a/apps/sim/lib/a2a/push-notifications.ts +++ b/apps/sim/lib/a2a/push-notifications.ts @@ -3,7 +3,7 @@ import { db } from '@sim/db' import { a2aPushNotificationConfig, a2aTask } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { eq } from 'drizzle-orm' -import { isTriggerDevEnabled } from '@/lib/core/config/feature-flags' +import { isTriggerDevEnabled } from '@/lib/core/config/env-flags' import { secureFetchWithPinnedIP, validateUrlWithDNS, diff --git a/apps/sim/lib/analytics/profound.ts b/apps/sim/lib/analytics/profound.ts index eedcb95572..ff8c568e14 100644 --- a/apps/sim/lib/analytics/profound.ts +++ b/apps/sim/lib/analytics/profound.ts @@ -7,7 +7,7 @@ */ import { createLogger } from '@sim/logger' import { env } from '@/lib/core/config/env' -import { isHosted } from '@/lib/core/config/feature-flags' +import { isHosted } from '@/lib/core/config/env-flags' import { getClientIp } from '@/lib/core/utils/request' import { getBaseDomain } from '@/lib/core/utils/urls' diff --git a/apps/sim/lib/api-key/byok.test.ts b/apps/sim/lib/api-key/byok.test.ts index 65e0f80362..6c1fcba13f 100644 --- a/apps/sim/lib/api-key/byok.test.ts +++ b/apps/sim/lib/api-key/byok.test.ts @@ -35,7 +35,7 @@ vi.mock('@/lib/core/config/env', () => ({ env: {}, })) -vi.mock('@/lib/core/config/feature-flags', () => ({ +vi.mock('@/lib/core/config/env-flags', () => ({ isHosted: false, })) diff --git a/apps/sim/lib/api-key/byok.ts b/apps/sim/lib/api-key/byok.ts index b0b19a9a49..b131d2f742 100644 --- a/apps/sim/lib/api-key/byok.ts +++ b/apps/sim/lib/api-key/byok.ts @@ -4,7 +4,7 @@ import { createLogger } from '@sim/logger' import { and, asc, eq } from 'drizzle-orm' import { getRotatingApiKey } from '@/lib/core/config/api-keys' import { env } from '@/lib/core/config/env' -import { isHosted } from '@/lib/core/config/feature-flags' +import { isHosted } from '@/lib/core/config/env-flags' import { decryptSecret } from '@/lib/core/security/encryption' import { getWorkspaceById } from '@/lib/workspaces/permissions/utils' import { getHostedModels } from '@/providers/models' diff --git a/apps/sim/lib/auth/access-control.test.ts b/apps/sim/lib/auth/access-control.test.ts index 8e685d3f7a..5667f724ca 100644 --- a/apps/sim/lib/auth/access-control.test.ts +++ b/apps/sim/lib/auth/access-control.test.ts @@ -28,7 +28,7 @@ vi.mock('@/lib/core/config/env', () => ({ }, })) -vi.mock('@/lib/core/config/feature-flags', () => ({ +vi.mock('@/lib/core/config/env-flags', () => ({ get isAppConfigEnabled() { return flagRef.isAppConfigEnabled }, diff --git a/apps/sim/lib/auth/access-control.ts b/apps/sim/lib/auth/access-control.ts index c597259029..a460257e29 100644 --- a/apps/sim/lib/auth/access-control.ts +++ b/apps/sim/lib/auth/access-control.ts @@ -1,7 +1,7 @@ import { normalizeEmail } from '@sim/utils/string' import { fetchAppConfigProfile } from '@/lib/core/config/appconfig' import { env } from '@/lib/core/config/env' -import { isAppConfigEnabled } from '@/lib/core/config/feature-flags' +import { isAppConfigEnabled } from '@/lib/core/config/env-flags' /** * Name of the AppConfig configuration profile holding the signup/login gating diff --git a/apps/sim/lib/auth/auth-client.ts b/apps/sim/lib/auth/auth-client.ts index 1a969c7cd1..b1ca36b84d 100644 --- a/apps/sim/lib/auth/auth-client.ts +++ b/apps/sim/lib/auth/auth-client.ts @@ -11,7 +11,7 @@ import { import { createAuthClient } from 'better-auth/react' import type { auth } from '@/lib/auth' import { env } from '@/lib/core/config/env' -import { isBillingEnabled, isOrganizationsEnabled } from '@/lib/core/config/feature-flags' +import { isBillingEnabled, isOrganizationsEnabled } from '@/lib/core/config/env-flags' import { getBaseUrl, getBrowserOrigin } from '@/lib/core/utils/urls' import { SessionContext, type SessionHookResult } from '@/app/_shell/providers/session-provider' diff --git a/apps/sim/lib/auth/auth.ts b/apps/sim/lib/auth/auth.ts index ed95b3f5c1..9b0f4523a4 100644 --- a/apps/sim/lib/auth/auth.ts +++ b/apps/sim/lib/auth/auth.ts @@ -76,7 +76,7 @@ import { isSignupEmailValidationEnabled, isSignupMxValidationEnabled, isSsoEnabled, -} from '@/lib/core/config/feature-flags' +} from '@/lib/core/config/env-flags' import { PlatformEvents } from '@/lib/core/telemetry' import { getBaseUrl, isLocalhostUrl, parseOriginList } from '@/lib/core/utils/urls' import { processCredentialDraft } from '@/lib/credentials/draft-processor' diff --git a/apps/sim/lib/auth/ban.test.ts b/apps/sim/lib/auth/ban.test.ts index f6e53aa9bb..7a38b91429 100644 --- a/apps/sim/lib/auth/ban.test.ts +++ b/apps/sim/lib/auth/ban.test.ts @@ -22,7 +22,7 @@ vi.mock('@/lib/core/config/env', () => ({ return envRef }, })) -vi.mock('@/lib/core/config/feature-flags', () => ({ isAppConfigEnabled: false })) +vi.mock('@/lib/core/config/env-flags', () => ({ isAppConfigEnabled: false })) import { getActivelyBannedUserIds, isBanActive, isEmailBlocked } from '@/lib/auth/ban' diff --git a/apps/sim/lib/billing/calculations/usage-monitor.test.ts b/apps/sim/lib/billing/calculations/usage-monitor.test.ts index d78d984d9a..b59dd045a1 100644 --- a/apps/sim/lib/billing/calculations/usage-monitor.test.ts +++ b/apps/sim/lib/billing/calculations/usage-monitor.test.ts @@ -11,7 +11,7 @@ const { mockFlags, mockDbLimit, mockGetOrgMemberUsageLimit, mockGetOrgMemberWork mockGetOrgMemberWorkspaceUsage: vi.fn(), })) -vi.mock('@/lib/core/config/feature-flags', () => ({ +vi.mock('@/lib/core/config/env-flags', () => ({ get isHosted() { return mockFlags.isHosted }, diff --git a/apps/sim/lib/billing/calculations/usage-monitor.ts b/apps/sim/lib/billing/calculations/usage-monitor.ts index df6e7d7fda..906007689f 100644 --- a/apps/sim/lib/billing/calculations/usage-monitor.ts +++ b/apps/sim/lib/billing/calculations/usage-monitor.ts @@ -22,7 +22,7 @@ import { import { getPlanTierDollars, isPaid } from '@/lib/billing/plan-helpers' import { isOrgScopedSubscription } from '@/lib/billing/subscriptions/utils' import { toDecimal, toNumber } from '@/lib/billing/utils/decimal' -import { isBillingEnabled, isHosted } from '@/lib/core/config/feature-flags' +import { isBillingEnabled, isHosted } from '@/lib/core/config/env-flags' const logger = createLogger('UsageMonitor') diff --git a/apps/sim/lib/billing/calculations/usage-reservation.test.ts b/apps/sim/lib/billing/calculations/usage-reservation.test.ts index a6435aff69..11a9305500 100644 --- a/apps/sim/lib/billing/calculations/usage-reservation.test.ts +++ b/apps/sim/lib/billing/calculations/usage-reservation.test.ts @@ -8,7 +8,7 @@ const { mockFlags } = vi.hoisted(() => ({ mockFlags: { isBillingEnabled: true }, })) -vi.mock('@/lib/core/config/feature-flags', () => ({ +vi.mock('@/lib/core/config/env-flags', () => ({ get isBillingEnabled() { return mockFlags.isBillingEnabled }, diff --git a/apps/sim/lib/billing/calculations/usage-reservation.ts b/apps/sim/lib/billing/calculations/usage-reservation.ts index c448c88776..a66e9e43f7 100644 --- a/apps/sim/lib/billing/calculations/usage-reservation.ts +++ b/apps/sim/lib/billing/calculations/usage-reservation.ts @@ -3,7 +3,7 @@ import { toError } from '@sim/utils/errors' import { BASE_EXECUTION_CHARGE } from '@/lib/billing/constants' import { getPlanTypeForLimits } from '@/lib/billing/plan-helpers' import { isOrgScopedSubscription } from '@/lib/billing/subscriptions/utils' -import { isBillingEnabled } from '@/lib/core/config/feature-flags' +import { isBillingEnabled } from '@/lib/core/config/env-flags' import { getRedisClient } from '@/lib/core/config/redis' import { getMaxExecutionTimeout } from '@/lib/core/execution-limits' import type { SubscriptionPlan } from '@/lib/core/rate-limiter/types' diff --git a/apps/sim/lib/billing/core/api-access.test.ts b/apps/sim/lib/billing/core/api-access.test.ts index fd5bfe8d92..fc1341d1c0 100644 --- a/apps/sim/lib/billing/core/api-access.test.ts +++ b/apps/sim/lib/billing/core/api-access.test.ts @@ -10,7 +10,7 @@ const { mockGetHighestPrioritySubscription, mockGetWorkspaceBilledAccountUserId, billingState: { isBillingEnabled: true, isFreeApiDeploymentGateEnabled: true }, })) -vi.mock('@/lib/core/config/feature-flags', () => ({ +vi.mock('@/lib/core/config/env-flags', () => ({ get isBillingEnabled() { return billingState.isBillingEnabled }, diff --git a/apps/sim/lib/billing/core/api-access.ts b/apps/sim/lib/billing/core/api-access.ts index be41d29293..43e65d67d8 100644 --- a/apps/sim/lib/billing/core/api-access.ts +++ b/apps/sim/lib/billing/core/api-access.ts @@ -1,6 +1,6 @@ import { getHighestPrioritySubscription } from '@/lib/billing/core/subscription' import { isPaid } from '@/lib/billing/plan-helpers' -import { isBillingEnabled, isFreeApiDeploymentGateEnabled } from '@/lib/core/config/feature-flags' +import { isBillingEnabled, isFreeApiDeploymentGateEnabled } from '@/lib/core/config/env-flags' import { getWorkspaceBilledAccountUserId } from '@/lib/workspaces/utils' /** The programmatic-execution paywall is active only when billing is enforced AND the gate flag is on. */ diff --git a/apps/sim/lib/billing/core/subscription.test.ts b/apps/sim/lib/billing/core/subscription.test.ts index 2f49aa6ba2..4d25f9ec42 100644 --- a/apps/sim/lib/billing/core/subscription.test.ts +++ b/apps/sim/lib/billing/core/subscription.test.ts @@ -30,7 +30,7 @@ vi.mock('@/lib/billing/subscriptions/utils', () => ({ USABLE_SUBSCRIPTION_STATUSES: ['active', 'trialing'], })) -vi.mock('@/lib/core/config/feature-flags', () => ({ +vi.mock('@/lib/core/config/env-flags', () => ({ isAccessControlEnabled: false, isBillingEnabled: true, isCredentialSetsEnabled: false, diff --git a/apps/sim/lib/billing/core/subscription.ts b/apps/sim/lib/billing/core/subscription.ts index 550fdcd340..ca0996eabc 100644 --- a/apps/sim/lib/billing/core/subscription.ts +++ b/apps/sim/lib/billing/core/subscription.ts @@ -27,7 +27,7 @@ import { isHosted, isInboxEnabled, isSsoEnabled, -} from '@/lib/core/config/feature-flags' +} from '@/lib/core/config/env-flags' import { getBaseUrl } from '@/lib/core/utils/urls' const logger = createLogger('SubscriptionCore') diff --git a/apps/sim/lib/billing/core/usage-log.test.ts b/apps/sim/lib/billing/core/usage-log.test.ts index ff2f446c83..889cb3662f 100644 --- a/apps/sim/lib/billing/core/usage-log.test.ts +++ b/apps/sim/lib/billing/core/usage-log.test.ts @@ -57,7 +57,7 @@ vi.mock('@/lib/billing/subscriptions/utils', () => ({ isOrgScopedSubscription: mockIsOrgScopedSubscription, })) -vi.mock('@/lib/core/config/feature-flags', () => ({ +vi.mock('@/lib/core/config/env-flags', () => ({ isBillingEnabled: true, })) diff --git a/apps/sim/lib/billing/core/usage.ts b/apps/sim/lib/billing/core/usage.ts index 3d1fcf04be..ef3cc7d763 100644 --- a/apps/sim/lib/billing/core/usage.ts +++ b/apps/sim/lib/billing/core/usage.ts @@ -32,7 +32,7 @@ import { } from '@/lib/billing/subscriptions/utils' import type { BillingData, UsageData, UsageLimitInfo } from '@/lib/billing/types' import { Decimal, toDecimal, toNumber } from '@/lib/billing/utils/decimal' -import { isBillingEnabled } from '@/lib/core/config/feature-flags' +import { isBillingEnabled } from '@/lib/core/config/env-flags' import { getBaseUrl } from '@/lib/core/utils/urls' import type { DbClient } from '@/lib/db/types' import { sendEmail } from '@/lib/messaging/email/mailer' diff --git a/apps/sim/lib/billing/organizations/seat-drift.test.ts b/apps/sim/lib/billing/organizations/seat-drift.test.ts index 63d319effa..b3b2de307b 100644 --- a/apps/sim/lib/billing/organizations/seat-drift.test.ts +++ b/apps/sim/lib/billing/organizations/seat-drift.test.ts @@ -27,7 +27,7 @@ vi.mock('@/lib/billing/organizations/seats', () => ({ reconcileOrganizationSeats: mockReconcileOrganizationSeats, })) -vi.mock('@/lib/core/config/feature-flags', () => ({ +vi.mock('@/lib/core/config/env-flags', () => ({ get isBillingEnabled() { return mockFeatureFlags.isBillingEnabled }, diff --git a/apps/sim/lib/billing/organizations/seat-drift.ts b/apps/sim/lib/billing/organizations/seat-drift.ts index 18819ac645..f4f06d1a9f 100644 --- a/apps/sim/lib/billing/organizations/seat-drift.ts +++ b/apps/sim/lib/billing/organizations/seat-drift.ts @@ -4,7 +4,7 @@ import { createLogger } from '@sim/logger' import { and, eq, inArray, isNotNull, like, or, sql } from 'drizzle-orm' import { reconcileOrganizationSeats } from '@/lib/billing/organizations/seats' import { USABLE_SUBSCRIPTION_STATUSES } from '@/lib/billing/subscriptions/utils' -import { isBillingEnabled } from '@/lib/core/config/feature-flags' +import { isBillingEnabled } from '@/lib/core/config/env-flags' const logger = createLogger('SeatDriftSweep') diff --git a/apps/sim/lib/billing/organizations/seats.test.ts b/apps/sim/lib/billing/organizations/seats.test.ts index 95725b9466..da1d27aef9 100644 --- a/apps/sim/lib/billing/organizations/seats.test.ts +++ b/apps/sim/lib/billing/organizations/seats.test.ts @@ -52,7 +52,7 @@ vi.mock('@/lib/billing/webhooks/outbox-handlers', () => ({ }, })) -vi.mock('@/lib/core/config/feature-flags', () => ({ +vi.mock('@/lib/core/config/env-flags', () => ({ get isBillingEnabled() { return mockFeatureFlags.isBillingEnabled }, diff --git a/apps/sim/lib/billing/organizations/seats.ts b/apps/sim/lib/billing/organizations/seats.ts index ac2b85f8d9..a956a38d44 100644 --- a/apps/sim/lib/billing/organizations/seats.ts +++ b/apps/sim/lib/billing/organizations/seats.ts @@ -6,7 +6,7 @@ import { syncSubscriptionUsageLimits } from '@/lib/billing/organization' import { isTeam } from '@/lib/billing/plan-helpers' import { USABLE_SUBSCRIPTION_STATUSES } from '@/lib/billing/subscriptions/utils' import { OUTBOX_EVENT_TYPES } from '@/lib/billing/webhooks/outbox-handlers' -import { isBillingEnabled } from '@/lib/core/config/feature-flags' +import { isBillingEnabled } from '@/lib/core/config/env-flags' import { enqueueOutboxEvent } from '@/lib/core/outbox/service' const logger = createLogger('OrganizationSeats') diff --git a/apps/sim/lib/billing/storage/limits.ts b/apps/sim/lib/billing/storage/limits.ts index 7cfb6b19ff..6c9c96d420 100644 --- a/apps/sim/lib/billing/storage/limits.ts +++ b/apps/sim/lib/billing/storage/limits.ts @@ -16,7 +16,7 @@ import { eq } from 'drizzle-orm' import { getPlanTypeForLimits, isEnterprise, isFree } from '@/lib/billing/plan-helpers' import { isOrgScopedSubscription } from '@/lib/billing/subscriptions/utils' import { getEnv } from '@/lib/core/config/env' -import { isBillingEnabled } from '@/lib/core/config/feature-flags' +import { isBillingEnabled } from '@/lib/core/config/env-flags' const logger = createLogger('StorageLimits') diff --git a/apps/sim/lib/billing/storage/tracking.ts b/apps/sim/lib/billing/storage/tracking.ts index 8fb3d962ef..5e838d7636 100644 --- a/apps/sim/lib/billing/storage/tracking.ts +++ b/apps/sim/lib/billing/storage/tracking.ts @@ -9,7 +9,7 @@ import { organization, userStats } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { eq, sql } from 'drizzle-orm' import { isOrgScopedSubscription } from '@/lib/billing/subscriptions/utils' -import { isBillingEnabled } from '@/lib/core/config/feature-flags' +import { isBillingEnabled } from '@/lib/core/config/env-flags' const logger = createLogger('StorageTracking') diff --git a/apps/sim/lib/billing/validation/seat-management.test.ts b/apps/sim/lib/billing/validation/seat-management.test.ts index c99cd09e26..cbab22d989 100644 --- a/apps/sim/lib/billing/validation/seat-management.test.ts +++ b/apps/sim/lib/billing/validation/seat-management.test.ts @@ -37,7 +37,7 @@ vi.mock('@/lib/billing/subscriptions/utils', () => ({ getEffectiveSeats: vi.fn().mockReturnValue(10), })) -vi.mock('@/lib/core/config/feature-flags', () => ({ +vi.mock('@/lib/core/config/env-flags', () => ({ get isBillingEnabled() { return mockFeatureFlags.isBillingEnabled }, diff --git a/apps/sim/lib/billing/validation/seat-management.ts b/apps/sim/lib/billing/validation/seat-management.ts index 48bb573633..f9f671ddc1 100644 --- a/apps/sim/lib/billing/validation/seat-management.ts +++ b/apps/sim/lib/billing/validation/seat-management.ts @@ -6,7 +6,7 @@ import { getOrganizationSubscription } from '@/lib/billing/core/billing' import { isEnterprise, isFree } from '@/lib/billing/plan-helpers' import { getEffectiveSeats } from '@/lib/billing/subscriptions/utils' import { OUTBOX_EVENT_TYPES } from '@/lib/billing/webhooks/outbox-handlers' -import { isBillingEnabled } from '@/lib/core/config/feature-flags' +import { isBillingEnabled } from '@/lib/core/config/env-flags' import { hasInflightOutboxEvent } from '@/lib/core/outbox/service' import { quickValidateEmail } from '@/lib/messaging/email/validation' diff --git a/apps/sim/lib/copilot/chat/payload.test.ts b/apps/sim/lib/copilot/chat/payload.test.ts index a1f92a0819..1afbfbbaac 100644 --- a/apps/sim/lib/copilot/chat/payload.test.ts +++ b/apps/sim/lib/copilot/chat/payload.test.ts @@ -1,7 +1,7 @@ /** * @vitest-environment node */ -import { featureFlagsMock, workflowsUtilsMock } from '@sim/testing' +import { envFlagsMock, workflowsUtilsMock } from '@sim/testing' import { beforeEach, describe, expect, it, vi } from 'vitest' const { mockCreateUserToolSchema, mockGetHighestPrioritySubscription } = vi.hoisted(() => ({ @@ -19,7 +19,7 @@ vi.mock('@/lib/billing/plan-helpers', () => ({ ), })) -vi.mock('@/lib/core/config/feature-flags', () => featureFlagsMock) +vi.mock('@/lib/core/config/env-flags', () => envFlagsMock) vi.mock('@/lib/mcp/utils', () => ({ createMcpToolId: vi.fn(), diff --git a/apps/sim/lib/copilot/chat/payload.ts b/apps/sim/lib/copilot/chat/payload.ts index 6c5e881e54..a3d0bb9014 100644 --- a/apps/sim/lib/copilot/chat/payload.ts +++ b/apps/sim/lib/copilot/chat/payload.ts @@ -7,7 +7,7 @@ import { getExposedIntegrationTools } from '@/lib/copilot/integration-tools' import { getToolEntry } from '@/lib/copilot/tool-executor/router' import { getCopilotToolDescription } from '@/lib/copilot/tools/descriptions' import { encodeVfsSegment } from '@/lib/copilot/vfs/path-utils' -import { isE2BDocEnabled, isHosted } from '@/lib/core/config/feature-flags' +import { isE2BDocEnabled, isHosted } from '@/lib/core/config/env-flags' import { buildUserSkillTool } from '@/lib/mothership/skills' import { trackChatUpload } from '@/lib/uploads/contexts/workspace/workspace-file-manager' import { stripVersionSuffix } from '@/tools/utils' diff --git a/apps/sim/lib/copilot/chat/process-contents.ts b/apps/sim/lib/copilot/chat/process-contents.ts index cc930889f1..81f4eae47c 100644 --- a/apps/sim/lib/copilot/chat/process-contents.ts +++ b/apps/sim/lib/copilot/chat/process-contents.ts @@ -16,7 +16,7 @@ import { encodeVfsPathSegments, encodeVfsSegment, } from '@/lib/copilot/vfs/path-utils' -import { getAllowedIntegrationsFromEnv } from '@/lib/core/config/feature-flags' +import { getAllowedIntegrationsFromEnv } from '@/lib/core/config/env-flags' import { getTableById } from '@/lib/table/service' import { getWorkspaceFileFolderPath } from '@/lib/uploads/contexts/workspace/workspace-file-folder-manager' import { getWorkspaceFile } from '@/lib/uploads/contexts/workspace/workspace-file-manager' diff --git a/apps/sim/lib/copilot/tools/handlers/function-execute.ts b/apps/sim/lib/copilot/tools/handlers/function-execute.ts index 725a2d2391..d3893be70b 100644 --- a/apps/sim/lib/copilot/tools/handlers/function-execute.ts +++ b/apps/sim/lib/copilot/tools/handlers/function-execute.ts @@ -2,7 +2,7 @@ import { createLogger } from '@sim/logger' import { decodeVfsPathSegments, encodeVfsPathSegments } from '@/lib/copilot/vfs/path-utils' import { resolveWorkflowAliasForWorkspace } from '@/lib/copilot/vfs/workflow-alias-resolver' import { isPlanAliasPath, workflowAliasSandboxPath } from '@/lib/copilot/vfs/workflow-aliases' -import { isMothershipBetaFeaturesEnabled } from '@/lib/core/config/feature-flags' +import { isMothershipBetaFeaturesEnabled } from '@/lib/core/config/env-flags' import { queryRows } from '@/lib/table/rows/service' import { getTableById, listTables } from '@/lib/table/service' import { listWorkspaceFileFolders } from '@/lib/uploads/contexts/workspace/workspace-file-folder-manager' diff --git a/apps/sim/lib/copilot/tools/server/blocks/get-blocks-metadata-tool.ts b/apps/sim/lib/copilot/tools/server/blocks/get-blocks-metadata-tool.ts index 1e1e5c3033..f2fc3846ad 100644 --- a/apps/sim/lib/copilot/tools/server/blocks/get-blocks-metadata-tool.ts +++ b/apps/sim/lib/copilot/tools/server/blocks/get-blocks-metadata-tool.ts @@ -5,7 +5,7 @@ import { toError } from '@sim/utils/errors' import { z } from 'zod' import { getCopilotToolDescription } from '@/lib/copilot/tools/descriptions' import type { BaseServerTool } from '@/lib/copilot/tools/server/base-tool' -import { getAllowedIntegrationsFromEnv, isHosted } from '@/lib/core/config/feature-flags' +import { getAllowedIntegrationsFromEnv, isHosted } from '@/lib/core/config/env-flags' import { getServiceAccountProviderForProviderId } from '@/lib/oauth/utils' import { getBlock } from '@/blocks/registry' import { AuthMode, type BlockConfig, isHiddenFromDisplay } from '@/blocks/types' diff --git a/apps/sim/lib/copilot/tools/server/blocks/get-trigger-blocks.ts b/apps/sim/lib/copilot/tools/server/blocks/get-trigger-blocks.ts index 0227357908..6a12632149 100644 --- a/apps/sim/lib/copilot/tools/server/blocks/get-trigger-blocks.ts +++ b/apps/sim/lib/copilot/tools/server/blocks/get-trigger-blocks.ts @@ -1,7 +1,7 @@ import { createLogger } from '@sim/logger' import { z } from 'zod' import type { BaseServerTool } from '@/lib/copilot/tools/server/base-tool' -import { getAllowedIntegrationsFromEnv } from '@/lib/core/config/feature-flags' +import { getAllowedIntegrationsFromEnv } from '@/lib/core/config/env-flags' import { getAllBlocks } from '@/blocks/registry' import { getUserPermissionConfig } from '@/ee/access-control/utils/permission-check' diff --git a/apps/sim/lib/copilot/tools/server/files/doc-compile.ts b/apps/sim/lib/copilot/tools/server/files/doc-compile.ts index 920830f193..45604e9667 100644 --- a/apps/sim/lib/copilot/tools/server/files/doc-compile.ts +++ b/apps/sim/lib/copilot/tools/server/files/doc-compile.ts @@ -1,5 +1,5 @@ import { createLogger } from '@sim/logger' -import { isMothershipBetaFeaturesEnabled } from '@/lib/core/config/feature-flags' +import { isMothershipBetaFeaturesEnabled } from '@/lib/core/config/env-flags' import { executeInE2B, executeShellInE2B, type SandboxFile } from '@/lib/execution/e2b' import { CodeLanguage } from '@/lib/execution/languages' import { diff --git a/apps/sim/lib/copilot/tools/server/files/edit-content.ts b/apps/sim/lib/copilot/tools/server/files/edit-content.ts index e272146045..35b2f599ad 100644 --- a/apps/sim/lib/copilot/tools/server/files/edit-content.ts +++ b/apps/sim/lib/copilot/tools/server/files/edit-content.ts @@ -5,7 +5,7 @@ import { type BaseServerTool, type ServerToolContext, } from '@/lib/copilot/tools/server/base-tool' -import { isE2BDocEnabled } from '@/lib/core/config/feature-flags' +import { isE2BDocEnabled } from '@/lib/core/config/env-flags' import { updateWorkspaceFileContent } from '@/lib/uploads/contexts/workspace/workspace-file-manager' import { getE2BDocFormat } from './doc-compile' import { consumeLatestFileIntent } from './file-intent-store' diff --git a/apps/sim/lib/copilot/tools/server/files/workspace-file.ts b/apps/sim/lib/copilot/tools/server/files/workspace-file.ts index 2ebc731904..77f5a71e67 100644 --- a/apps/sim/lib/copilot/tools/server/files/workspace-file.ts +++ b/apps/sim/lib/copilot/tools/server/files/workspace-file.ts @@ -11,7 +11,7 @@ import { import { ensureWorkflowAliasBacking } from '@/lib/copilot/vfs/workflow-alias-backing' import { resolveWorkflowAliasForWorkspace } from '@/lib/copilot/vfs/workflow-alias-resolver' import { isPlanAliasPath } from '@/lib/copilot/vfs/workflow-aliases' -import { isE2BDocEnabled } from '@/lib/core/config/feature-flags' +import { isE2BDocEnabled } from '@/lib/core/config/env-flags' import { runSandboxTask } from '@/lib/execution/sandbox/run-task' import { ensureWorkspaceFileFolderPath } from '@/lib/uploads/contexts/workspace/workspace-file-folder-manager' import { diff --git a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/validation.test.ts b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/validation.test.ts index 5493727f72..9c92998803 100644 --- a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/validation.test.ts +++ b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/validation.test.ts @@ -1,7 +1,7 @@ /** * @vitest-environment node */ -import { featureFlagsMock } from '@sim/testing' +import { envFlagsMock } from '@sim/testing' import { beforeEach, describe, expect, it, vi } from 'vitest' import { normalizeConditionRouterIds } from './builders' @@ -92,7 +92,7 @@ vi.mock('@/lib/copilot/validation/selector-validator', () => ({ validateSelectorIds: mockValidateSelectorIds, })) -vi.mock('@/lib/core/config/feature-flags', () => featureFlagsMock) +vi.mock('@/lib/core/config/env-flags', () => envFlagsMock) vi.mock('@/providers/utils', () => ({ getHostedModels: () => [], diff --git a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/validation.ts b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/validation.ts index bf43f411f2..58d0478561 100644 --- a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/validation.ts +++ b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow/validation.ts @@ -949,7 +949,7 @@ export async function preValidateCredentialInputs( context: { userId: string; workspaceId?: string }, workflowState?: Record ): Promise<{ filteredOperations: EditWorkflowOperation[]; errors: ValidationError[] }> { - const { isHosted } = await import('@/lib/core/config/feature-flags') + const { isHosted } = await import('@/lib/core/config/env-flags') const { getHostedModels } = await import('@/providers/utils') const logger = createLogger('PreValidateCredentials') diff --git a/apps/sim/lib/copilot/vfs/serializers.ts b/apps/sim/lib/copilot/vfs/serializers.ts index 984f341978..b5a8701421 100644 --- a/apps/sim/lib/copilot/vfs/serializers.ts +++ b/apps/sim/lib/copilot/vfs/serializers.ts @@ -1,6 +1,6 @@ import { truncate } from '@sim/utils/string' import { getCopilotToolDescription } from '@/lib/copilot/tools/descriptions' -import { isHosted } from '@/lib/core/config/feature-flags' +import { isHosted } from '@/lib/core/config/env-flags' import { isSubBlockHidden } from '@/lib/workflows/subblocks/visibility' import type { BlockConfig, SubBlockConfig } from '@/blocks/types' import { DYNAMIC_MODEL_PROVIDERS, PROVIDER_DEFINITIONS } from '@/providers/models' diff --git a/apps/sim/lib/copilot/vfs/workflow-alias-resolver.ts b/apps/sim/lib/copilot/vfs/workflow-alias-resolver.ts index aebfb6baa8..4e062a51fc 100644 --- a/apps/sim/lib/copilot/vfs/workflow-alias-resolver.ts +++ b/apps/sim/lib/copilot/vfs/workflow-alias-resolver.ts @@ -8,7 +8,7 @@ import { resolveWorkspacePlanAliasPath, type WorkflowAliasTarget, } from '@/lib/copilot/vfs/workflow-aliases' -import { isMothershipBetaFeaturesEnabled } from '@/lib/core/config/feature-flags' +import { isMothershipBetaFeaturesEnabled } from '@/lib/core/config/env-flags' import { canonicalizeVfsPath } from './path-utils' export async function resolveWorkflowAliasForWorkspace(args: { diff --git a/apps/sim/lib/copilot/vfs/workspace-vfs.ts b/apps/sim/lib/copilot/vfs/workspace-vfs.ts index 29b20ab5a8..66cf1724de 100644 --- a/apps/sim/lib/copilot/vfs/workspace-vfs.ts +++ b/apps/sim/lib/copilot/vfs/workspace-vfs.ts @@ -89,7 +89,7 @@ import { workspacePlanBackingPath, workspacePlansBackingFolderPath, } from '@/lib/copilot/vfs/workflow-aliases' -import { isE2BDocEnabled, isMothershipBetaFeaturesEnabled } from '@/lib/core/config/feature-flags' +import { isE2BDocEnabled, isMothershipBetaFeaturesEnabled } from '@/lib/core/config/env-flags' import { getAccessibleEnvCredentials, getAccessibleOAuthCredentials, diff --git a/apps/sim/lib/core/async-jobs/config.ts b/apps/sim/lib/core/async-jobs/config.ts index 5d32dc6fcd..7f1e3797b3 100644 --- a/apps/sim/lib/core/async-jobs/config.ts +++ b/apps/sim/lib/core/async-jobs/config.ts @@ -1,7 +1,7 @@ import { createLogger } from '@sim/logger' import { taskContext } from '@trigger.dev/core/v3' import type { AsyncBackendType, JobQueueBackend } from '@/lib/core/async-jobs/types' -import { isTriggerDevEnabled } from '@/lib/core/config/feature-flags' +import { isTriggerDevEnabled } from '@/lib/core/config/env-flags' const logger = createLogger('AsyncJobsConfig') diff --git a/apps/sim/lib/core/config/env-flags.ts b/apps/sim/lib/core/config/env-flags.ts new file mode 100644 index 0000000000..918747577a --- /dev/null +++ b/apps/sim/lib/core/config/env-flags.ts @@ -0,0 +1,339 @@ +/** + * Environment utility functions for consistent environment detection across the application + */ +import { env, getEnv, isFalsy, isTruthy } from './env' + +/** + * Is the application running in production mode + */ +export const isProd = env.NODE_ENV === 'production' + +/** + * Is the application running in development mode + */ +export const isDev = env.NODE_ENV === 'development' + +/** + * Is the application running in test mode + */ +export const isTest = env.NODE_ENV === 'test' + +/** + * Is this the hosted version of the application. + * True for sim.ai and any subdomain of sim.ai (e.g. staging.sim.ai, dev.sim.ai). + */ +const appUrl = getEnv('NEXT_PUBLIC_APP_URL') +let appHostname = '' +try { + appHostname = appUrl ? new URL(appUrl).hostname : '' +} catch { + // invalid URL — isHosted stays false +} +export const isHosted = appHostname === 'sim.ai' || appHostname.endsWith('.sim.ai') + +/** + * Is billing enforcement enabled + */ +export const isBillingEnabled = isTruthy(env.BILLING_ENABLED) + +/** + * Block free-plan accounts from programmatic workflow execution (API key, public + * API, MCP server, A2A agent server, generic webhooks, cross-origin chat embeds). + * Gated behind {@link isBillingEnabled}; off by default so the paywall can ship + * dark and be enabled per-deployment once verified. + */ +export const isFreeApiDeploymentGateEnabled = isTruthy(env.FREE_API_DEPLOYMENT_GATE_ENABLED) + +/** + * Order table rows by fractional `order_key` (O(1) insert/delete) instead of the + * legacy integer `position`. When off, behavior is unchanged. Keys are written + * regardless of this flag; it only controls which column is authoritative for + * reads/ordering and whether inserts/deletes reshift positions. + */ +export const isTablesFractionalOrderingEnabled = isTruthy(env.TABLES_FRACTIONAL_ORDERING) + +/** + * Is email verification enabled + */ +export const isEmailVerificationEnabled = isTruthy(env.EMAIL_VERIFICATION_ENABLED) + +/** + * Is authentication disabled (for self-hosted deployments behind private networks) + * This flag is blocked when isHosted is true. + */ +export const isAuthDisabled = isTruthy(env.DISABLE_AUTH) && !isHosted + +if (isTruthy(env.DISABLE_AUTH)) { + import('@sim/logger') + .then(({ createLogger }) => { + const logger = createLogger('EnvFlags') + if (isHosted) { + logger.error( + 'DISABLE_AUTH is set but ignored on hosted environment. Authentication remains enabled for security.' + ) + } else { + logger.warn( + 'DISABLE_AUTH is enabled. Authentication is bypassed and all requests use an anonymous session. Only use this in trusted private networks.' + ) + } + }) + .catch(() => { + // Fallback during config compilation when logger is unavailable + }) +} + +/** + * Is user registration disabled + */ +export const isRegistrationDisabled = isTruthy(env.DISABLE_REGISTRATION) + +/** + * Is email/password authentication enabled (defaults to true) + */ +export const isEmailPasswordEnabled = !isFalsy(env.EMAIL_PASSWORD_SIGNUP_ENABLED) + +/** + * Is signup email validation enabled (disposable email blocking via better-auth-harmony) + */ +export const isSignupEmailValidationEnabled = isTruthy(env.SIGNUP_EMAIL_VALIDATION_ENABLED) + +/** + * Is MX-based signup validation enabled (blocks no-MX domains and denylisted shared spam + * mail backends). Opt-in to avoid adding a DNS dependency or blocking legitimate signups on + * self-hosted deployments with non-standard mail setups; enable on abuse-targeted deployments. + */ +export const isSignupMxValidationEnabled = isTruthy(env.SIGNUP_MX_VALIDATION_ENABLED) + +/** + * Is AWS AppConfig the source of truth for the signup/login gating lists. + * Hosted-only and requires both AppConfig identifiers (injected by the infra + * stack). Self-hosted/OSS deployments always use the env-var fallback, so the + * AppConfig client is never reached off-hosted. + */ +export const isAppConfigEnabled = + isHosted && Boolean(env.APPCONFIG_APPLICATION && env.APPCONFIG_ENVIRONMENT) + +/** + * Is Trigger.dev enabled for async job processing + */ +export const isTriggerDevEnabled = isTruthy(env.TRIGGER_DEV_ENABLED) + +/** + * Is SSO enabled for enterprise authentication + */ +export const isSsoEnabled = isTruthy(env.SSO_ENABLED) + +/** + * Is credential sets (email polling) enabled via env var override + * This bypasses plan requirements for self-hosted deployments + */ +export const isCredentialSetsEnabled = isTruthy(env.CREDENTIAL_SETS_ENABLED) + +/** + * Is access control (permission groups) enabled via env var override + * This bypasses plan requirements for self-hosted deployments + */ +export const isAccessControlEnabled = isTruthy(env.ACCESS_CONTROL_ENABLED) + +/** + * Is organizations enabled + * True if billing is enabled (orgs come with billing), OR explicitly enabled via env var, + * OR if access control is enabled (access control requires organizations) + */ +export const isOrganizationsEnabled = + isBillingEnabled || isTruthy(env.ORGANIZATIONS_ENABLED) || isAccessControlEnabled + +/** + * Is inbox (Sim Mailer) enabled via env var override + * This bypasses hosted requirements for self-hosted deployments + */ +export const isInboxEnabled = isTruthy(env.INBOX_ENABLED) + +/** + * Is whitelabeling enabled via env var override + * This bypasses hosted requirements for self-hosted deployments + */ +export const isWhitelabelingEnabled = isTruthy(env.WHITELABELING_ENABLED) + +/** + * Is audit logs enabled via env var override + * This bypasses hosted requirements for self-hosted deployments + */ +export const isAuditLogsEnabled = isTruthy(env.AUDIT_LOGS_ENABLED) + +/** + * Is data retention enabled via env var override + * This bypasses hosted requirements for self-hosted deployments + */ +export const isDataRetentionEnabled = isTruthy(env.DATA_RETENTION_ENABLED) + +/** + * Is data drains enabled via env var override + * This bypasses hosted requirements for self-hosted deployments + */ +export const isDataDrainsEnabled = isTruthy(env.DATA_DRAINS_ENABLED) + +/** + * Are workflow output columns enabled in user tables. + * Defaults to false; set NEXT_PUBLIC_WORKFLOW_COLUMNS_ENABLED=true to show + * the "Workflow" column type in the new-column dropdown. + */ +export const isWorkflowColumnsEnabledClient = isTruthy( + getEnv('NEXT_PUBLIC_WORKFLOW_COLUMNS_ENABLED') +) + +/** + * Enables beta Mothership plan/changelog artifact surfaces. + */ +export const isMothershipBetaFeaturesEnabled = isTruthy(env.MOTHERSHIP_BETA_FEATURES) + +/** + * Is E2B enabled for remote code execution + */ +export const isE2bEnabled = isTruthy(env.E2B_ENABLED) + +/** + * Whether the E2B document-generation sandbox is enabled. + * + * Requires E2B (with an API key) AND a dedicated doc-generation template id. + * When true, ALL four formats compile in the E2B doc sandbox: pptx/docx via Node + * (pptxgenjs/docx + react-icons/sharp icons), pdf/xlsx via Python + * (reportlab/openpyxl). When false, compilation stays on the JavaScript + * (isolated-vm) path, byte-identical to its prior behavior (and xlsx is + * unavailable). Drives both the Sim compile backend and the `docCompiler` flag + * sent to the copilot file subagent so the agent's output and compiler agree. + */ +export const isE2BDocEnabled = + isE2bEnabled && Boolean(env.E2B_API_KEY) && Boolean(env.MOTHERSHIP_E2B_DOC_TEMPLATE_ID) + +/** + * Whether Ollama is configured (OLLAMA_URL is set). + * When true, models that are not in the static cloud model list and have no + * slash-prefixed provider namespace are assumed to be Ollama models + * and do not require an API key. + */ +export const isOllamaConfigured = Boolean(env.OLLAMA_URL) + +/** + * Whether Azure OpenAI / Azure Anthropic credentials are pre-configured at the server level + * (via AZURE_OPENAI_ENDPOINT, AZURE_OPENAI_API_KEY, AZURE_ANTHROPIC_ENDPOINT, etc.). + * When true, the endpoint, API key, and API version fields are hidden in the Agent block UI. + * Set NEXT_PUBLIC_AZURE_CONFIGURED=true in self-hosted deployments on Azure. + */ +export const isAzureConfigured = isTruthy(getEnv('NEXT_PUBLIC_AZURE_CONFIGURED')) + +/** + * Whether a Cohere API key is pre-configured server-side for the Knowledge block reranker + * (`COHERE_API_KEY` or `COHERE_API_KEY_1/2/3`). When true, the Cohere API Key field is hidden + * in the Knowledge block UI. + * Set NEXT_PUBLIC_COHERE_CONFIGURED=true in self-hosted deployments that ship a Cohere key. + */ +export const isCohereConfigured = isTruthy(getEnv('NEXT_PUBLIC_COHERE_CONFIGURED')) + +/** + * Are invitations disabled globally + * When true, workspace invitations are disabled for all users + */ +export const isInvitationsDisabled = isTruthy(env.DISABLE_INVITATIONS) + +/** + * Is public API access disabled globally + * When true, the public API toggle is hidden and public API access is blocked + */ +export const isPublicApiDisabled = isTruthy(env.DISABLE_PUBLIC_API) + +/** + * Is Google OAuth login disabled + * When true, the Google OAuth login button is hidden even when credentials are configured + */ +export const isGoogleAuthDisabled = isTruthy(env.DISABLE_GOOGLE_AUTH) + +/** + * Is GitHub OAuth login disabled + * When true, the GitHub OAuth login button is hidden even when credentials are configured + */ +export const isGithubAuthDisabled = isTruthy(env.DISABLE_GITHUB_AUTH) + +/** + * Is Microsoft OAuth login disabled + * When true, the Microsoft OAuth login button is hidden even when credentials are configured + */ +export const isMicrosoftAuthDisabled = isTruthy(env.DISABLE_MICROSOFT_AUTH) + +/** + * Is email/password signup disabled + * When true, new registrations via email/password are blocked at the server level. + * Existing users can still sign in with email/password. + */ +export const isEmailSignupDisabled = isTruthy(env.DISABLE_EMAIL_SIGNUP) + +/** + * Is React Grab enabled for UI element debugging + * When true and in development mode, enables React Grab for copying UI element context to clipboard + */ +export const isReactGrabEnabled = isDev && isTruthy(env.REACT_GRAB_ENABLED) + +/** + * Is React Scan enabled for performance debugging + * When true and in development mode, enables React Scan for detecting render performance issues + */ +export const isReactScanEnabled = isDev && isTruthy(env.REACT_SCAN_ENABLED) + +/** + * Returns the parsed allowlist of integration block types from the environment variable. + * If not set or empty, returns null (meaning all integrations are allowed). + */ +export function getAllowedIntegrationsFromEnv(): string[] | null { + if (!env.ALLOWED_INTEGRATIONS) return null + const parsed = env.ALLOWED_INTEGRATIONS.split(',') + .map((i) => i.trim().toLowerCase()) + .filter(Boolean) + return parsed.length > 0 ? parsed : null +} + +/** + * Returns the list of blacklisted provider IDs from the environment variable. + * If not set or empty, returns an empty array (meaning no providers are blacklisted). + */ +export function getBlacklistedProvidersFromEnv(): string[] { + if (!env.BLACKLISTED_PROVIDERS) return [] + return env.BLACKLISTED_PROVIDERS.split(',') + .map((p) => p.trim().toLowerCase()) + .filter(Boolean) +} + +/** + * Normalizes a domain entry from the ALLOWED_MCP_DOMAINS env var. + * Accepts bare hostnames (e.g., "mcp.company.com") or full URLs (e.g., "https://mcp.company.com"). + * Extracts the hostname in either case. + */ +function normalizeDomainEntry(entry: string): string { + const trimmed = entry.trim().toLowerCase() + if (!trimmed) return '' + if (trimmed.includes('://')) { + try { + return new URL(trimmed).hostname + } catch { + return trimmed + } + } + return trimmed +} + +/** + * Get allowed MCP server domains from the ALLOWED_MCP_DOMAINS env var. + * Returns null if not set (all domains allowed), or parsed array of lowercase hostnames. + * Accepts both bare hostnames and full URLs in the env var value. + */ +export function getAllowedMcpDomainsFromEnv(): string[] | null { + if (!env.ALLOWED_MCP_DOMAINS) return null + const parsed = env.ALLOWED_MCP_DOMAINS.split(',').map(normalizeDomainEntry).filter(Boolean) + return parsed.length > 0 ? parsed : null +} + +/** + * Get cost multiplier based on environment + */ +export function getCostMultiplier(): number { + return isProd ? (env.COST_MULTIPLIER ?? 1) : 1 +} diff --git a/apps/sim/lib/core/config/feature-flags.test.ts b/apps/sim/lib/core/config/feature-flags.test.ts new file mode 100644 index 0000000000..d2250a60f8 --- /dev/null +++ b/apps/sim/lib/core/config/feature-flags.test.ts @@ -0,0 +1,168 @@ +/** + * @vitest-environment node + */ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { + FeatureFlagContext, + FeatureFlagName, + FeatureFlagsConfig, +} from '@/lib/core/config/feature-flags' + +const { mockFetch, mockIsPlatformAdmin, envRef, flagRef } = vi.hoisted(() => ({ + mockFetch: vi.fn(), + mockIsPlatformAdmin: vi.fn(), + envRef: { + APPCONFIG_APPLICATION: 'sim-staging' as string | undefined, + APPCONFIG_ENVIRONMENT: 'staging' as string | undefined, + }, + flagRef: { isAppConfigEnabled: false }, +})) + +vi.mock('@/lib/core/config/appconfig', () => ({ + fetchAppConfigProfile: mockFetch, +})) + +vi.mock('@/lib/core/config/env', () => ({ + get env() { + return envRef + }, +})) + +vi.mock('@/lib/core/config/env-flags', () => ({ + get isAppConfigEnabled() { + return flagRef.isAppConfigEnabled + }, +})) + +vi.mock('@/lib/permissions/super-user', () => ({ + isPlatformAdmin: mockIsPlatformAdmin, +})) + +import { getFeatureFlags, isFeatureEnabled } from '@/lib/core/config/feature-flags' + +/** Make `getFeatureFlags` resolve to `doc` via the AppConfig path (also exercises parseConfig). */ +function withAppConfig(doc: unknown) { + flagRef.isAppConfigEnabled = true + mockFetch.mockImplementation((_ids, parse) => Promise.resolve(parse(doc))) +} + +/** + * `isFeatureEnabled` only accepts registered `FeatureFlagName`s. The registry is + * empty in this PR, so tests reference flags through the AppConfig document and + * cast their throwaway names through this helper. + */ +const enabled = (flag: string, ctx?: FeatureFlagContext) => + isFeatureEnabled(flag as FeatureFlagName, ctx) + +describe('getFeatureFlags', () => { + beforeEach(() => { + vi.clearAllMocks() + flagRef.isAppConfigEnabled = false + }) + + it('derives flags from fallback secrets (empty registry → empty) when AppConfig is disabled, without fetching', async () => { + expect(await getFeatureFlags()).toEqual({ flags: {} }) + expect(mockFetch).not.toHaveBeenCalled() + }) + + it('reads the feature-flags profile and normalizes the payload when enabled', async () => { + withAppConfig({ + flags: { + a: { enabled: true }, + b: { orgIds: ['Org_1', ' org_1 ', '', 'org_2'], userIds: 'nope' }, + c: 'not-an-object', + }, + }) + + const { flags } = await getFeatureFlags() + expect(flags.a).toEqual({ enabled: true }) + expect(flags.b).toEqual({ orgIds: ['Org_1', 'org_1', 'org_2'] }) + expect(flags.c).toBeUndefined() + expect(mockFetch).toHaveBeenCalledWith( + { application: 'sim-staging', environment: 'staging', profile: 'feature-flags' }, + expect.any(Function) + ) + }) + + it('falls back to the secret-derived document when the fetch yields null', async () => { + flagRef.isAppConfigEnabled = true + mockFetch.mockResolvedValue(null) + expect(await getFeatureFlags()).toEqual({ flags: {} }) + }) + + it('degrades gracefully on a malformed document', async () => { + withAppConfig({ flags: 'not-an-object' }) + expect(await getFeatureFlags()).toEqual({ flags: {} }) + withAppConfig(null) + expect(await getFeatureFlags()).toEqual({ flags: {} }) + }) +}) + +describe('isFeatureEnabled', () => { + beforeEach(() => { + vi.clearAllMocks() + flagRef.isAppConfigEnabled = false + }) + + it('returns false for an unknown flag', async () => { + withAppConfig({ flags: {} }) + expect(await enabled('missing', { userId: 'u1' })).toBe(false) + }) + + it('matches the global enabled clause', async () => { + withAppConfig({ flags: { f: { enabled: true } } }) + expect(await enabled('f')).toBe(true) + }) + + it('matches the userId allowlist', async () => { + withAppConfig({ flags: { f: { userIds: ['u1'] } } }) + expect(await enabled('f', { userId: 'u1' })).toBe(true) + expect(await enabled('f', { userId: 'u2' })).toBe(false) + expect(await enabled('f', {})).toBe(false) + }) + + it('matches the orgId allowlist', async () => { + withAppConfig({ flags: { f: { orgIds: ['o1'] } } }) + expect(await enabled('f', { orgId: 'o1' })).toBe(true) + expect(await enabled('f', { orgId: 'o2' })).toBe(false) + }) + + describe('admin clause (lazy resolution)', () => { + it('resolves admin from userId when admins is the deciding clause', async () => { + withAppConfig({ flags: { f: { admins: true } } }) + mockIsPlatformAdmin.mockResolvedValue(true) + expect(await enabled('f', { userId: 'u1' })).toBe(true) + expect(mockIsPlatformAdmin).toHaveBeenCalledWith('u1') + + mockIsPlatformAdmin.mockResolvedValue(false) + expect(await enabled('f', { userId: 'u2' })).toBe(false) + }) + + it('uses the isAdmin override without querying', async () => { + withAppConfig({ flags: { f: { admins: true } } }) + expect(await enabled('f', { userId: 'u1', isAdmin: true })).toBe(true) + expect(mockIsPlatformAdmin).not.toHaveBeenCalled() + }) + + it('resolves to false without querying when userId is absent', async () => { + withAppConfig({ flags: { f: { admins: true } } }) + expect(await enabled('f', { orgId: 'o1' })).toBe(false) + expect(mockIsPlatformAdmin).not.toHaveBeenCalled() + }) + + it('does not query when an earlier clause already matched', async () => { + withAppConfig({ flags: { f: { enabled: true, admins: true } } }) + expect(await enabled('f', { userId: 'u1' })).toBe(true) + + withAppConfig({ flags: { g: { userIds: ['u1'], admins: true } } }) + expect(await enabled('g', { userId: 'u1' })).toBe(true) + expect(mockIsPlatformAdmin).not.toHaveBeenCalled() + }) + + it('does not query when the rule has no admins clause', async () => { + withAppConfig({ flags: { f: { userIds: ['u2'] } } }) + expect(await enabled('f', { userId: 'u1' })).toBe(false) + expect(mockIsPlatformAdmin).not.toHaveBeenCalled() + }) + }) +}) diff --git a/apps/sim/lib/core/config/feature-flags.ts b/apps/sim/lib/core/config/feature-flags.ts index cde33a750e..b4018581bb 100644 --- a/apps/sim/lib/core/config/feature-flags.ts +++ b/apps/sim/lib/core/config/feature-flags.ts @@ -1,339 +1,181 @@ -/** - * Environment utility functions for consistent environment detection across the application - */ -import { env, getEnv, isFalsy, isTruthy } from './env' +import { fetchAppConfigProfile } from '@/lib/core/config/appconfig' +import { env, isTruthy } from '@/lib/core/config/env' +import { isAppConfigEnabled } from '@/lib/core/config/env-flags' /** - * Is the application running in production mode + * Name of the AppConfig configuration profile holding the gated feature flags. + * Cross-repo contract: must match the `CfnConfigurationProfile` name created by + * the infra stack. */ -export const isProd = env.NODE_ENV === 'production' +const FEATURE_FLAGS_PROFILE = 'feature-flags' /** - * Is the application running in development mode + * A single flag's gating rule. A flag is ON for a context when ANY clause matches: + * the global `enabled` default, the org/user allowlists, or `admins` for platform + * admins. An absent clause never matches. */ -export const isDev = env.NODE_ENV === 'development' - -/** - * Is the application running in test mode - */ -export const isTest = env.NODE_ENV === 'test' - -/** - * Is this the hosted version of the application. - * True for sim.ai and any subdomain of sim.ai (e.g. staging.sim.ai, dev.sim.ai). - */ -const appUrl = getEnv('NEXT_PUBLIC_APP_URL') -let appHostname = '' -try { - appHostname = appUrl ? new URL(appUrl).hostname : '' -} catch { - // invalid URL — isHosted stays false +export interface FeatureFlagRule { + enabled?: boolean + orgIds?: string[] + userIds?: string[] + admins?: boolean } -export const isHosted = appHostname === 'sim.ai' || appHostname.endsWith('.sim.ai') -/** - * Is billing enforcement enabled - */ -export const isBillingEnabled = isTruthy(env.BILLING_ENABLED) - -/** - * Block free-plan accounts from programmatic workflow execution (API key, public - * API, MCP server, A2A agent server, generic webhooks, cross-origin chat embeds). - * Gated behind {@link isBillingEnabled}; off by default so the paywall can ship - * dark and be enabled per-deployment once verified. - */ -export const isFreeApiDeploymentGateEnabled = isTruthy(env.FREE_API_DEPLOYMENT_GATE_ENABLED) - -/** - * Order table rows by fractional `order_key` (O(1) insert/delete) instead of the - * legacy integer `position`. When off, behavior is unchanged. Keys are written - * regardless of this flag; it only controls which column is authoritative for - * reads/ordering and whether inserts/deletes reshift positions. - */ -export const isTablesFractionalOrderingEnabled = isTruthy(env.TABLES_FRACTIONAL_ORDERING) - -/** - * Is email verification enabled - */ -export const isEmailVerificationEnabled = isTruthy(env.EMAIL_VERIFICATION_ENABLED) - -/** - * Is authentication disabled (for self-hosted deployments behind private networks) - * This flag is blocked when isHosted is true. - */ -export const isAuthDisabled = isTruthy(env.DISABLE_AUTH) && !isHosted - -if (isTruthy(env.DISABLE_AUTH)) { - import('@sim/logger') - .then(({ createLogger }) => { - const logger = createLogger('FeatureFlags') - if (isHosted) { - logger.error( - 'DISABLE_AUTH is set but ignored on hosted environment. Authentication remains enabled for security.' - ) - } else { - logger.warn( - 'DISABLE_AUTH is enabled. Authentication is bypassed and all requests use an anonymous session. Only use this in trusted private networks.' - ) - } - }) - .catch(() => { - // Fallback during config compilation when logger is unavailable - }) +export interface FeatureFlagsConfig { + flags: Record } /** - * Is user registration disabled + * Per-request evaluation context. Pass only the ids you have — a missing id skips + * its clause. Admin status is resolved internally from `userId`; `isAdmin` is an + * optional fast-path override for callers that already know it (e.g. admin routes). */ -export const isRegistrationDisabled = isTruthy(env.DISABLE_REGISTRATION) +export interface FeatureFlagContext { + userId?: string | null + orgId?: string | null + isAdmin?: boolean +} /** - * Is email/password authentication enabled (defaults to true) - */ -export const isEmailPasswordEnabled = !isFalsy(env.EMAIL_PASSWORD_SIGNUP_ENABLED) - -/** - * Is signup email validation enabled (disposable email blocking via better-auth-harmony) - */ -export const isSignupEmailValidationEnabled = isTruthy(env.SIGNUP_EMAIL_VALIDATION_ENABLED) - -/** - * Is MX-based signup validation enabled (blocks no-MX domains and denylisted shared spam - * mail backends). Opt-in to avoid adding a DNS dependency or blocking legitimate signups on - * self-hosted deployments with non-standard mail setups; enable on abuse-targeted deployments. - */ -export const isSignupMxValidationEnabled = isTruthy(env.SIGNUP_MX_VALIDATION_ENABLED) - -/** - * Is AWS AppConfig the source of truth for the signup/login gating lists. - * Hosted-only and requires both AppConfig identifiers (injected by the infra - * stack). Self-hosted/OSS deployments always use the env-var fallback, so the - * AppConfig client is never reached off-hosted. - */ -export const isAppConfigEnabled = - isHosted && Boolean(env.APPCONFIG_APPLICATION && env.APPCONFIG_ENVIRONMENT) - -/** - * Is Trigger.dev enabled for async job processing - */ -export const isTriggerDevEnabled = isTruthy(env.TRIGGER_DEV_ENABLED) - -/** - * Is SSO enabled for enterprise authentication - */ -export const isSsoEnabled = isTruthy(env.SSO_ENABLED) - -/** - * Is credential sets (email polling) enabled via env var override - * This bypasses plan requirements for self-hosted deployments - */ -export const isCredentialSetsEnabled = isTruthy(env.CREDENTIAL_SETS_ENABLED) - -/** - * Is access control (permission groups) enabled via env var override - * This bypasses plan requirements for self-hosted deployments - */ -export const isAccessControlEnabled = isTruthy(env.ACCESS_CONTROL_ENABLED) - -/** - * Is organizations enabled - * True if billing is enabled (orgs come with billing), OR explicitly enabled via env var, - * OR if access control is enabled (access control requires organizations) - */ -export const isOrganizationsEnabled = - isBillingEnabled || isTruthy(env.ORGANIZATIONS_ENABLED) || isAccessControlEnabled - -/** - * Is inbox (Sim Mailer) enabled via env var override - * This bypasses hosted requirements for self-hosted deployments - */ -export const isInboxEnabled = isTruthy(env.INBOX_ENABLED) - -/** - * Is whitelabeling enabled via env var override - * This bypasses hosted requirements for self-hosted deployments - */ -export const isWhitelabelingEnabled = isTruthy(env.WHITELABELING_ENABLED) - -/** - * Is audit logs enabled via env var override - * This bypasses hosted requirements for self-hosted deployments - */ -export const isAuditLogsEnabled = isTruthy(env.AUDIT_LOGS_ENABLED) - -/** - * Is data retention enabled via env var override - * This bypasses hosted requirements for self-hosted deployments - */ -export const isDataRetentionEnabled = isTruthy(env.DATA_RETENTION_ENABLED) - -/** - * Is data drains enabled via env var override - * This bypasses hosted requirements for self-hosted deployments - */ -export const isDataDrainsEnabled = isTruthy(env.DATA_DRAINS_ENABLED) - -/** - * Are workflow output columns enabled in user tables. - * Defaults to false; set NEXT_PUBLIC_WORKFLOW_COLUMNS_ENABLED=true to show - * the "Workflow" column type in the new-column dropdown. - */ -export const isWorkflowColumnsEnabledClient = isTruthy( - getEnv('NEXT_PUBLIC_WORKFLOW_COLUMNS_ENABLED') -) - -/** - * Enables beta Mothership plan/changelog artifact surfaces. - */ -export const isMothershipBetaFeaturesEnabled = isTruthy(env.MOTHERSHIP_BETA_FEATURES) - -/** - * Is E2B enabled for remote code execution - */ -export const isE2bEnabled = isTruthy(env.E2B_ENABLED) - -/** - * Whether the E2B document-generation sandbox is enabled. + * Registry of known feature flags. Each maps to the secret consulted ONLY when + * AppConfig is not the source of truth (self-hosted/OSS, local dev, or hosted + * without APPCONFIG_*). A truthy secret turns the flag on globally. * - * Requires E2B (with an API key) AND a dedicated doc-generation template id. - * When true, ALL four formats compile in the E2B doc sandbox: pptx/docx via Node - * (pptxgenjs/docx + react-icons/sharp icons), pdf/xlsx via Python - * (reportlab/openpyxl). When false, compilation stays on the JavaScript - * (isolated-vm) path, byte-identical to its prior behavior (and xlsx is - * unavailable). Drives both the Sim compile backend and the `docCompiler` flag - * sent to the copilot file subagent so the agent's output and compiler agree. + * Gating by org/user/admin is available ONLY through the hosted AppConfig document + * — it deliberately cannot be expressed here, so no environment can grant (e.g.) + * admin access from a code literal. To add a flag, register its name and the secret + * to fall back on. */ -export const isE2BDocEnabled = - isE2bEnabled && Boolean(env.E2B_API_KEY) && Boolean(env.MOTHERSHIP_E2B_DOC_TEMPLATE_ID) - /** - * Whether Ollama is configured (OLLAMA_URL is set). - * When true, models that are not in the static cloud model list and have no - * slash-prefixed provider namespace are assumed to be Ollama models - * and do not require an API key. + * The single definition of a feature flag. Everything about a flag lives in one + * place: its name (the registry key), a human-readable `description`, and the + * `fallback` secret consulted when AppConfig isn't the source of truth (truthy ⇒ on + * globally). + * + * Gating by org/user/admin is deliberately NOT part of a definition — it lives only + * in the hosted AppConfig document, so no environment can grant access from a code + * literal. */ -export const isOllamaConfigured = Boolean(env.OLLAMA_URL) - -/** - * Whether Azure OpenAI / Azure Anthropic credentials are pre-configured at the server level - * (via AZURE_OPENAI_ENDPOINT, AZURE_OPENAI_API_KEY, AZURE_ANTHROPIC_ENDPOINT, etc.). - * When true, the endpoint, API key, and API version fields are hidden in the Agent block UI. - * Set NEXT_PUBLIC_AZURE_CONFIGURED=true in self-hosted deployments on Azure. - */ -export const isAzureConfigured = isTruthy(getEnv('NEXT_PUBLIC_AZURE_CONFIGURED')) - -/** - * Whether a Cohere API key is pre-configured server-side for the Knowledge block reranker - * (`COHERE_API_KEY` or `COHERE_API_KEY_1/2/3`). When true, the Cohere API Key field is hidden - * in the Knowledge block UI. - * Set NEXT_PUBLIC_COHERE_CONFIGURED=true in self-hosted deployments that ship a Cohere key. - */ -export const isCohereConfigured = isTruthy(getEnv('NEXT_PUBLIC_COHERE_CONFIGURED')) - -/** - * Are invitations disabled globally - * When true, workspace invitations are disabled for all users - */ -export const isInvitationsDisabled = isTruthy(env.DISABLE_INVITATIONS) - -/** - * Is public API access disabled globally - * When true, the public API toggle is hidden and public API access is blocked - */ -export const isPublicApiDisabled = isTruthy(env.DISABLE_PUBLIC_API) - -/** - * Is Google OAuth login disabled - * When true, the Google OAuth login button is hidden even when credentials are configured - */ -export const isGoogleAuthDisabled = isTruthy(env.DISABLE_GOOGLE_AUTH) - -/** - * Is GitHub OAuth login disabled - * When true, the GitHub OAuth login button is hidden even when credentials are configured - */ -export const isGithubAuthDisabled = isTruthy(env.DISABLE_GITHUB_AUTH) - -/** - * Is Microsoft OAuth login disabled - * When true, the Microsoft OAuth login button is hidden even when credentials are configured - */ -export const isMicrosoftAuthDisabled = isTruthy(env.DISABLE_MICROSOFT_AUTH) - -/** - * Is email/password signup disabled - * When true, new registrations via email/password are blocked at the server level. - * Existing users can still sign in with email/password. - */ -export const isEmailSignupDisabled = isTruthy(env.DISABLE_EMAIL_SIGNUP) - -/** - * Is React Grab enabled for UI element debugging - * When true and in development mode, enables React Grab for copying UI element context to clipboard - */ -export const isReactGrabEnabled = isDev && isTruthy(env.REACT_GRAB_ENABLED) - -/** - * Is React Scan enabled for performance debugging - * When true and in development mode, enables React Scan for detecting render performance issues - */ -export const isReactScanEnabled = isDev && isTruthy(env.REACT_SCAN_ENABLED) - -/** - * Returns the parsed allowlist of integration block types from the environment variable. - * If not set or empty, returns null (meaning all integrations are allowed). - */ -export function getAllowedIntegrationsFromEnv(): string[] | null { - if (!env.ALLOWED_INTEGRATIONS) return null - const parsed = env.ALLOWED_INTEGRATIONS.split(',') - .map((i) => i.trim().toLowerCase()) - .filter(Boolean) - return parsed.length > 0 ? parsed : null +interface FeatureFlagDefinition { + description: string + /** Env/secret key consulted when AppConfig isn't the source of truth. Truthy ⇒ on. */ + fallback: keyof typeof env } -/** - * Returns the list of blacklisted provider IDs from the environment variable. - * If not set or empty, returns an empty array (meaning no providers are blacklisted). - */ -export function getBlacklistedProvidersFromEnv(): string[] { - if (!env.BLACKLISTED_PROVIDERS) return [] - return env.BLACKLISTED_PROVIDERS.split(',') - .map((p) => p.trim().toLowerCase()) - .filter(Boolean) -} +/** The single registry of known flags. To add a flag, add one entry here. */ +const FEATURE_FLAGS = { + // 'new-canvas': { + // description: 'New canvas renderer', + // fallback: 'NEW_CANVAS_ENABLED', + // }, +} satisfies Record /** - * Normalizes a domain entry from the ALLOWED_MCP_DOMAINS env var. - * Accepts bare hostnames (e.g., "mcp.company.com") or full URLs (e.g., "https://mcp.company.com"). - * Extracts the hostname in either case. + * The closed set of known feature flags. Derived from the registry, so a flag + * cannot exist — or be checked — without a definition (and its mandatory fallback). */ -function normalizeDomainEntry(entry: string): string { - const trimmed = entry.trim().toLowerCase() - if (!trimmed) return '' - if (trimmed.includes('://')) { - try { - return new URL(trimmed).hostname - } catch { - return trimmed - } +export type FeatureFlagName = keyof typeof FEATURE_FLAGS + +/** Build the fallback document from each flag's secret. Truthy secret ⇒ enabled. */ +function fallbackFlags(): FeatureFlagsConfig { + const flags: Record = {} + for (const [name, def] of Object.entries(FEATURE_FLAGS) as Array< + [string, FeatureFlagDefinition] + >) { + flags[name] = { enabled: isTruthy(env[def.fallback]) } } - return trimmed + return { flags } +} + +function normalizeIds(values: unknown): string[] | undefined { + if (!Array.isArray(values)) return undefined + const ids = Array.from(new Set(values.map((v) => String(v).trim()).filter(Boolean))) + return ids.length > 0 ? ids : undefined +} + +function normalizeRule(value: unknown): FeatureFlagRule | null { + if (!value || typeof value !== 'object') return null + const obj = value as Record + const rule: FeatureFlagRule = {} + if (typeof obj.enabled === 'boolean') rule.enabled = obj.enabled + if (typeof obj.admins === 'boolean') rule.admins = obj.admins + const orgIds = normalizeIds(obj.orgIds) + if (orgIds) rule.orgIds = orgIds + const userIds = normalizeIds(obj.userIds) + if (userIds) rule.userIds = userIds + return rule +} + +/** Coerce an arbitrary AppConfig/JSON value into a config, dropping malformed entries. */ +function parseConfig(json: unknown): FeatureFlagsConfig { + const obj = (json && typeof json === 'object' ? json : {}) as Record + const rawFlags = (obj.flags && typeof obj.flags === 'object' ? obj.flags : {}) as Record< + string, + unknown + > + const flags: Record = {} + for (const [name, value] of Object.entries(rawFlags)) { + const rule = normalizeRule(value) + if (rule) flags[name] = rule + } + return { flags } } /** - * Get allowed MCP server domains from the ALLOWED_MCP_DOMAINS env var. - * Returns null if not set (all domains allowed), or parsed array of lowercase hostnames. - * Accepts both bare hostnames and full URLs in the env var value. + * Resolve platform-admin status lazily. Dynamically imported so the DB-backed + * helper (and `@sim/db`) stay out of this config module's load graph for callers + * that never reach an admin-gated flag. */ -export function getAllowedMcpDomainsFromEnv(): string[] | null { - if (!env.ALLOWED_MCP_DOMAINS) return null - const parsed = env.ALLOWED_MCP_DOMAINS.split(',').map(normalizeDomainEntry).filter(Boolean) - return parsed.length > 0 ? parsed : null +async function resolveAdmin(userId: string): Promise { + const { isPlatformAdmin } = await import('@/lib/permissions/super-user') + return isPlatformAdmin(userId) } /** - * Get cost multiplier based on environment + * The admin clause is resolved last and lazily: a global/userId/orgId match + * short-circuits before any DB read, a rule without `admins` never queries, and a + * missing `userId` resolves to `false` without a query. */ -export function getCostMultiplier(): number { - return isProd ? (env.COST_MULTIPLIER ?? 1) : 1 +async function evaluate( + rule: FeatureFlagRule | undefined, + ctx: FeatureFlagContext +): Promise { + if (!rule) return false + if (rule.enabled) return true + if (ctx.userId && rule.userIds?.includes(ctx.userId)) return true + if (ctx.orgId && rule.orgIds?.includes(ctx.orgId)) return true + if (rule.admins) { + const admin = ctx.isAdmin ?? (ctx.userId ? await resolveAdmin(ctx.userId) : false) + if (admin) return true + } + return false +} + +/** + * Resolve the full flag document. Reads from AWS AppConfig on hosted deployments + * (cached, ~30s TTL, never blocks after the first fetch), otherwise derives each + * flag's on/off state from its registered fallback secret ({@link fallbackFlags}). + */ +export async function getFeatureFlags(): Promise { + if (!isAppConfigEnabled) return fallbackFlags() + + const value = await fetchAppConfigProfile( + { + application: env.APPCONFIG_APPLICATION as string, + environment: env.APPCONFIG_ENVIRONMENT as string, + profile: FEATURE_FLAGS_PROFILE, + }, + parseConfig + ) + + return value ?? fallbackFlags() +} + +/** Resolve a single flag for a context. Admin status is resolved internally from `userId`. */ +export async function isFeatureEnabled( + flag: FeatureFlagName, + ctx: FeatureFlagContext = {} +): Promise { + const { flags } = await getFeatureFlags() + return evaluate(flags[flag], ctx) } diff --git a/apps/sim/lib/core/execution-limits/types.ts b/apps/sim/lib/core/execution-limits/types.ts index 774e87ac1e..ae1174aadb 100644 --- a/apps/sim/lib/core/execution-limits/types.ts +++ b/apps/sim/lib/core/execution-limits/types.ts @@ -1,6 +1,6 @@ import { getPlanTypeForLimits } from '@/lib/billing/plan-helpers' import { env } from '@/lib/core/config/env' -import { isBillingEnabled } from '@/lib/core/config/feature-flags' +import { isBillingEnabled } from '@/lib/core/config/env-flags' import type { SubscriptionPlan } from '@/lib/core/rate-limiter/types' interface ExecutionTimeoutConfig { diff --git a/apps/sim/lib/core/rate-limiter/rate-limiter.test.ts b/apps/sim/lib/core/rate-limiter/rate-limiter.test.ts index 23f0f90b89..a53a29b77a 100644 --- a/apps/sim/lib/core/rate-limiter/rate-limiter.test.ts +++ b/apps/sim/lib/core/rate-limiter/rate-limiter.test.ts @@ -3,7 +3,7 @@ import { RateLimiter } from './rate-limiter' import type { ConsumeResult, RateLimitStorageAdapter, TokenStatus } from './storage' import { MANUAL_EXECUTION_LIMIT, RATE_LIMITS, RateLimitError } from './types' -vi.mock('@/lib/core/config/feature-flags', () => ({ isBillingEnabled: true })) +vi.mock('@/lib/core/config/env-flags', () => ({ isBillingEnabled: true })) interface MockAdapter { consumeTokens: Mock diff --git a/apps/sim/lib/core/rate-limiter/types.ts b/apps/sim/lib/core/rate-limiter/types.ts index 268b5e8780..dbf023af41 100644 --- a/apps/sim/lib/core/rate-limiter/types.ts +++ b/apps/sim/lib/core/rate-limiter/types.ts @@ -1,6 +1,6 @@ import { getPlanTypeForLimits } from '@/lib/billing/plan-helpers' import { env } from '@/lib/core/config/env' -import { isBillingEnabled } from '@/lib/core/config/feature-flags' +import { isBillingEnabled } from '@/lib/core/config/env-flags' import type { CoreTriggerType } from '@/stores/logs/filters/types' import type { TokenBucketConfig } from './storage' diff --git a/apps/sim/lib/core/security/csp.test.ts b/apps/sim/lib/core/security/csp.test.ts index 9f31550226..a91c97d650 100644 --- a/apps/sim/lib/core/security/csp.test.ts +++ b/apps/sim/lib/core/security/csp.test.ts @@ -1,4 +1,4 @@ -import { createEnvMock, featureFlagsMock } from '@sim/testing' +import { createEnvMock, envFlagsMock } from '@sim/testing' import { afterEach, describe, expect, it, vi } from 'vitest' vi.mock('@/lib/core/config/env', () => @@ -17,7 +17,7 @@ vi.mock('@/lib/core/config/env', () => }) ) -vi.mock('@/lib/core/config/feature-flags', () => featureFlagsMock) +vi.mock('@/lib/core/config/env-flags', () => envFlagsMock) import { addCSPSource, diff --git a/apps/sim/lib/core/security/csp.ts b/apps/sim/lib/core/security/csp.ts index 2dc60412c6..01ba261f8f 100644 --- a/apps/sim/lib/core/security/csp.ts +++ b/apps/sim/lib/core/security/csp.ts @@ -1,5 +1,5 @@ import { env, getEnv } from '../config/env' -import { isDev, isHosted, isReactGrabEnabled } from '../config/feature-flags' +import { isDev, isHosted, isReactGrabEnabled } from '../config/env-flags' /** * Content Security Policy (CSP) configuration builder diff --git a/apps/sim/lib/core/security/deployment.ts b/apps/sim/lib/core/security/deployment.ts index aabab928be..7bc03b2537 100644 --- a/apps/sim/lib/core/security/deployment.ts +++ b/apps/sim/lib/core/security/deployment.ts @@ -3,7 +3,7 @@ import { sha256Hex } from '@sim/security/hash' import { hmacSha256Hex } from '@sim/security/hmac' import type { NextResponse } from 'next/server' import { env } from '@/lib/core/config/env' -import { isDev } from '@/lib/core/config/feature-flags' +import { isDev } from '@/lib/core/config/env-flags' /** * Shared authentication utilities for deployed chat endpoints. diff --git a/apps/sim/lib/core/security/input-validation.server.ts b/apps/sim/lib/core/security/input-validation.server.ts index 81fbfe75e5..bd344e3899 100644 --- a/apps/sim/lib/core/security/input-validation.server.ts +++ b/apps/sim/lib/core/security/input-validation.server.ts @@ -6,7 +6,7 @@ import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' import * as ipaddr from 'ipaddr.js' import { Agent, type RequestInit as UndiciRequestInit, fetch as undiciFetch } from 'undici' -import { isHosted } from '@/lib/core/config/feature-flags' +import { isHosted } from '@/lib/core/config/env-flags' import { type ValidationResult, validateExternalUrl } from '@/lib/core/security/input-validation' import { PayloadSizeLimitError } from '@/lib/core/utils/stream-limits' diff --git a/apps/sim/lib/core/security/input-validation.test.ts b/apps/sim/lib/core/security/input-validation.test.ts index 55b07a72db..7e8be4caa1 100644 --- a/apps/sim/lib/core/security/input-validation.test.ts +++ b/apps/sim/lib/core/security/input-validation.test.ts @@ -1,4 +1,4 @@ -import { featureFlagsMock } from '@sim/testing' +import { envFlagsMock } from '@sim/testing' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { validateAirtableId, @@ -32,7 +32,7 @@ import { } from '@/lib/core/security/input-validation.server' import { sanitizeForLogging } from '@/lib/core/security/redaction' -vi.mock('@/lib/core/config/feature-flags', () => featureFlagsMock) +vi.mock('@/lib/core/config/env-flags', () => envFlagsMock) describe('validatePathSegment', () => { describe('valid inputs', () => { diff --git a/apps/sim/lib/core/security/input-validation.ts b/apps/sim/lib/core/security/input-validation.ts index 57ba5939b9..fb73b30056 100644 --- a/apps/sim/lib/core/security/input-validation.ts +++ b/apps/sim/lib/core/security/input-validation.ts @@ -1,6 +1,6 @@ import { createLogger } from '@sim/logger' import * as ipaddr from 'ipaddr.js' -import { isHosted } from '@/lib/core/config/feature-flags' +import { isHosted } from '@/lib/core/config/env-flags' const logger = createLogger('InputValidation') diff --git a/apps/sim/lib/core/security/pinned-fetch.server.test.ts b/apps/sim/lib/core/security/pinned-fetch.server.test.ts index d63bad257e..e07d0d412d 100644 --- a/apps/sim/lib/core/security/pinned-fetch.server.test.ts +++ b/apps/sim/lib/core/security/pinned-fetch.server.test.ts @@ -1,7 +1,7 @@ /** * @vitest-environment node */ -import { featureFlagsMock } from '@sim/testing' +import { envFlagsMock } from '@sim/testing' import { beforeEach, describe, expect, it, vi } from 'vitest' const { mockAgent, mockUndiciFetch, capturedAgentOptions, agentCloses } = vi.hoisted(() => { @@ -25,7 +25,7 @@ const { mockAgent, mockUndiciFetch, capturedAgentOptions, agentCloses } = vi.hoi }) vi.mock('undici', () => ({ Agent: mockAgent, fetch: mockUndiciFetch })) -vi.mock('@/lib/core/config/feature-flags', () => featureFlagsMock) +vi.mock('@/lib/core/config/env-flags', () => envFlagsMock) import { createPinnedFetch } from '@/lib/core/security/input-validation.server' diff --git a/apps/sim/lib/core/utils/urls.test.ts b/apps/sim/lib/core/utils/urls.test.ts index 689f5b51a9..2878a49feb 100644 --- a/apps/sim/lib/core/utils/urls.test.ts +++ b/apps/sim/lib/core/utils/urls.test.ts @@ -12,7 +12,7 @@ vi.mock('@/lib/core/config/env', () => ({ getEnv: mockGetEnv, })) -vi.mock('@/lib/core/config/feature-flags', () => ({ +vi.mock('@/lib/core/config/env-flags', () => ({ isProd: false, })) diff --git a/apps/sim/lib/core/utils/urls.ts b/apps/sim/lib/core/utils/urls.ts index 1c2da16ec5..1a014b295d 100644 --- a/apps/sim/lib/core/utils/urls.ts +++ b/apps/sim/lib/core/utils/urls.ts @@ -1,5 +1,5 @@ import { env, getEnv } from '@/lib/core/config/env' -import { isProd } from '@/lib/core/config/feature-flags' +import { isProd } from '@/lib/core/config/env-flags' /** Canonical base URL for the public-facing marketing site. No trailing slash. */ export const SITE_URL = 'https://www.sim.ai' diff --git a/apps/sim/lib/data-drains/access.ts b/apps/sim/lib/data-drains/access.ts index 6a36170f6f..5a1db37544 100644 --- a/apps/sim/lib/data-drains/access.ts +++ b/apps/sim/lib/data-drains/access.ts @@ -4,7 +4,7 @@ import { and, eq } from 'drizzle-orm' import { NextResponse } from 'next/server' import { getSession } from '@/lib/auth' import { isOrganizationOnEnterprisePlan } from '@/lib/billing/core/subscription' -import { isBillingEnabled, isDataDrainsEnabled } from '@/lib/core/config/feature-flags' +import { isBillingEnabled, isDataDrainsEnabled } from '@/lib/core/config/env-flags' interface DrainAccessSession { user: { diff --git a/apps/sim/lib/data-drains/dispatcher.test.ts b/apps/sim/lib/data-drains/dispatcher.test.ts index ffffac5147..0d94887b7d 100644 --- a/apps/sim/lib/data-drains/dispatcher.test.ts +++ b/apps/sim/lib/data-drains/dispatcher.test.ts @@ -19,7 +19,7 @@ vi.mock('@/lib/billing/core/subscription', () => ({ isOrganizationOnEnterprisePlan: mockIsEnterprise, })) vi.mock('@/lib/core/async-jobs', () => ({ getJobQueue: mockGetJobQueue })) -vi.mock('@/lib/core/config/feature-flags', () => ({ isBillingEnabled: true })) +vi.mock('@/lib/core/config/env-flags', () => ({ isBillingEnabled: true })) import { dispatchDueDrains, reapOrphanedRuns } from '@/lib/data-drains/dispatcher' diff --git a/apps/sim/lib/data-drains/dispatcher.ts b/apps/sim/lib/data-drains/dispatcher.ts index c7021ed9a6..aec56a3bf3 100644 --- a/apps/sim/lib/data-drains/dispatcher.ts +++ b/apps/sim/lib/data-drains/dispatcher.ts @@ -5,7 +5,7 @@ import { toError } from '@sim/utils/errors' import { and, eq, isNull, lt, or } from 'drizzle-orm' import { isOrganizationOnEnterprisePlan } from '@/lib/billing/core/subscription' import { getJobQueue } from '@/lib/core/async-jobs' -import { isBillingEnabled } from '@/lib/core/config/feature-flags' +import { isBillingEnabled } from '@/lib/core/config/env-flags' const logger = createLogger('DataDrainsDispatcher') diff --git a/apps/sim/lib/invitations/core.test.ts b/apps/sim/lib/invitations/core.test.ts index 2ac35c77c0..02cfaa0b2c 100644 --- a/apps/sim/lib/invitations/core.test.ts +++ b/apps/sim/lib/invitations/core.test.ts @@ -50,7 +50,7 @@ vi.mock('@/lib/workspaces/permissions/utils', () => ({ getWorkspaceWithOwner: mockGetWorkspaceWithOwner, })) -vi.mock('@/lib/core/config/feature-flags', () => ({ +vi.mock('@/lib/core/config/env-flags', () => ({ get isBillingEnabled() { return mockFeatureFlags.isBillingEnabled }, diff --git a/apps/sim/lib/invitations/core.ts b/apps/sim/lib/invitations/core.ts index 122cf5d6b5..fb2fa17388 100644 --- a/apps/sim/lib/invitations/core.ts +++ b/apps/sim/lib/invitations/core.ts @@ -26,7 +26,7 @@ import { } from '@/lib/billing/organizations/membership' import { ensureTeamOrganizationForAcceptance } from '@/lib/billing/organizations/provision-seat' import { reconcileOrganizationSeats } from '@/lib/billing/organizations/seats' -import { isBillingEnabled } from '@/lib/core/config/feature-flags' +import { isBillingEnabled } from '@/lib/core/config/env-flags' import { syncWorkspaceEnvCredentials } from '@/lib/credentials/environment' import { captureServerEvent } from '@/lib/posthog/server' import { getWorkspaceWithOwner } from '@/lib/workspaces/permissions/utils' diff --git a/apps/sim/lib/knowledge/documents/service.ts b/apps/sim/lib/knowledge/documents/service.ts index bdfe3e5d55..bce2932ebf 100644 --- a/apps/sim/lib/knowledge/documents/service.ts +++ b/apps/sim/lib/knowledge/documents/service.ts @@ -33,7 +33,7 @@ import { recordUsage } from '@/lib/billing/core/usage-log' import { checkAndBillOverageThreshold } from '@/lib/billing/threshold-billing' import type { ChunkingStrategy, StrategyOptions } from '@/lib/chunkers/types' import { env, envNumber } from '@/lib/core/config/env' -import { getCostMultiplier, isTriggerDevEnabled } from '@/lib/core/config/feature-flags' +import { getCostMultiplier, isTriggerDevEnabled } from '@/lib/core/config/env-flags' import { processDocument } from '@/lib/knowledge/documents/document-processor' import type { DocumentSortField, SortOrder } from '@/lib/knowledge/documents/types' import { getEmbeddingModelInfo } from '@/lib/knowledge/embedding-models' diff --git a/apps/sim/lib/knowledge/reranker.ts b/apps/sim/lib/knowledge/reranker.ts index e9b0fea850..4c20847f53 100644 --- a/apps/sim/lib/knowledge/reranker.ts +++ b/apps/sim/lib/knowledge/reranker.ts @@ -2,7 +2,7 @@ import { createLogger } from '@sim/logger' import { getBYOKKey } from '@/lib/api-key/byok' import { getRotatingApiKey } from '@/lib/core/config/api-keys' import { env } from '@/lib/core/config/env' -import { isHosted } from '@/lib/core/config/feature-flags' +import { isHosted } from '@/lib/core/config/env-flags' import { isRetryableError, retryWithExponentialBackoff } from '@/lib/knowledge/documents/utils' import { isSupportedRerankerModel } from '@/lib/knowledge/reranker-models' diff --git a/apps/sim/lib/logs/execution/logger.test.ts b/apps/sim/lib/logs/execution/logger.test.ts index 3acb14dcae..923542fde1 100644 --- a/apps/sim/lib/logs/execution/logger.test.ts +++ b/apps/sim/lib/logs/execution/logger.test.ts @@ -1,4 +1,4 @@ -import { featureFlagsMock } from '@sim/testing' +import { envFlagsMock } from '@sim/testing' import { beforeEach, describe, expect, test, vi } from 'vitest' import { recordUsage } from '@/lib/billing/core/usage-log' import { ExecutionLogger } from '@/lib/logs/execution/logger' @@ -60,7 +60,7 @@ vi.mock('@/lib/billing/threshold-billing', () => ({ checkAndBillOverageThreshold: vi.fn(() => Promise.resolve()), })) -vi.mock('@/lib/core/config/feature-flags', () => featureFlagsMock) +vi.mock('@/lib/core/config/env-flags', () => envFlagsMock) // Mock security module vi.mock('@/lib/core/security/redaction', () => ({ diff --git a/apps/sim/lib/logs/execution/logger.ts b/apps/sim/lib/logs/execution/logger.ts index 9733a5550d..e8c6edd7c5 100644 --- a/apps/sim/lib/logs/execution/logger.ts +++ b/apps/sim/lib/logs/execution/logger.ts @@ -25,7 +25,7 @@ import { stableEventKey, } from '@/lib/billing/core/usage-log' import { checkAndBillOverageThreshold } from '@/lib/billing/threshold-billing' -import { isBillingEnabled } from '@/lib/core/config/feature-flags' +import { isBillingEnabled } from '@/lib/core/config/env-flags' import { redactApiKeys } from '@/lib/core/security/redaction' import { filterForDisplay } from '@/lib/core/utils/display-filters' import { diff --git a/apps/sim/lib/mcp/connection-manager.test.ts b/apps/sim/lib/mcp/connection-manager.test.ts index 8b48392e52..f4845b099e 100644 --- a/apps/sim/lib/mcp/connection-manager.test.ts +++ b/apps/sim/lib/mcp/connection-manager.test.ts @@ -40,7 +40,7 @@ const { mockGetOrCreateOauthRow: vi.fn(), })) -vi.mock('@/lib/core/config/feature-flags', () => ({ isTest: false })) +vi.mock('@/lib/core/config/env-flags', () => ({ isTest: false })) vi.mock('@/lib/mcp/pubsub', () => ({ mcpPubSub: { onToolsChanged: mockOnToolsChanged, diff --git a/apps/sim/lib/mcp/connection-manager.ts b/apps/sim/lib/mcp/connection-manager.ts index 158983739b..78b8acc265 100644 --- a/apps/sim/lib/mcp/connection-manager.ts +++ b/apps/sim/lib/mcp/connection-manager.ts @@ -12,7 +12,7 @@ import { createLogger } from '@sim/logger' import { backoffWithJitter } from '@sim/utils/retry' -import { isTest } from '@/lib/core/config/feature-flags' +import { isTest } from '@/lib/core/config/env-flags' import { McpClient } from '@/lib/mcp/client' import { getOrCreateOauthRow, loadPreregisteredClient, SimMcpOauthProvider } from '@/lib/mcp/oauth' import { mcpPubSub } from '@/lib/mcp/pubsub' diff --git a/apps/sim/lib/mcp/domain-check.test.ts b/apps/sim/lib/mcp/domain-check.test.ts index ff559caa8c..22497bfe54 100644 --- a/apps/sim/lib/mcp/domain-check.test.ts +++ b/apps/sim/lib/mcp/domain-check.test.ts @@ -10,7 +10,7 @@ const { mockGetAllowedMcpDomainsFromEnv, mockDnsLookup, hostedFlag } = vi.hoiste hostedFlag: { value: false }, })) -vi.mock('@/lib/core/config/feature-flags', () => ({ +vi.mock('@/lib/core/config/env-flags', () => ({ getAllowedMcpDomainsFromEnv: mockGetAllowedMcpDomainsFromEnv, get isHosted() { return hostedFlag.value diff --git a/apps/sim/lib/mcp/domain-check.ts b/apps/sim/lib/mcp/domain-check.ts index 9e57b23c7f..fcc721203c 100644 --- a/apps/sim/lib/mcp/domain-check.ts +++ b/apps/sim/lib/mcp/domain-check.ts @@ -2,7 +2,7 @@ import dns from 'dns/promises' import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' import * as ipaddr from 'ipaddr.js' -import { getAllowedMcpDomainsFromEnv, isHosted } from '@/lib/core/config/feature-flags' +import { getAllowedMcpDomainsFromEnv, isHosted } from '@/lib/core/config/env-flags' import { isPrivateOrReservedIP } from '@/lib/core/security/input-validation.server' import { createEnvVarPattern } from '@/executor/utils/reference-validation' diff --git a/apps/sim/lib/mcp/service.ts b/apps/sim/lib/mcp/service.ts index 4ef5338248..12c30e7c8c 100644 --- a/apps/sim/lib/mcp/service.ts +++ b/apps/sim/lib/mcp/service.ts @@ -6,7 +6,7 @@ import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' import { sleep } from '@sim/utils/helpers' import { and, eq, isNull } from 'drizzle-orm' -import { isTest } from '@/lib/core/config/feature-flags' +import { isTest } from '@/lib/core/config/env-flags' import { generateRequestId } from '@/lib/core/utils/request' import { McpClient } from '@/lib/mcp/client' import { mcpConnectionManager } from '@/lib/mcp/connection-manager' diff --git a/apps/sim/lib/messaging/lifecycle.ts b/apps/sim/lib/messaging/lifecycle.ts index 5d6da19881..95b109d9cb 100644 --- a/apps/sim/lib/messaging/lifecycle.ts +++ b/apps/sim/lib/messaging/lifecycle.ts @@ -1,7 +1,7 @@ import { createLogger } from '@sim/logger' import { tasks } from '@trigger.dev/sdk' import { env } from '@/lib/core/config/env' -import { isTriggerDevEnabled } from '@/lib/core/config/feature-flags' +import { isTriggerDevEnabled } from '@/lib/core/config/env-flags' const logger = createLogger('LifecycleEmail') diff --git a/apps/sim/lib/mothership/inbox/executor.ts b/apps/sim/lib/mothership/inbox/executor.ts index 1e25907211..e82025d853 100644 --- a/apps/sim/lib/mothership/inbox/executor.ts +++ b/apps/sim/lib/mothership/inbox/executor.ts @@ -16,7 +16,7 @@ import { chatPubSub } from '@/lib/copilot/chat-status' import { runHeadlessCopilotLifecycle } from '@/lib/copilot/request/lifecycle/headless' import { requestChatTitle } from '@/lib/copilot/request/lifecycle/start' import type { OrchestratorResult } from '@/lib/copilot/request/types' -import { isE2BDocEnabled, isHosted } from '@/lib/core/config/feature-flags' +import { isE2BDocEnabled, isHosted } from '@/lib/core/config/env-flags' import * as agentmail from '@/lib/mothership/inbox/agentmail-client' import { formatEmailAsMessage } from '@/lib/mothership/inbox/format' import { sendInboxResponse } from '@/lib/mothership/inbox/response' diff --git a/apps/sim/lib/permissions/super-user.ts b/apps/sim/lib/permissions/super-user.ts index 953a2ea157..597ca135e4 100644 --- a/apps/sim/lib/permissions/super-user.ts +++ b/apps/sim/lib/permissions/super-user.ts @@ -1,4 +1,4 @@ -import { db } from '@sim/db' +import { db, dbReplica } from '@sim/db' import { settings, user } from '@sim/db/schema' import { eq } from 'drizzle-orm' @@ -35,3 +35,19 @@ export async function verifyEffectiveSuperUser(userId: string): Promise<{ superUserModeEnabled, } } + +/** + * True when the user is a platform admin (`role === 'admin'`). A single-column read + * served from the replica: this gates features, not security-critical auth, so it + * tolerates the replica's bounded staleness (admin role rarely changes). Falls back + * to the primary when no replica is configured. + */ +export async function isPlatformAdmin(userId: string): Promise { + const [row] = await dbReplica + .select({ role: user.role }) + .from(user) + .where(eq(user.id, userId)) + .limit(1) + + return row?.role === 'admin' +} diff --git a/apps/sim/lib/table/__tests__/lock-order.test.ts b/apps/sim/lib/table/__tests__/lock-order.test.ts index 51c66714df..9a7adbb6e8 100644 --- a/apps/sim/lib/table/__tests__/lock-order.test.ts +++ b/apps/sim/lib/table/__tests__/lock-order.test.ts @@ -15,7 +15,7 @@ import type { TableDefinition } from '@/lib/table/types' vi.mock('@sim/db', () => dbChainMock) -vi.mock('@/lib/core/config/feature-flags', () => ({ +vi.mock('@/lib/core/config/env-flags', () => ({ isTablesFractionalOrderingEnabled: false, })) diff --git a/apps/sim/lib/table/__tests__/update-row.test.ts b/apps/sim/lib/table/__tests__/update-row.test.ts index e43fa16a4b..2d18842949 100644 --- a/apps/sim/lib/table/__tests__/update-row.test.ts +++ b/apps/sim/lib/table/__tests__/update-row.test.ts @@ -18,7 +18,7 @@ vi.mock('@sim/db', () => dbChainMock) // These suites assert flag-off position-shift semantics; pin the flag so they're // deterministic regardless of a local TABLES_FRACTIONAL_ORDERING env value. -vi.mock('@/lib/core/config/feature-flags', () => ({ +vi.mock('@/lib/core/config/env-flags', () => ({ isTablesFractionalOrderingEnabled: false, })) diff --git a/apps/sim/lib/table/backfill-runner.ts b/apps/sim/lib/table/backfill-runner.ts index f9e00bdb46..387c89e145 100644 --- a/apps/sim/lib/table/backfill-runner.ts +++ b/apps/sim/lib/table/backfill-runner.ts @@ -4,7 +4,7 @@ import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' import { generateId } from '@sim/utils/id' import { and, asc, count, eq, gt, inArray } from 'drizzle-orm' -import { isTriggerDevEnabled } from '@/lib/core/config/feature-flags' +import { isTriggerDevEnabled } from '@/lib/core/config/env-flags' import { runDetached } from '@/lib/core/utils/background' import { MATERIALIZE_CONCURRENCY, mapWithConcurrency } from '@/lib/core/utils/concurrency' import { materializeExecutionData } from '@/lib/logs/execution/trace-store' diff --git a/apps/sim/lib/table/rows/ordering.ts b/apps/sim/lib/table/rows/ordering.ts index c140874432..d0403f9bcd 100644 --- a/apps/sim/lib/table/rows/ordering.ts +++ b/apps/sim/lib/table/rows/ordering.ts @@ -9,7 +9,7 @@ import { db } from '@sim/db' import { userTableRows } from '@sim/db/schema' import { and, asc, desc, eq, gt, gte, inArray, lt, lte, type SQL, sql } from 'drizzle-orm' -import { isTablesFractionalOrderingEnabled } from '@/lib/core/config/feature-flags' +import { isTablesFractionalOrderingEnabled } from '@/lib/core/config/env-flags' import type { DbOrTx } from '@/lib/db/types' import { TABLE_LIMITS } from '@/lib/table/constants' import { keyBetween, nKeysBetween } from '@/lib/table/order-key' diff --git a/apps/sim/lib/table/rows/service.ts b/apps/sim/lib/table/rows/service.ts index 5f08305ade..152f1730cc 100644 --- a/apps/sim/lib/table/rows/service.ts +++ b/apps/sim/lib/table/rows/service.ts @@ -16,7 +16,7 @@ import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' import { generateId } from '@sim/utils/id' import { and, count, eq, inArray, lte, notInArray, type SQL, sql } from 'drizzle-orm' -import { isTablesFractionalOrderingEnabled } from '@/lib/core/config/feature-flags' +import { isTablesFractionalOrderingEnabled } from '@/lib/core/config/env-flags' import { getColumnId } from '@/lib/table/column-keys' import { TABLE_LIMITS, USER_TABLE_ROWS_SQL_NAME } from '@/lib/table/constants' import { nKeysBetween } from '@/lib/table/order-key' diff --git a/apps/sim/lib/table/workflow-columns.ts b/apps/sim/lib/table/workflow-columns.ts index 231f020d44..bfa7de1a36 100644 --- a/apps/sim/lib/table/workflow-columns.ts +++ b/apps/sim/lib/table/workflow-columns.ts @@ -16,7 +16,7 @@ import { toError } from '@sim/utils/errors' import { generateId } from '@sim/utils/id' import { and, eq, inArray, notInArray, sql } from 'drizzle-orm' import type { EnqueueOptions } from '@/lib/core/async-jobs/types' -import { isTriggerDevEnabled } from '@/lib/core/config/feature-flags' +import { isTriggerDevEnabled } from '@/lib/core/config/env-flags' import { buildCancelledExecution } from '@/lib/table/cell-write' import type { Filter, diff --git a/apps/sim/lib/webhooks/processor.test.ts b/apps/sim/lib/webhooks/processor.test.ts index 381cc6108a..60f26d86c3 100644 --- a/apps/sim/lib/webhooks/processor.test.ts +++ b/apps/sim/lib/webhooks/processor.test.ts @@ -4,9 +4,9 @@ import { createMockRequest, + envFlagsMock, executionPreprocessingMock, executionPreprocessingMockFns, - featureFlagsMock, } from '@sim/testing' import { beforeEach, describe, expect, it, vi } from 'vitest' @@ -67,7 +67,7 @@ vi.mock('@/lib/core/async-jobs', () => ({ shouldExecuteInline: mockShouldExecuteInline, })) -vi.mock('@/lib/core/config/feature-flags', () => featureFlagsMock) +vi.mock('@/lib/core/config/env-flags', () => envFlagsMock) vi.mock('@sim/security/compare', () => ({ safeCompare: vi.fn().mockReturnValue(true), diff --git a/apps/sim/lib/webhooks/processor.ts b/apps/sim/lib/webhooks/processor.ts index 5515d39d35..e261a9a77d 100644 --- a/apps/sim/lib/webhooks/processor.ts +++ b/apps/sim/lib/webhooks/processor.ts @@ -9,7 +9,7 @@ import { isOrganizationOnTeamOrEnterprisePlan } from '@/lib/billing/core/subscri import { tryAdmit } from '@/lib/core/admission/gate' import { getInlineJobQueue, getJobQueue, shouldExecuteInline } from '@/lib/core/async-jobs' import type { AsyncExecutionCorrelation } from '@/lib/core/async-jobs/types' -import { isProd } from '@/lib/core/config/feature-flags' +import { isProd } from '@/lib/core/config/env-flags' import { assertContentLengthWithinLimit, isPayloadSizeLimitError, diff --git a/apps/sim/lib/workflows/subblocks/visibility.ts b/apps/sim/lib/workflows/subblocks/visibility.ts index fb8de2121a..b49d2730df 100644 --- a/apps/sim/lib/workflows/subblocks/visibility.ts +++ b/apps/sim/lib/workflows/subblocks/visibility.ts @@ -1,5 +1,5 @@ import { getEnv, isTruthy } from '@/lib/core/config/env' -import { isHosted } from '@/lib/core/config/feature-flags' +import { isHosted } from '@/lib/core/config/env-flags' import type { SubBlockConfig } from '@/blocks/types' export type CanonicalMode = 'basic' | 'advanced' diff --git a/apps/sim/lib/workspaces/policy.test.ts b/apps/sim/lib/workspaces/policy.test.ts index 9c75eda721..0828cff428 100644 --- a/apps/sim/lib/workspaces/policy.test.ts +++ b/apps/sim/lib/workspaces/policy.test.ts @@ -58,7 +58,7 @@ vi.mock('@/lib/billing/core/plan', () => ({ getHighestPrioritySubscription: mockGetHighestPrioritySubscription, })) -vi.mock('@/lib/core/config/feature-flags', () => ({ +vi.mock('@/lib/core/config/env-flags', () => ({ get isBillingEnabled() { return mockFeatureFlags.isBillingEnabled }, diff --git a/apps/sim/lib/workspaces/policy.ts b/apps/sim/lib/workspaces/policy.ts index 26cd530ea9..641dd15e9a 100644 --- a/apps/sim/lib/workspaces/policy.ts +++ b/apps/sim/lib/workspaces/policy.ts @@ -8,7 +8,7 @@ import { getUserOrganization } from '@/lib/billing/organizations/membership' import type { PlanCategory } from '@/lib/billing/plan-helpers' import { getPlanType, isEnterprise, isMax, isPro, isTeam } from '@/lib/billing/plan-helpers' import { hasUsableSubscriptionStatus } from '@/lib/billing/subscriptions/utils' -import { isBillingEnabled } from '@/lib/core/config/feature-flags' +import { isBillingEnabled } from '@/lib/core/config/env-flags' import { UPGRADE_TO_INVITE_REASON } from '@/lib/workspaces/policy-constants' const logger = createLogger('WorkspacePolicy') diff --git a/apps/sim/next.config.ts b/apps/sim/next.config.ts index 204ada466e..bb6e939f4b 100644 --- a/apps/sim/next.config.ts +++ b/apps/sim/next.config.ts @@ -1,6 +1,6 @@ import type { NextConfig } from 'next' import { env, isTruthy } from './lib/core/config/env' -import { isDev } from './lib/core/config/feature-flags' +import { isDev } from './lib/core/config/env-flags' import { getChatEmbedCSPPolicy, getMainCSPPolicy, diff --git a/apps/sim/providers/index.test.ts b/apps/sim/providers/index.test.ts index 19d0a41ac8..73b62a79ab 100644 --- a/apps/sim/providers/index.test.ts +++ b/apps/sim/providers/index.test.ts @@ -18,7 +18,7 @@ vi.mock('@/providers/registry', () => ({ }), })) -vi.mock('@/lib/core/config/feature-flags', () => ({ +vi.mock('@/lib/core/config/env-flags', () => ({ getCostMultiplier: vi.fn(() => 1), })) diff --git a/apps/sim/providers/index.ts b/apps/sim/providers/index.ts index 19dcb5fda6..26433940e3 100644 --- a/apps/sim/providers/index.ts +++ b/apps/sim/providers/index.ts @@ -1,7 +1,7 @@ import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' import { getApiKeyWithBYOK } from '@/lib/api-key/byok' -import { getCostMultiplier } from '@/lib/core/config/feature-flags' +import { getCostMultiplier } from '@/lib/core/config/env-flags' import type { StreamingExecution } from '@/executor/types' import { getProviderExecutor } from '@/providers/registry' import type { ProviderId, ProviderRequest, ProviderResponse } from '@/providers/types' diff --git a/apps/sim/providers/utils.test.ts b/apps/sim/providers/utils.test.ts index 46d414cbb6..9ed017b581 100644 --- a/apps/sim/providers/utils.test.ts +++ b/apps/sim/providers/utils.test.ts @@ -1,5 +1,5 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import * as environmentModule from '@/lib/core/config/feature-flags' +import * as environmentModule from '@/lib/core/config/env-flags' import { calculateCost, extractAndParseJSON, diff --git a/apps/sim/providers/utils.ts b/apps/sim/providers/utils.ts index c584261b4e..2c22c865e4 100644 --- a/apps/sim/providers/utils.ts +++ b/apps/sim/providers/utils.ts @@ -5,7 +5,7 @@ import type { ChatCompletionChunk } from 'openai/resources/chat/completions' import type { CompletionUsage } from 'openai/resources/completions' import { formatCreditCost } from '@/lib/billing/credits/conversion' import { env } from '@/lib/core/config/env' -import { getBlacklistedProvidersFromEnv, isHosted } from '@/lib/core/config/feature-flags' +import { getBlacklistedProvidersFromEnv, isHosted } from '@/lib/core/config/env-flags' import { normalizeRecord, normalizeStringRecord, diff --git a/apps/sim/proxy.ts b/apps/sim/proxy.ts index 0bf9114699..1d5e058158 100644 --- a/apps/sim/proxy.ts +++ b/apps/sim/proxy.ts @@ -3,7 +3,7 @@ import { getSessionCookie } from 'better-auth/cookies' import { type NextRequest, NextResponse } from 'next/server' import { sendToProfound } from './lib/analytics/profound' import { getEnv } from './lib/core/config/env' -import { isAuthDisabled, isHosted } from './lib/core/config/feature-flags' +import { isAuthDisabled, isHosted } from './lib/core/config/env-flags' import { generateRuntimeCSP } from './lib/core/security/csp' import { getClientIp } from './lib/core/utils/request' diff --git a/apps/sim/scripts/process-docs.ts b/apps/sim/scripts/process-docs.ts index bef7938ccb..1d8c5cf355 100644 --- a/apps/sim/scripts/process-docs.ts +++ b/apps/sim/scripts/process-docs.ts @@ -6,7 +6,7 @@ import { docsEmbeddings } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { sql } from 'drizzle-orm' import { type DocChunk, DocsChunker } from '@/lib/chunkers' -import { isDev } from '@/lib/core/config/feature-flags' +import { isDev } from '@/lib/core/config/env-flags' const logger = createLogger('ProcessDocs') diff --git a/apps/sim/tools/index.test.ts b/apps/sim/tools/index.test.ts index fc22a66fab..0b4d4ccbd5 100644 --- a/apps/sim/tools/index.test.ts +++ b/apps/sim/tools/index.test.ts @@ -51,7 +51,7 @@ const mockSecureFetchWithPinnedIP = inputValidationMockFns.mockSecureFetchWithPi const mockValidateUrlWithDNS = inputValidationMockFns.mockValidateUrlWithDNS // Mock feature flags -vi.mock('@/lib/core/config/feature-flags', () => ({ +vi.mock('@/lib/core/config/env-flags', () => ({ get isHosted() { return mockIsHosted.value }, diff --git a/apps/sim/tools/index.ts b/apps/sim/tools/index.ts index ca122a6c05..28eb17aaeb 100644 --- a/apps/sim/tools/index.ts +++ b/apps/sim/tools/index.ts @@ -4,7 +4,7 @@ import { sleep } from '@sim/utils/helpers' import { randomFloat } from '@sim/utils/random' import { getBYOKKey } from '@/lib/api-key/byok' import { generateInternalToken } from '@/lib/auth/internal' -import { isHosted } from '@/lib/core/config/feature-flags' +import { isHosted } from '@/lib/core/config/env-flags' import { DEFAULT_EXECUTION_TIMEOUT_MS, getMaxExecutionTimeout } from '@/lib/core/execution-limits' import { getHostedKeyRateLimiter } from '@/lib/core/rate-limiter' import { diff --git a/apps/sim/tools/supabase/utils.test.ts b/apps/sim/tools/supabase/utils.test.ts index ac649b008f..b12e98ac42 100644 --- a/apps/sim/tools/supabase/utils.test.ts +++ b/apps/sim/tools/supabase/utils.test.ts @@ -1,10 +1,10 @@ /** * @vitest-environment node */ -import { featureFlagsMock } from '@sim/testing' +import { envFlagsMock } from '@sim/testing' import { describe, expect, it, vi } from 'vitest' -vi.mock('@/lib/core/config/feature-flags', () => featureFlagsMock) +vi.mock('@/lib/core/config/env-flags', () => envFlagsMock) import { supabaseBaseUrl } from '@/tools/supabase/utils' diff --git a/packages/testing/src/mocks/feature-flags.mock.ts b/packages/testing/src/mocks/env-flags.mock.ts similarity index 88% rename from packages/testing/src/mocks/feature-flags.mock.ts rename to packages/testing/src/mocks/env-flags.mock.ts index da512c99a8..02be7bfbbf 100644 --- a/packages/testing/src/mocks/feature-flags.mock.ts +++ b/packages/testing/src/mocks/env-flags.mock.ts @@ -1,15 +1,15 @@ import { vi } from 'vitest' /** - * Static mock module for `@/lib/core/config/feature-flags`. + * Static mock module for `@/lib/core/config/env-flags`. * All boolean flags default to `false` for safe test isolation. * * @example * ```ts - * vi.mock('@/lib/core/config/feature-flags', () => featureFlagsMock) + * vi.mock('@/lib/core/config/env-flags', () => envFlagsMock) * ``` */ -export const featureFlagsMock = { +export const envFlagsMock = { isProd: false, isDev: false, isTest: true, diff --git a/packages/testing/src/mocks/index.ts b/packages/testing/src/mocks/index.ts index e8e4de49e2..7bff861785 100644 --- a/packages/testing/src/mocks/index.ts +++ b/packages/testing/src/mocks/index.ts @@ -52,13 +52,13 @@ export { export { encryptionMock, encryptionMockFns } from './encryption.mock' // Env mocks export { createEnvMock, createMockGetEnv, defaultMockEnv, envMock } from './env.mock' +// Env flag mocks +export { envFlagsMock } from './env-flags.mock' // Execution preprocessing mocks (for @/lib/execution/preprocessing) export { executionPreprocessingMock, executionPreprocessingMockFns, } from './execution-preprocessing.mock' -// Feature flag mocks -export { featureFlagsMock } from './feature-flags.mock' // Executor mocks - use side-effect import: import '@sim/testing/mocks/executor' // Fetch mocks export { diff --git a/scripts/generate-mship-contracts.ts b/scripts/generate-mship-contracts.ts index 4d0a990708..a789f0825f 100644 --- a/scripts/generate-mship-contracts.ts +++ b/scripts/generate-mship-contracts.ts @@ -9,8 +9,7 @@ // old per-script `--check`, but accounts for post-generate formatting. import { spawnSync } from 'node:child_process' -import { copyFileSync, cpSync, mkdirSync, mkdtempSync, readFileSync, rmSync } from 'node:fs' -import { tmpdir } from 'node:os' +import { readFileSync } from 'node:fs' import { dirname, join, resolve } from 'node:path' import { fileURLToPath } from 'node:url' @@ -103,9 +102,7 @@ function runCheck(): void { } if (stale.length > 0) { - console.error( - `Generated contracts are stale: ${stale.join(', ')}. Run: bun run mship:generate`, - ) + console.error(`Generated contracts are stale: ${stale.join(', ')}. Run: bun run mship:generate`) process.exit(1) } console.log('All generated contracts up to date.')