diff --git a/.changeset/5869-runtime-features-scim.md b/.changeset/5869-runtime-features-scim.md new file mode 100644 index 0000000000..68d8e09c83 --- /dev/null +++ b/.changeset/5869-runtime-features-scim.md @@ -0,0 +1,15 @@ +--- +'@object-ui/app-shell': patch +--- + +Declare `scim?: boolean` on `RuntimeFeatures` and map it through +`initRuntimeConfig` (objectui#5869), mirroring its two commercial siblings +`customDomain?` / `sso?` end to end: same doc-comment style +(server-derived, absent-on-vanilla), same `false` default, same +`body.features.scim === true` derivation. + +This documents and now honestly carries the wire a shipped cloud producer +already emits in the same `resolveFeatures` object literal as +`customDomain` / `sso`; the key already arrives at the SPA today, untyped. +Declaration plus plumbing only — this patch adds no read point, no gate, +and no SCIM UI affordance. Any actual SCIM-gated UI is future work. diff --git a/packages/app-shell/src/runtime-config.test.ts b/packages/app-shell/src/runtime-config.test.ts index a3cc2bf2e6..021f0697ce 100644 --- a/packages/app-shell/src/runtime-config.test.ts +++ b/packages/app-shell/src/runtime-config.test.ts @@ -3,9 +3,9 @@ /** * runtime-config commercial-feature parsing (cloud ADR-0011/0012). * - * `customDomain` / `sso` are paid flags: they must default OFF and only turn on - * when the server explicitly grants them, so an older/vanilla runtime that - * omits them never surfaces a paid affordance. + * `customDomain` / `sso` / `scim` are paid flags: they must default OFF and + * only turn on when the server explicitly grants them, so an older/vanilla + * runtime that omits them never surfaces a paid affordance. */ import { describe, it, expect, afterEach, vi } from 'vitest'; @@ -24,31 +24,39 @@ afterEach(() => { }); describe('runtime-config commercial features', () => { - it('defaults customDomain/sso OFF before init', () => { + it('defaults customDomain/sso/scim OFF before init', () => { resetRuntimeConfigForTesting(); expect(getRuntimeConfig().features.customDomain).toBe(false); expect(getRuntimeConfig().features.sso).toBe(false); + expect(getRuntimeConfig().features.scim).toBe(false); }); - it('grants customDomain/sso only when the server says true', async () => { - mockConfig({ customDomain: true, sso: false }); + it('grants customDomain/sso only when the server says true (scim stays OFF when the server says false)', async () => { + mockConfig({ customDomain: true, sso: false, scim: false }); await initRuntimeConfig(); expect(getRuntimeConfig().features.customDomain).toBe(true); expect(getRuntimeConfig().features.sso).toBe(false); + // The negative leg that actually tests fail-closed: `scim: false` sits + // in the SAME payload as `customDomain: true`, so a hardcoded + // `scim: true` (or "any granted sibling flips scim on") would fail + // here even though it would pass the all-true case below. + expect(getRuntimeConfig().features.scim).toBe(false); }); - it('business-tier grants both', async () => { - mockConfig({ customDomain: true, sso: true }); + it('business-tier grants all three', async () => { + mockConfig({ customDomain: true, sso: true, scim: true }); await initRuntimeConfig(); expect(getRuntimeConfig().features.customDomain).toBe(true); expect(getRuntimeConfig().features.sso).toBe(true); + expect(getRuntimeConfig().features.scim).toBe(true); }); it('older runtime omitting the flags keeps them OFF (no paid surface leak)', async () => { - mockConfig({ aiStudio: true }); // no customDomain/sso keys at all + mockConfig({ aiStudio: true }); // no customDomain/sso/scim keys at all await initRuntimeConfig(); expect(getRuntimeConfig().features.customDomain).toBe(false); expect(getRuntimeConfig().features.sso).toBe(false); + expect(getRuntimeConfig().features.scim).toBe(false); // sanity: existing flags still parse expect(getRuntimeConfig().features.aiStudio).toBe(true); }); diff --git a/packages/app-shell/src/runtime-config.ts b/packages/app-shell/src/runtime-config.ts index 0cbaec2191..70f1f75477 100644 --- a/packages/app-shell/src/runtime-config.ts +++ b/packages/app-shell/src/runtime-config.ts @@ -67,6 +67,17 @@ export interface RuntimeFeatures { * (treated as off). Server-derived from the plan entitlements. */ sso?: boolean; + /** + * SCIM-based user/group provisioning is available on this environment's + * plan. Optional commercial flag — absent on self-hosted / vanilla + * runtimes (treated as off). Server-derived from the plan entitlements, + * the same producer object as `customDomain` / `sso`. Mapped through by + * `initRuntimeConfig` alongside its two siblings so the typed value + * matches the wire the producer already emits — no SPA read point or + * gate consumes it yet (any actual SCIM UI gating is future work, not + * implied by this declaration). + */ + scim?: boolean; } /** @@ -278,7 +289,7 @@ const defaults: AppShellRuntimeConfig = { singleEnvironment: false, defaultOrgId: null, defaultEnvironmentId: null, - features: { installLocal: false, marketplace: true, aiStudio: true, autoPublishAiBuilds: true, customDomain: false, sso: false }, + features: { installLocal: false, marketplace: true, aiStudio: true, autoPublishAiBuilds: true, customDomain: false, sso: false, scim: false }, // `stage: 'preview'` while the whole platform is pre-GA, so the badge shows // out of the box on any runtime that hasn't sent an explicit stage yet. branding: { productName: 'ObjectOS', productShortName: 'ObjectOS', stage: 'preview', brandColor: '#4F46E5', pwaThemeColor: '#4f46e5' }, @@ -364,6 +375,7 @@ export async function initRuntimeConfig(baseUrl: string = ''): Promise { // them — never show a paid surface on an unknown/older runtime. customDomain: body.features.customDomain === true, sso: body.features.sso === true, + scim: body.features.scim === true, } : current.features, // Read off the RAW body, not off `body.telemetry`: the mirrored reader