Skip to content

Marketplace catalog page answers "no marketplace here" before "not an admin" (#5557) - #5582

Merged
os-sales merged 1 commit into
mainfrom
claude/issue-5557-marketplace-runtime-before-admin
Aug 21, 2026
Merged

Marketplace catalog page answers "no marketplace here" before "not an admin" (#5557)#5582
os-sales merged 1 commit into
mainfrom
claude/issue-5557-marketplace-runtime-before-admin

Conversation

@os-sales

Copy link
Copy Markdown
Collaborator

Fixes#5557

The defect

MarketplacePage — the marketplace catalog page — ordered its two early returns admin-first:

if (!isAdmin) return ( MarketplaceAccessDenied ) // was line 200
if (!marketplaceEnabled) return ( MarketplaceDisabled ) // was line 203

So on a runtime that mounts no marketplace at all (features.marketplace: false — an OS_CLOUD_URL=off deployment, the EE deploy template's factory default) an unprivileged member was told they lack permission for a surface that exists for nobody. That answer sends them to ask an administrator for a grant that would not help them, and it left the informational disabled state built in #5504 unreachable for every non-admin.

The fix

The runtime check answers first, because "this deployment has no marketplace" is true regardless of who is asking. That is the ordering MarketplacePackagePage landed under #5533, so the two sibling pages stop disagreeing about one runtime for the one class of viewer they still disagreed for.

The retired code comment claimed the old order was deliberate — "a non-admin has no business reading this runtime's marketplace posture either way". The merged sibling rebuts it on the record, and this PR carries that rationale over: features.marketplace is public runtime config every client already reads, so the old order withheld nothing. It only misdirected.

Boundary — what this PR does not claim

Admin-first ordering stays correct where a marketplace exists. On features.marketplace: true a non-admin still gets MarketplaceAccessDenied; the catalog is an install surface. That boundary is a test assertion, not prose — it is the control for the whole change: without it, a "fix" that simply deleted the admin check would satisfy every other case in the file while handing an install surface to every member.

Reverse-verification (ablation)

The pre-fix ordering was restored on disk and the suite re-run. No rebuild is involved: the tests import the subject as a relative source path (from '../MarketplacePage'), so nothing resolves through dist/.

result
mutation proven on diskadmin guard moved to line 209, runtime guard to 214; git diff --stat 2 insertions/2 deletions; sha256 fc7e9405…60753dec…
under the mutation3 failed / 29 passed — the three non-admin marketplace-off cases went red
the boundary controlstayed green, as required
restorevia trap ... EXIT INT TERM; sha256 back to fc7e9405…, git status clean

Gates — all at 880929160

gateverdict
pnpm --filter '@object-ui/app-shell^...' buildcommand-exit 0 (run first; without it type-check reports TS2307 on every sibling)
pnpm --filter @object-ui/app-shell type-checkTYPECHECK_EXIT=0tsc --noEmit && tsc -p tsconfig.test.json
pnpm --filter @object-ui/app-shell lintLINT_EXIT=02509 problems (0 errors, 2509 warnings)
vitest run packages/app-shell/src/console/marketplace/Test Files 5 passed (5) · Tests 32 passed (32)
check-control-bytesOK (scanned 4638 tracked text file(s); skipped 85 binary)
check-changeset-presence2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)
check-changeset-no-majorNo changeset declares a major bump.
check-changeset-fixedAll workspace packages are in the changeset fixed group.
check-i18n-call-site-keysexit 0 — no new keys; every call-site key resolves
check-lint-coverage · check-type-check-coverage46/46 packages linted · 41/41 packages compile their tests

Declared narrowing — vitest was run over console/marketplace/ rather than all of packages/app-shell, and here is why that cannot hide a failure. The changed function body can only execute where the real component is imported. Measured across every test file in the repo: exactly 3 import it for real, and all three are in the directory that was run. The only two others that reach the marketplace route replace it with a stub (vi.mock('../marketplace/MarketplacePage', …)), so the changed code cannot run in them. Invariance for untouched files: the change is a statement reorder inside one function body and alters no export signature, and type-check compiled the whole package (both tsconfigs) green.

Scope

MarketplacePage.tsx, one new test file, one changeset. MarketplacePackagePage.tsx is read-only here — it is the already-fixed sibling, mounted by the new tests only to prove the two pages agree. MarketplaceAccessDenied.tsx and MarketplaceDisabled.tsx are untouched: only which of the two the page reaches for changes, and in which order it decides.

Why a new test file rather than growing MarketplacePage.disabledState.test.tsx: that suite hard-mocks useIsWorkspaceAdmin: () => true at module scope, so every case in it is an admin and none of them can see this defect. The admin flag has to vary per case here, which is a different module mock and therefore a different file.


Generated by Claude Code

…re" before "not an admin" (#5557)
MarketplacePage ordered its early returns admin-first, so on a runtime that
mounts no marketplace (features.marketplace: false -- an OS_CLOUD_URL=off
deployment) a non-admin was told they lack PERMISSION for a surface that exists
for nobody, and the informational disabled state was unreachable for them.
The runtime check now answers first: "this deployment has no marketplace" is
true regardless of who is asking, and features.marketplace is public runtime
config every client already reads. This is the ordering MarketplacePackagePage
landed under #5533, so the sibling pages stop disagreeing for non-admins.
Admin-first ordering stays correct where a marketplace exists, pinned by an
explicit boundary case: a non-admin on a marketplace-ON runtime still gets
MarketplaceAccessDenied.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012u2pRjcqAYtoEjgr3wwhnK
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3784.8 KB3867.2 KB
Main entry chunk (gzip)151.2 KB350 KB
Entry fileindex-CCWXRPgG.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (index.js)10.04KB3.72KB
app-shell (runtime-config.js)8.91KB2.99KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)29.34KB7.05KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)6.35KB2.43KB
auth (index.js)2.77KB1.22KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.02KB0.89KB
auth (useIsWorkspaceAdmin.js)3.04KB1.45KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)506.88KB113.68KB
core (index.js)4.51KB1.80KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)159.80KB44.33KB
fields (index.js)237.61KB59.63KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.22KB3.08KB
i18n (provider.js)23.13KB7.63KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)30.51KB7.57KB
i18n (useSafeTranslation.js)7.77KB3.13KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.35KB3.31KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.42KB1.42KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)1.81KB0.83KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.62KB12.83KB
plugin-charts (index.js)64.72KB18.35KB
plugin-chatbot (index.js)181.21KB43.14KB
plugin-dashboard (index.js)128.51KB32.96KB
plugin-designer (index.js)212.39KB42.83KB
plugin-detail (index.js)242.15KB60.89KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)125.07KB30.43KB
plugin-gantt (index.js)164.10KB39.87KB
plugin-grid (index.js)200.79KB54.26KB
plugin-kanban (index.js)52.93KB14.60KB
plugin-list (index.js)111.70KB27.17KB
plugin-map (index.js)20.06KB6.62KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.49KB11.93KB
plugin-timeline (index.js)26.68KB7.66KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)84.48KB20.67KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)43.66KB14.77KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.33KB0.69KB
react (schema-input.js)1.45KB0.83KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (index.js)4.77KB2.16KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)10.76KB3.17KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.29KB0.24KB
sdui-parser (validate.js)6.92KB2.40KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)0.20KB0.18KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-retry.js)4.32KB2.02KB
types (index.js)3.08KB1.53KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)0.20KB0.18KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-sales
os-sales marked this pull request as ready for review August 21, 2026 15:30
@os-sales
os-sales added this pull request to the merge queueAug 21, 2026
Merged via the queue into main with commit d524bdeAug 21, 2026
23 checks passed
@os-sales
os-sales deleted the claude/issue-5557-marketplace-runtime-before-admin branch August 21, 2026 15:31
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Marketplace catalog page tells a NON-ADMIN "access denied" on a runtime that has no marketplace at all

1 participant

@os-sales