|
1 | 1 | /** |
2 | 2 | * Environment utility functions for consistent environment detection across the application |
3 | 3 | */ |
4 | | -import { env, getEnv, isFalsy, isTruthy } from './env' |
| 4 | +import { |
| 5 | + ENTERPRISE_FEATURE_LEGACY_DEFAULTS, |
| 6 | + type EnterpriseFeature, |
| 7 | + resolveEnterpriseEntitlement, |
| 8 | +} from './enterprise-entitlements' |
| 9 | +import { env, envBoolean, getEnv, isFalsy, isTruthy } from './env' |
5 | 10 |
|
6 | 11 | /** |
7 | 12 | * Is the application running in production mode |
@@ -45,6 +50,18 @@ export const isCopilotBillingAttributionV1Enabled = isTruthy( |
45 | 50 | */ |
46 | 51 | export const isCopilotBillingProtocolRequired = isTruthy(env.COPILOT_BILLING_PROTOCOL_REQUIRED) |
47 | 52 |
|
| 53 | +/** |
| 54 | + * Holds tools the catalog marks `requiresApproval` — shell commands, workflow |
| 55 | + * runs, sandboxed code, deployments, integration calls — behind an explicit |
| 56 | + * Allow / Skip prompt, blocking the mothership turn until the user answers. |
| 57 | + * |
| 58 | + * Off by default: turning it on makes the copilot prompt on its most frequently |
| 59 | + * used tools, so it is an opt-in change in how the product feels, not just a |
| 60 | + * safety toggle. With it off nothing is stamped, gated, or persisted, and an |
| 61 | + * approval stamp arriving from Go is cleared on the way to the client. |
| 62 | + */ |
| 63 | +export const isCopilotToolPermissionsEnabled = isTruthy(env.COPILOT_TOOL_PERMISSIONS_ENABLED) |
| 64 | + |
48 | 65 | /** |
49 | 66 | * Is billing enforcement enabled. |
50 | 67 | * |
@@ -172,73 +189,161 @@ export const isSlackExtendedScopesEnabled = |
172 | 189 | export const isTriggerDevEnabled = isTruthy(env.TRIGGER_DEV_ENABLED) |
173 | 190 |
|
174 | 191 | /** |
175 | | - * Is SSO enabled for enterprise authentication |
| 192 | + * Turns on the whole enterprise suite for a deployment that does not run |
| 193 | + * billing. Individual feature flags below still win where they are set, so an |
| 194 | + * operator can enable everything and then switch one feature back off. |
| 195 | + * |
| 196 | + * Server code reads `ENTERPRISE_ENABLED`; the browser reads the |
| 197 | + * `NEXT_PUBLIC_ENTERPRISE_ENABLED` twin (see {@link isBillingEnabled}). |
| 198 | + * Deployments must set both together. |
| 199 | + */ |
| 200 | +export const isEnterpriseEnabled = |
| 201 | + typeof window === 'undefined' |
| 202 | + ? isTruthy(env.ENTERPRISE_ENABLED) |
| 203 | + : isTruthy(getEnv('NEXT_PUBLIC_ENTERPRISE_ENABLED')) |
| 204 | + |
| 205 | +/** |
| 206 | + * Reads a feature's own flag as a tri-state, picking the server var or its |
| 207 | + * browser twin for the current runtime. `undefined` means the operator left it |
| 208 | + * unset, which is what lets the master switch and legacy default apply. |
176 | 209 | */ |
177 | | -export const isSsoEnabled = isTruthy(env.SSO_ENABLED) |
| 210 | +function explicitEnterpriseFlag( |
| 211 | + serverValue: boolean | string | undefined, |
| 212 | + clientKey: string |
| 213 | +): boolean | undefined { |
| 214 | + return typeof window === 'undefined' ? envBoolean(serverValue) : envBoolean(getEnv(clientKey)) |
| 215 | +} |
178 | 216 |
|
179 | 217 | /** |
180 | | - * Is access control (permission groups) enabled via env var override. |
181 | | - * This bypasses plan requirements for self-hosted deployments. |
| 218 | + * Resolves one enterprise feature for this deployment. |
182 | 219 | * |
183 | | - * Server code reads `ACCESS_CONTROL_ENABLED`; the browser reads the |
184 | | - * `NEXT_PUBLIC_ACCESS_CONTROL_ENABLED` twin (see {@link isBillingEnabled}). |
| 220 | + * When billing runs, subscription plans decide entitlement and these flags are |
| 221 | + * only explicit overrides — so an unset flag stays `false` and never widens |
| 222 | + * access on Sim Cloud. When billing is off there is no plan to consult, so |
| 223 | + * resolution falls through the master switch to the feature's legacy default |
| 224 | + * (see {@link ENTERPRISE_FEATURE_LEGACY_DEFAULTS}). |
| 225 | + */ |
| 226 | +function enterpriseFeatureEnabled( |
| 227 | + feature: EnterpriseFeature, |
| 228 | + serverValue: boolean | string | undefined, |
| 229 | + clientKey: string |
| 230 | +): boolean { |
| 231 | + const explicit = explicitEnterpriseFlag(serverValue, clientKey) |
| 232 | + if (isBillingEnabled) return explicit ?? false |
| 233 | + return resolveEnterpriseEntitlement({ |
| 234 | + explicit, |
| 235 | + masterEnabled: isEnterpriseEnabled, |
| 236 | + legacyDefault: ENTERPRISE_FEATURE_LEGACY_DEFAULTS[feature], |
| 237 | + }) |
| 238 | +} |
| 239 | + |
| 240 | +/** |
| 241 | + * Is SSO enabled for enterprise authentication |
185 | 242 | */ |
186 | | -export const isAccessControlEnabled = |
187 | | - typeof window === 'undefined' |
188 | | - ? isTruthy(env.ACCESS_CONTROL_ENABLED) |
189 | | - : isTruthy(getEnv('NEXT_PUBLIC_ACCESS_CONTROL_ENABLED')) |
| 243 | +export const isSsoEnabled = enterpriseFeatureEnabled( |
| 244 | + 'sso', |
| 245 | + env.SSO_ENABLED, |
| 246 | + 'NEXT_PUBLIC_SSO_ENABLED' |
| 247 | +) |
| 248 | + |
| 249 | +/** |
| 250 | + * Is access control (permission groups) enabled. |
| 251 | + * Required for permission-group enforcement to run at all off-hosted. |
| 252 | + */ |
| 253 | +export const isAccessControlEnabled = enterpriseFeatureEnabled( |
| 254 | + 'accessControl', |
| 255 | + env.ACCESS_CONTROL_ENABLED, |
| 256 | + 'NEXT_PUBLIC_ACCESS_CONTROL_ENABLED' |
| 257 | +) |
190 | 258 |
|
191 | 259 | /** |
192 | 260 | * Is organizations enabled. |
193 | | - * True if billing is enabled (orgs come with billing), OR explicitly enabled via env var, |
194 | | - * OR if access control is enabled (access control requires organizations). |
| 261 | + * True if billing is enabled (orgs come with billing), OR resolved on for this |
| 262 | + * deployment, OR if access control is enabled (access control requires |
| 263 | + * organizations). |
195 | 264 | * |
196 | 265 | * Each term resolves through its `NEXT_PUBLIC_*` twin in the browser (see |
197 | 266 | * {@link isBillingEnabled}), so client code — e.g. the better-auth |
198 | 267 | * `organizationClient` plugin registration — sees the same value as the server. |
199 | 268 | */ |
200 | 269 | export const isOrganizationsEnabled = |
201 | 270 | isBillingEnabled || |
202 | | - (typeof window === 'undefined' |
203 | | - ? isTruthy(env.ORGANIZATIONS_ENABLED) |
204 | | - : isTruthy(getEnv('NEXT_PUBLIC_ORGANIZATIONS_ENABLED'))) || |
| 271 | + enterpriseFeatureEnabled( |
| 272 | + 'organizations', |
| 273 | + env.ORGANIZATIONS_ENABLED, |
| 274 | + 'NEXT_PUBLIC_ORGANIZATIONS_ENABLED' |
| 275 | + ) || |
205 | 276 | isAccessControlEnabled |
206 | 277 |
|
207 | 278 | /** |
208 | | - * Is inbox (Sim Mailer) enabled via env var override |
209 | | - * This bypasses hosted requirements for self-hosted deployments |
| 279 | + * Is inbox (Sim Mailer) enabled |
210 | 280 | */ |
211 | | -export const isInboxEnabled = isTruthy(env.INBOX_ENABLED) |
| 281 | +export const isInboxEnabled = enterpriseFeatureEnabled( |
| 282 | + 'inbox', |
| 283 | + env.INBOX_ENABLED, |
| 284 | + 'NEXT_PUBLIC_INBOX_ENABLED' |
| 285 | +) |
212 | 286 |
|
213 | 287 | /** |
214 | | - * Is whitelabeling enabled via env var override |
215 | | - * This bypasses hosted requirements for self-hosted deployments |
| 288 | + * Is whitelabeling enabled |
216 | 289 | */ |
217 | | -export const isWhitelabelingEnabled = isTruthy(env.WHITELABELING_ENABLED) |
| 290 | +export const isWhitelabelingEnabled = enterpriseFeatureEnabled( |
| 291 | + 'whitelabeling', |
| 292 | + env.WHITELABELING_ENABLED, |
| 293 | + 'NEXT_PUBLIC_WHITELABELING_ENABLED' |
| 294 | +) |
218 | 295 |
|
219 | 296 | /** |
220 | | - * Is audit logs enabled via env var override |
221 | | - * This bypasses hosted requirements for self-hosted deployments |
| 297 | + * Is audit log reading enabled. |
| 298 | + * |
| 299 | + * Off-hosted this replaces the enterprise-subscription check that audit access |
| 300 | + * used to require, which no billing-free deployment could ever satisfy. |
222 | 301 | */ |
223 | | -export const isAuditLogsEnabled = isTruthy(env.AUDIT_LOGS_ENABLED) |
| 302 | +export const isAuditLogsEnabled = enterpriseFeatureEnabled( |
| 303 | + 'auditLogs', |
| 304 | + env.AUDIT_LOGS_ENABLED, |
| 305 | + 'NEXT_PUBLIC_AUDIT_LOGS_ENABLED' |
| 306 | +) |
| 307 | + |
| 308 | +/** |
| 309 | + * Is retention *deletion* enabled. |
| 310 | + * |
| 311 | + * Configuring retention has always been possible with billing off; this flag |
| 312 | + * governs whether the cleanup pass actually expires data. Opt-in on purpose — |
| 313 | + * see the note on `dataRetention` in {@link ENTERPRISE_FEATURE_LEGACY_DEFAULTS}. |
| 314 | + */ |
| 315 | +export const isDataRetentionEnabled = enterpriseFeatureEnabled( |
| 316 | + 'dataRetention', |
| 317 | + env.DATA_RETENTION_ENABLED, |
| 318 | + 'NEXT_PUBLIC_DATA_RETENTION_ENABLED' |
| 319 | +) |
224 | 320 |
|
225 | 321 | /** |
226 | | - * Is data retention enabled via env var override |
227 | | - * This bypasses hosted requirements for self-hosted deployments |
| 322 | + * Is data drains enabled |
228 | 323 | */ |
229 | | -export const isDataRetentionEnabled = isTruthy(env.DATA_RETENTION_ENABLED) |
| 324 | +export const isDataDrainsEnabled = enterpriseFeatureEnabled( |
| 325 | + 'dataDrains', |
| 326 | + env.DATA_DRAINS_ENABLED, |
| 327 | + 'NEXT_PUBLIC_DATA_DRAINS_ENABLED' |
| 328 | +) |
230 | 329 |
|
231 | 330 | /** |
232 | | - * Is data drains enabled via env var override |
233 | | - * This bypasses hosted requirements for self-hosted deployments |
| 331 | + * Are organization session policies enabled |
234 | 332 | */ |
235 | | -export const isDataDrainsEnabled = isTruthy(env.DATA_DRAINS_ENABLED) |
| 333 | +export const isSessionPoliciesEnabled = enterpriseFeatureEnabled( |
| 334 | + 'sessionPolicies', |
| 335 | + env.SESSION_POLICIES_ENABLED, |
| 336 | + 'NEXT_PUBLIC_SESSION_POLICIES_ENABLED' |
| 337 | +) |
236 | 338 |
|
237 | 339 | /** |
238 | | - * Is workspace forking enabled via env var override |
239 | | - * This bypasses hosted (Enterprise) requirements for self-hosted deployments |
| 340 | + * Is workspace forking enabled |
240 | 341 | */ |
241 | | -export const isForkingEnabled = isTruthy(env.FORKING_ENABLED) |
| 342 | +export const isForkingEnabled = enterpriseFeatureEnabled( |
| 343 | + 'forking', |
| 344 | + env.FORKING_ENABLED, |
| 345 | + 'NEXT_PUBLIC_FORKING_ENABLED' |
| 346 | +) |
242 | 347 |
|
243 | 348 | /** |
244 | 349 | * The selected remote sandbox provider (`SANDBOX_PROVIDER`), defaulting to E2B. |
|
0 commit comments