4646 * Escape hatch: `// client-boundary-allow: <reason>` on the line directly above
4747 * the import (reason required). Use only for a genuinely browser-only code path.
4848 *
49+ * ## Deployment-shape flags in client surfaces
50+ *
51+ * A `'use client'` module under the workspace, organization, or standalone settings
52+ * surfaces must not import `isHosted`, `isBillingEnabled`, `isChatEnabled`, or the
53+ * enterprise feature flags from `env-flags`: those freeze at module init from the root
54+ * layout's `NEXT_PUBLIC_*` transport, which a recovered 404 or `global-error` tab never
55+ * ran, so Sim Cloud renders as self-hosted. Read them through `useDeploymentShape()` /
56+ * `getDeploymentShape()` from `@/lib/core/config/deployment-shape` (CLAUDE.md).
57+ *
4958 * Usage:
5059 * bun run scripts/check-client-boundary-imports.ts # report
5160 * bun run scripts/check-client-boundary-imports.ts --check # CI gate (fail on any)
@@ -67,6 +76,32 @@ function isServerSurface(rel: string): boolean {
6776 return false
6877}
6978
79+ /** `env-flags` exports that `@/lib/core/config/deployment-shape` re-serves to the browser. */
80+ const DEPLOYMENT_SHAPE_FLAGS = new Set ( [
81+ 'isHosted' ,
82+ 'isBillingEnabled' ,
83+ 'isChatEnabled' ,
84+ 'isAzureConfigured' ,
85+ 'isCohereConfigured' ,
86+ 'isAccessControlEnabled' ,
87+ 'isAuditLogsEnabled' ,
88+ 'isCustomBlocksEnabled' ,
89+ 'isDataDrainsEnabled' ,
90+ 'isDataRetentionEnabled' ,
91+ 'isInboxEnabled' ,
92+ 'isSandboxesEnabled' ,
93+ 'isScimEnabled' ,
94+ 'isSessionPoliciesEnabled' ,
95+ 'isSsoEnabled' ,
96+ 'isUsageMonitoringEnabled' ,
97+ 'isWhitelabelingEnabled' ,
98+ ] )
99+
100+ /** Surfaces whose shell seeds the server-resolved deployment shape (paths relative to apps/sim). */
101+ function isDeploymentShapeSurface ( rel : string ) : boolean {
102+ return / ^ (?: a p p \/ (?: w o r k s p a c e | o | a c c o u n t | s e l f h o s t \/ s e t t i n g s ) | c o m p o n e n t s \/ s e t t i n g s | e e ) \/ / . test ( rel )
103+ }
104+
70105const SOURCE_EXTENSIONS = [ '.ts' , '.tsx' ]
71106const ALLOW_DIRECTIVE = 'client-boundary-allow'
72107const sourceCache = new Map < string , string > ( )
@@ -310,6 +345,38 @@ async function main() {
310345 }
311346 }
312347
348+ const shapeViolations : Array < Violation & { flags : string [ ] } > = [ ]
349+ for ( const absFile of allFiles ) {
350+ if ( ! absFile . startsWith ( `${ APP_DIR } ${ path . sep } ` ) ) continue
351+ const rel = path . relative ( APP_DIR , absFile )
352+ if ( ! isDeploymentShapeSurface ( rel ) || ! ( await isUseClientModule ( absFile ) ) ) continue
353+ const content = await readSource ( absFile )
354+ for ( const imp of parseImports ( content ) ) {
355+ if ( imp . specifier !== '@/lib/core/config/env-flags' || ! importsAValue ( imp . clause ) ) continue
356+ const braced = imp . clause . slice ( imp . clause . indexOf ( '{' ) + 1 , imp . clause . lastIndexOf ( '}' ) )
357+ const flags = braced
358+ . split ( ',' )
359+ . map ( ( member ) => member . trim ( ) . split ( / \s + a s \s + / ) [ 0 ] )
360+ . filter ( ( name ) => DEPLOYMENT_SHAPE_FLAGS . has ( name ) )
361+ if ( flags . length === 0 || hasAllowDirective ( content , imp . line ) ) continue
362+ shapeViolations . push ( { file : rel , line : imp . line , specifier : imp . specifier , flags } )
363+ }
364+ }
365+
366+ if ( shapeViolations . length === 0 ) {
367+ console . log ( '✓ No client settings surface reads deployment-shape flags from env-flags.' )
368+ } else {
369+ failed = true
370+ console . error (
371+ `\n✗ ${ shapeViolations . length } 'use client' module(s) in a workspace/organization/settings surface import deployment-shape flags from env-flags.\n` +
372+ ` Those constants freeze from the root layout's NEXT_PUBLIC_* transport, so a recovered 404/global-error tab renders Sim Cloud as self-hosted.\n` +
373+ ` Read them via useDeploymentShape() (components) or getDeploymentShape() (helpers) from @/lib/core/config/deployment-shape.\n`
374+ )
375+ for ( const v of shapeViolations ) {
376+ console . error ( ` ${ v . file } :${ v . line } imports ${ v . flags . join ( ', ' ) } from '${ v . specifier } '` )
377+ }
378+ }
379+
313380 if ( failed && checkMode ) process . exit ( 1 )
314381}
315382
0 commit comments