From 506c54e15e13cb3a31a09b6400289cad335c9119 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 31 Aug 2026 03:32:38 +0000 Subject: [PATCH 1/3] feat(app-shell): declare scim on RuntimeFeatures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `RuntimeFeatures.scim` documents the wire cloud's `resolveFeatures` already emits in the same object literal as `customDomain` / `sso` (objectui#5869). Declaration only, mirroring the two commercial siblings' "server-derived, absent on vanilla" doc comment — no read point, no gate, and no SCIM UI affordance added. Card relationship is stated once in the PR body (this branch squashes). Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB --- .changeset/5869-runtime-features-scim.md | 13 +++++++++++++ packages/app-shell/src/runtime-config.ts | 10 ++++++++++ 2 files changed, 23 insertions(+) create mode 100644 .changeset/5869-runtime-features-scim.md diff --git a/.changeset/5869-runtime-features-scim.md b/.changeset/5869-runtime-features-scim.md new file mode 100644 index 0000000000..1d1ad12cb0 --- /dev/null +++ b/.changeset/5869-runtime-features-scim.md @@ -0,0 +1,13 @@ +--- +'@object-ui/app-shell': patch +--- + +Declare `scim?: boolean` on `RuntimeFeatures` (objectui#5869), mirroring its +two commercial siblings `customDomain?` / `sso?` with the same +server-derived, absent-on-vanilla doc comment. + +This documents 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 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.ts b/packages/app-shell/src/runtime-config.ts index 0cbaec2191..11e342775d 100644 --- a/packages/app-shell/src/runtime-config.ts +++ b/packages/app-shell/src/runtime-config.ts @@ -67,6 +67,16 @@ 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`. Declaration only: + * this keeps the interface in sync with the wire the producer already + * emits — no SPA read point or gate consumes it yet (future work, not + * implied by this declaration). + */ + scim?: boolean; } /** From a711cc14018d3794aa92307085bd51668f39fb84 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 31 Aug 2026 03:50:48 +0000 Subject: [PATCH 2/3] fix(app-shell): map scim through initRuntimeConfig alongside its siblings The declare-only version of scim?: boolean left it out of the default features object and the body.features.* -> current.features derivation, so getRuntimeConfig().features.scim would read undefined even when the server sent scim: true -- a fresh, opposite-direction instance of the declaration-vs-runtime divergence objectui#5869 was filed to record. Mirrors customDomain / sso end to end: scim: false in defaults, and scim: body.features.scim === true in the derivation. Still no read point, no gate, and no SCIM UI affordance -- the two lines make the declaration honest, they do not add a feature. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB --- .changeset/5869-runtime-features-scim.md | 18 ++++++++++-------- packages/app-shell/src/runtime-config.ts | 10 ++++++---- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/.changeset/5869-runtime-features-scim.md b/.changeset/5869-runtime-features-scim.md index 1d1ad12cb0..68d8e09c83 100644 --- a/.changeset/5869-runtime-features-scim.md +++ b/.changeset/5869-runtime-features-scim.md @@ -2,12 +2,14 @@ '@object-ui/app-shell': patch --- -Declare `scim?: boolean` on `RuntimeFeatures` (objectui#5869), mirroring its -two commercial siblings `customDomain?` / `sso?` with the same -server-derived, absent-on-vanilla doc comment. +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 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 only — this patch adds no -read point, no gate, and no SCIM UI affordance. Any actual SCIM-gated UI is -future work. +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.ts b/packages/app-shell/src/runtime-config.ts index 11e342775d..70f1f75477 100644 --- a/packages/app-shell/src/runtime-config.ts +++ b/packages/app-shell/src/runtime-config.ts @@ -71,9 +71,10 @@ export interface RuntimeFeatures { * 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`. Declaration only: - * this keeps the interface in sync with the wire the producer already - * emits — no SPA read point or gate consumes it yet (future work, not + * 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; @@ -288,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' }, @@ -374,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 From 07837a2b05ea6a237995b1961c41d2a5fdb7fc01 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 31 Aug 2026 04:08:24 +0000 Subject: [PATCH 3/3] test(app-shell): pin scim into the customDomain/sso commercial-flag doctrine Extends the four existing "runtime-config commercial features" cases (defaults-off, grants-only-when-true, business-tier-grants-all, and omitted-keys) to also assert on features.scim, matching the doctrine's own claim that customDomain/sso/scim are paid flags that must default off and only turn on when the server explicitly grants them. Reverse-verified: removing the scim: body.features.scim === true derivation line turns exactly the "business-tier grants all three" assertion red (1 failed, 29 passed) while the three negative-leg assertions stay green -- proving a positive leg was required for any of these pins to be load-bearing on the mapping, not merely a restatement of the already-false default. Restored and reran green (30 passed) before committing. No new test cases (extends existing ones only), no read point, no gate, no SCIM UI affordance, nothing under packages/types/**. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB --- packages/app-shell/src/runtime-config.test.ts | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) 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); });