Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .changeset/5869-runtime-features-scim.md
Original file line number Diff line number Diff line change
@@ -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.
26 changes: 17 additions & 9 deletions packages/app-shell/src/runtime-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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);
});
Expand Down
14 changes: 13 additions & 1 deletion packages/app-shell/src/runtime-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand Down Expand Up @@ -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' },
Expand Down Expand Up @@ -364,6 +375,7 @@ export async function initRuntimeConfig(baseUrl: string = ''): Promise<void> {
// 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
Expand Down
Loading