Files
simstudioai--sim/apps/sim/lib/core/config/block-visibility.ts
T
wehub-resource-sync d25d482dc2
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (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 / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:20:55 +08:00

111 lines
4.1 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 } from '@/lib/core/config/env'
import { getPreviewBlocksFromEnv, isAppConfigEnabled } from '@/lib/core/config/env-flags'
/**
* Name of the AppConfig configuration profile holding per-block visibility rules.
* Cross-repo contract: must match the `CfnConfigurationProfile` name created by
* the infra stack (`BLOCK_VISIBILITY_PROFILE_NAME`).
*/
const BLOCK_VISIBILITY_PROFILE = 'block-visibility'
/**
* Custom (deploy-as-block) block types are org-scoped and managed by their own
* enabled/disabled lifecycle — the visibility document must never gate them.
* Literal mirrors `CUSTOM_BLOCK_TYPE_PREFIX` in `@/blocks/custom/build-config`,
* not imported to keep the blocks graph out of this config module.
*/
const CUSTOM_BLOCK_KEY_PREFIX = 'custom_block_'
/** Per-request evaluation context; same shape as the feature-flag context. */
export type BlockVisibilityContext = AppConfigGateContext
/**
* The evaluated per-viewer visibility projection.
*
* - `revealed` — preview block types this viewer may see.
* - `disabled` — types whose rule exists but matched no clause; hides
* non-preview (shipped) blocks from discovery surfaces (the kill switch).
* - `previewTagged` — revealed types not globally GA (`enabled !== true`);
* the registry appends " (Preview)" to their names.
*
* All three are needed: `revealed \ previewTagged` is the "GA'd via config while
* `preview: true` is still in code" window, and `disabled` targets a disjoint
* (non-preview) population.
*/
export interface BlockVisibilityState {
revealed: Set<string>
disabled: Set<string>
previewTagged: Set<string>
}
function parseVisibilityConfig(json: unknown): Record<string, AppConfigGateRule> {
const rules = parseGateConfig(json)
for (const key of Object.keys(rules)) {
if (key.startsWith(CUSTOM_BLOCK_KEY_PREFIX)) delete rules[key]
}
return rules
}
/**
* 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 rule.
*/
async function resolveAdmin(userId: string): Promise<boolean> {
const { isPlatformAdmin } = await import('@/lib/permissions/super-user')
return isPlatformAdmin(userId)
}
/**
* Evaluate the block-visibility document for a viewer.
*
* On hosted deployments the rules come from the AppConfig profile (cached,
* ~30s TTL); off-AppConfig the `PREVIEW_BLOCKS` env allowlist is the only
* reveal path and nothing is disabled.
*
* Unlike feature-flags (one rule per call, admin resolved lazily per rule),
* this evaluates the whole document, so platform-admin status is resolved at
* most ONCE per call — and only when some rule actually has `adminEnabled` and
* the caller didn't already supply `ctx.isAdmin`.
*/
export async function getBlockVisibility(
ctx: BlockVisibilityContext = {}
): Promise<BlockVisibilityState> {
if (!isAppConfigEnabled) {
const revealed = new Set(getPreviewBlocksFromEnv())
return { revealed, disabled: new Set(), previewTagged: new Set(revealed) }
}
const rules =
(await fetchAppConfigProfile(
{
application: env.APPCONFIG_APPLICATION as string,
environment: env.APPCONFIG_ENVIRONMENT as string,
profile: BLOCK_VISIBILITY_PROFILE,
},
parseVisibilityConfig
)) ?? {}
const needsAdmin =
ctx.isAdmin === undefined &&
Boolean(ctx.userId) &&
Object.values(rules).some((rule) => rule.adminEnabled)
const isAdmin = ctx.isAdmin ?? (needsAdmin ? await resolveAdmin(ctx.userId as string) : false)
const revealed = new Set<string>()
const disabled = new Set<string>()
const previewTagged = new Set<string>()
for (const [type, rule] of Object.entries(rules)) {
if (matchesRule(rule, ctx, isAdmin)) {
revealed.add(type)
if (rule.enabled !== true) previewTagged.add(type)
} else {
disabled.add(type)
}
}
return { revealed, disabled, previewTagged }
}