Files
WeHub Mirror 6bf8bebf51
CI / Test and Build (push) Failing after 1s
CI / Migrate Dev DB (push) Has been skipped
CI / Migrate DB (push) Has been skipped
CodeQL / Analyze actions (push) Has been cancelled
CodeQL / Analyze javascript-typescript (push) Has been cancelled
CI / Detect Version (push) Has been cancelled
CI / Detect Desktop Changes (push) Has been cancelled
CI / Build AMD64 (blacksmith-2vcpu-ubuntu-2404, ./docker/cron.Dockerfile, ubuntu-latest, ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Build AMD64 (blacksmith-2vcpu-ubuntu-2404, ./docker/db.Dockerfile, ECR_MIGRATIONS, ubuntu-latest, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (blacksmith-4vcpu-ubuntu-2404, ./docker/pii.Dockerfile, ECR_PII, ubuntu-latest, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (blacksmith-4vcpu-ubuntu-2404, ./docker/realtime.Dockerfile, ECR_REALTIME, ubuntu-latest, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build AMD64 (blacksmith-8vcpu-ubuntu-2404, ./docker/app.Dockerfile, ECR_APP, linux-x64-8-core, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/cron.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/db.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/pii.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/realtime.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-8vcpu-ubuntu-2404-arm, ./docker/app.Dockerfile, linux-arm64-8-core, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
Helm Chart / Lint, test, and validate chart (push) Has been cancelled
Helm Chart / Chart version bumped (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
CI / Build Dev ECR (blacksmith-8vcpu-ubuntu-2404, ./docker/app.Dockerfile, ECR_APP, linux-x64-8-core) (push) Has been cancelled
CI / Promote Images (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-2vcpu-ubuntu-2404, ./docker/db.Dockerfile, ECR_MIGRATIONS, ubuntu-latest) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-4vcpu-ubuntu-2404, ./docker/pii.Dockerfile, ECR_PII, ubuntu-latest) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-4vcpu-ubuntu-2404, ./docker/realtime.Dockerfile, ECR_REALTIME, ubuntu-latest) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Check Desktop Signing Secrets (push) Has been cancelled
CI / Desktop Release (push) Has been cancelled
CI / Create Desktop Prerelease (push) Has been cancelled
CI / Desktop Prerelease Build (push) Has been cancelled
CI / Publish Desktop Prerelease (push) Has been cancelled
CI / Prune Desktop Prereleases (push) Has been cancelled
Helm Chart / Install on kind and run helm test (push) Has been cancelled
WeHub snapshot of cb28d14c6f2c081de7a0d8729a8c816c9adef67a
2026-08-10 11:17:50 +08:00

210 lines
9.4 KiB
TypeScript

import { fetchAppConfigProfile } from '@/lib/core/config/appconfig'
import type { AppConfigGateContext, AppConfigGateRule } from '@/lib/core/config/appconfig-rules'
import { matchesRule, parseGateConfig } from '@/lib/core/config/appconfig-rules'
import { env, isTruthy } from '@/lib/core/config/env'
import { isAppConfigEnabled } from '@/lib/core/config/env-flags'
/**
* Name of the AppConfig configuration profile holding the gated feature flags.
* Cross-repo contract: must match the `CfnConfigurationProfile` name created by
* the infra stack.
*/
const FEATURE_FLAGS_PROFILE = 'feature-flags'
/**
* 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 `adminEnabled` for
* platform admins. An absent clause never matches. Shape shared with the other
* AppConfig gating documents via {@link AppConfigGateRule}.
*/
export type FeatureFlagRule = AppConfigGateRule
export type FeatureFlagsConfig = Record<string, FeatureFlagRule>
/**
* 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 type FeatureFlagContext = AppConfigGateContext
/**
* 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.
*
* 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.
*/
/**
* 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.
*/
interface FeatureFlagDefinition {
description: string
/** Env/secret key consulted when AppConfig isn't the source of truth. Truthy ⇒ on. */
fallback: keyof typeof env
}
/** The single registry of known flags. To add a flag, add one entry here. */
const FEATURE_FLAGS = {
'table-snapshot-cache': {
description:
'Mount Sim tables into code sandboxes by reference via a version-keyed CSV snapshot in ' +
'object storage (reused across runs until the table mutates) instead of draining the whole ' +
'table into web-process heap. resolveInputFiles evaluates without user context — use ' +
'enabled:true for global rollout rather than per-user targeting.',
fallback: 'TABLE_SNAPSHOT_CACHE',
},
'pii-redaction': {
description:
'Redact PII from workflow logs via configurable Data Retention rules (Presidio at the ' +
'logger persist choke point) and expose the Data Retention config surfaces. Global on/off ' +
'only — evaluated without user/org context so the persist path and config routes always ' +
'agree.',
fallback: 'PII_REDACTION',
},
'pii-granular-redaction': {
description:
'Expose the execution-altering PII redaction stages (redact the workflow input and every ' +
'block output in-flight) in the Data Retention config, layered on top of pii-redaction. ' +
'Global on/off only — gates the config surfaces (route write + UI). Because stored rules ' +
'are the source of truth for the executor, a granular stage can only run once it was ' +
'writable, so the executor is never flag-gated at runtime (avoiding a fail-open leak).',
fallback: 'PII_GRANULAR_REDACTION',
},
'trigger-eu-region': {
description:
'Route Trigger.dev runs to eu-central-1 instead of the default us-east-1. Global on/off ' +
'only — resolved without user/org context at every task-trigger call site via ' +
'resolveTriggerRegion, so the whole deployment switches regions together.',
fallback: 'TRIGGER_EU_REGION',
},
'workspace-forking': {
description:
'Runtime rollout gate for workspace forking (fork/promote/rollback), layered on top of ' +
'the existing FORKING_ENABLED / Enterprise-plan gate at the shared assertForkingEnabled ' +
'choke point. Enforced ONLY where AppConfig is the source of truth (Sim Cloud), so ' +
'operators can dark-launch forking to specific orgs/users/admins without touching ' +
'self-hosted/local behaviour. Fallback mirrors FORKING_ENABLED for off-AppConfig reads.',
fallback: 'FORKING_ENABLED',
},
'deploy-as-block': {
description:
'Publish a deployed workflow as a reusable, org-wide custom block (custom name/SVG icon/' +
'description; Start inputs become block inputs). Gates the Deploy-modal "Block" tab and the ' +
'custom-block publish/list routes. Off-AppConfig falls back to DEPLOY_AS_BLOCK.',
fallback: 'DEPLOY_AS_BLOCK',
},
'tables-v2-api': {
description:
'Gate the v2 tables HTTP API — the public read API (GET /api/v2/tables, POST ' +
'/api/v2/tables/[tableId]/query) and the internal predicate-grammar query route (POST ' +
'/api/table/[tableId]/query). When off, those routes return 404 as if the surface does not ' +
'exist. Gated by userId/orgId/admins via AppConfig; off-AppConfig falls back to TABLES_V2_API.',
fallback: 'TABLES_V2_API',
},
'table-locks': {
description:
'Per-table mutation locks (schema/insert/update/delete) an admin toggles to make a table ' +
'append-only or read-only. Gates the ability to SET locks (the settings UI + the PATCH ' +
'locks branch); enforcement of any lock already stored always runs. Since new tables are ' +
'created unlocked and forks reset locks, an off flag means no table can become locked, so ' +
'the woven-in asserts stay no-ops. Off-AppConfig falls back to TABLE_LOCKS.',
fallback: 'TABLE_LOCKS',
},
'table-views': {
description:
'Saved table views (named filter/sort/column-visibility presets) plus the column show/hide ' +
'menu, in the table-detail options bar. UI-only gate: resolved in the table page (server) ' +
"and passed down, so the table falls back to today's Filter/Sort bar when off. The routes " +
'and the table_views table ship ungated — they are inert with no UI to call them, and a view ' +
'saved during a rollout must survive the flag being toggled back off. Embedded (mothership) ' +
'tables render without views regardless, since no server context resolves the flag there. ' +
'Off-AppConfig falls back to TABLE_VIEWS.',
fallback: 'TABLE_VIEWS',
},
} satisfies Record<string, FeatureFlagDefinition>
/**
* 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).
*/
export type FeatureFlagName = keyof typeof FEATURE_FLAGS
/** Build the fallback document from each flag's secret. Truthy secret ⇒ enabled. */
function fallbackFlags(): FeatureFlagsConfig {
const flags: FeatureFlagsConfig = {}
for (const [name, def] of Object.entries(FEATURE_FLAGS) as Array<
[string, FeatureFlagDefinition]
>) {
flags[name] = { enabled: isTruthy(env[def.fallback]) }
}
return flags
}
/**
* 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.
*/
async function resolveAdmin(userId: string): Promise<boolean> {
const { isPlatformAdmin } = await import('@/lib/permissions/super-user')
return isPlatformAdmin(userId)
}
/**
* The admin clause is resolved last and lazily: a global/userId/orgId match
* short-circuits before any DB read, a rule without `adminEnabled` never queries,
* and a missing `userId` resolves to `false` without a query.
*/
async function evaluate(
rule: FeatureFlagRule | undefined,
ctx: FeatureFlagContext
): Promise<boolean> {
if (!rule) return false
if (matchesRule(rule, ctx, false)) return true
if (rule.adminEnabled) {
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<FeatureFlagsConfig> {
if (!isAppConfigEnabled) return fallbackFlags()
const value = await fetchAppConfigProfile(
{
application: env.APPCONFIG_APPLICATION as string,
environment: env.APPCONFIG_ENVIRONMENT as string,
profile: FEATURE_FLAGS_PROFILE,
},
parseGateConfig
)
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<boolean> {
const flags = await getFeatureFlags()
return evaluate(flags[flag], ctx)
}