11import { isOrganizationOnEnterprisePlan } from '@/lib/billing/core/subscription'
2- import { isBillingEnabled , isForkingEnabled } from '@/lib/core/config/env-flags'
2+ import { isAppConfigEnabled , isBillingEnabled , isForkingEnabled } from '@/lib/core/config/env-flags'
3+ import { isFeatureEnabled } from '@/lib/core/config/feature-flags'
34import { HttpError } from '@/lib/core/utils/http-error'
45import { type ForkEdge , resolveForkEdge } from '@/lib/workspaces/fork/lineage/lineage'
56import { checkWorkspaceAccess , type WorkspaceWithOwner } from '@/lib/workspaces/permissions/utils'
@@ -9,12 +10,18 @@ import { getWorkspaceCreationPolicy, type WorkspaceCreationPolicy } from '@/lib/
910export type PromoteDirection = 'push' | 'pull'
1011
1112/**
12- * Enterprise-only gate shared by every fork/promote route. On Sim Cloud the gate
13- * is the Enterprise plan; on self-hosted it's `FORKING_ENABLED`, which 404s when
14- * unset so a newer image doesn't silently expose forking. Mirrors the data-drains
15- * gate - this repo gates EE features by plan + env flag, not by directory.
13+ * Gate shared by every fork/promote route. The deployment/entitlement gate is
14+ * unchanged: on Sim Cloud the gate is the Enterprise plan; on self-hosted it's
15+ * `FORKING_ENABLED`, which 404s when unset so a newer image doesn't silently expose
16+ * forking. Mirrors the data-drains gate - this repo gates EE features by plan + env
17+ * flag, not by directory.
18+ *
19+ * Layered on top is the runtime `workspace-forking` flag, a rollout switch enforced
20+ * ONLY where AppConfig is the source of truth (Sim Cloud). It lets us dark-launch
21+ * forking to specific orgs/users/admins without a redeploy; self-hosted/local
22+ * deployments have no AppConfig, so their behaviour is untouched by the flag.
1623 */
17- async function assertForkingEnabled ( organizationId : string | null ) : Promise < void > {
24+ async function assertForkingEnabled ( organizationId : string | null , userId : string ) : Promise < void > {
1825 if ( ! isBillingEnabled && ! isForkingEnabled ) {
1926 throw new ForkError ( 'Workspace forking is not enabled on this deployment' , 404 )
2027 }
@@ -26,6 +33,12 @@ async function assertForkingEnabled(organizationId: string | null): Promise<void
2633 throw new ForkError ( 'Workspace forking is available on Enterprise plans only' , 403 )
2734 }
2835 }
36+ if (
37+ isAppConfigEnabled &&
38+ ! ( await isFeatureEnabled ( 'workspace-forking' , { userId, orgId : organizationId } ) )
39+ ) {
40+ throw new ForkError ( 'Workspace forking is not enabled on this deployment' , 404 )
41+ }
2942}
3043
3144/**
@@ -51,7 +64,7 @@ async function requireWorkspace(
5164 if ( ! access . exists || ! access . workspace ) {
5265 throw new ForkError ( 'Workspace not found' , 404 )
5366 }
54- await assertForkingEnabled ( access . workspace . organizationId )
67+ await assertForkingEnabled ( access . workspace . organizationId , userId )
5568 return { workspace : access . workspace , canAdmin : access . canAdmin }
5669}
5770
0 commit comments