Skip to content

fix(app-shell): the lifecycle badge draws for an authoring caller only - #12227

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-12194-managed-by-badge-authoring
Oct 12, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-12194-managed-by-badge-authoring

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #12194

Clause-②: no

What changed

ManagedByBadge reads useCanAuthorMetadata() and renders nothing for a caller who cannot author metadata. Triage ruled the badge an authoring aid (option a, comment 6120170778). The check lives inside the badge, so the three pages that draw it follow one rule: the record page, the object list header and the record form page. No new prop, export, spec key or i18n key. ManagedByBadge is not exported from @object-ui/app-shell, and its signature is unchanged. Comments at the three call sites say where the check lives.

Effect: an end user opening a better-auth record sees no "Identity" badge (zh-CN shows 「身份」), for example on the cloud workspace page (sys_organization). An authoring caller still sees it.

Measured before coding (the dispatch's three mechanism assumptions)

  1. The capability and its fail-open branch. useCanAuthorMetadata() returns hasCapabilities(['manage_metadata']). MePermissionsProvider.hasCapabilities returns true only when systemPermissions is not an array. The framework's GET /api/v1/auth/me/permissions (objectstack plugin-hono-server, current-user-endpoints.ts, read at objectstack 6a40a09ed) always sends systemPermissions: Array.from(...) on its success path.
    • An end user on a deployment that serves the endpoint does NOT see the badge.
    • The check fails open (badge shown, as before) in three cases: the endpoint's branch when the security plugin is not wired, which sends no systemPermissions (the server itself fails open there); the endpoint's catch branch; and a host that mounts no permission provider.
    • The fail-open rule is unchanged, and a test pins it.
  2. Per-object authoring check. app-shell has none. Every authoring entry point reads the global useCanAuthorMetadata: HomePage's builder CTAs, AppHeader's Studio bridge, PageView's edit pencil and the metadata-admin read-only gates. AppHeader and PageView also require isAdmin for their editor entry points. The badge uses the capability alone, as the ruling names it. No new capability.
  3. The three sites and what each is for. All three are authoring aids, so the rule applies to each:
    • Record page (RecordDetailView, the card's site): drawn in the view's own chrome above the page tree. Edit affordances are already gated by the bucket.
    • List header (ObjectView): every caller still has the toolbar's hidden New/Import/Delete and the read-only buckets' empty state (resolveManagedByEmptyState). Neither reads the badge.
    • Form page (RecordFormPage): the dispatch asked whether the badge explains read-only fields to an end user. It does not. That sentence is plugin-form's ClosedAffordanceNotice (objectui#11000), drawn from closedFormAffordance, the same verdict that locks the fields. I also corrected the badge-site comment, which said forms disable inputs for every non-platform bucket. That is not true: config and system-data are writable.

Tests

The three new files mount the real view under the real MePermissionsProvider (initialPermissions), so the chain under test is the shipped one: badge, then useCanAuthorMetadata, then usePermissions, then the provider's hasCapabilities. An absence is read only after the page has painted, and the authoring case renders the badge in the same harness.

  • RecordDetailView.managedByBadgeAuthoring-12194.test.tsx: end user sees none; author sees better-auth; CONTROL: no managedBy draws none for author or end user; CONTROL: platform draws none for the author; an answer with no systemPermissions draws the badge (the fail-open rule, unchanged).
  • ObjectView.managedByBadgeAuthoring-12194.test.tsx and RecordFormPage.managedByBadgeAuthoring-12194.test.tsx: end user sees none, author sees it, and the no-managedBy CONTROL for both callers.

Ablation (one-off, no file kept). With the if (!canAuthor) return null; line replaced by a marker through objectstack's scripts/ablation-replace.mjs: anchor count 1 to 0, marker 0 to 1, blob 8d5e4835a52d to 1ce4cbfec118. The tests import the views and the badge relatively from src, so nothing was built. Prediction, written in each file's header before the run: the three end-user cases go RED and everything else stays GREEN. Observed: Tests 3 failed | 8 passed (11), and the three failures were exactly those cases. Restore was proven: blob equals HEAD (8d5e4835a52d) and git diff HEAD is empty.

Gates (worktree HEAD 4c026221, unless a line names another sha)

  • pnpm --filter @object-ui/app-shell type-check: exit 0. It ran after building the upstream closure with pnpm --workspace-concurrency=2 --filter '@object-ui/app-shell^...' build (exit 0). tsc -p tsconfig.test.json --listFilesOnly lists all three new test files and ManagedByBadge.tsx. A first attempt without the closure built exited 2 on TS2307 for workspace imports. That is a missing prerequisite, not a measurement.
  • pnpm --filter @object-ui/app-shell lint (eslint .): exit 0.
  • Tests, narrowed (declared): the three new files, 11/11 at 4c026221. Plus every app-shell and console test file that imports a view that draws the badge, or the shell that mounts them (ObjectView, RecordDetailView, RecordFormPage, ObjectDataPage, InterfaceListPage, AppContent, or @object-ui/app-shell), derived with git grep over test files: 166 files, all passed. 135 files / 1,135 tests ran at 28ef0f23; 31 files / 312 tests ran at 4c026221. The only change between those two commits is a typing cleanup in one new test file. The full app-shell suite is left to CI.
  • pnpm check:control-bytes OK · pnpm check:new-line-citations 0 new citation(s) · node scripts/check-changeset-no-major.mjs OK · changeset presence / fixed / overwrite / claims OK · check:vi-mock-specifiers / -inherit / -override-shape OK · check:test-path-roots OK · check:pending-changeset-literals OK · type-check and lint coverage OK · check:unreferenced-sources OK · node scripts/check-governed-queue-guard.mjs --test with the 8 paths: NOT GOVERNED.
  • NOT MEASURED: check:eager-closure, reason: it needs apps/console/dist (exit 2, No eager-closure report). The new import (hooks/useCanAuthorMetadata) is already imported by AppHeader and HomePage, so I expect no new module in the eager closure. Left to CI.

Acceptance notes

  • Trade-off, intended by the ruling. A non-authoring admin, such as an EE workspace owner (organization_admin withholds manage_metadata), no longer sees the better-auth tooltip's line about managing these records through the auth provider's flows. The better-auth list empty states still give that guidance to every caller.
  • utils/managedByEmptyState.ts mirrors the badge's mapping for empty states. It is out of scope and untouched.
  • Changeset: .changeset/12194-managed-by-badge-authoring.md, @object-ui/app-shell patch.

Session: https://claude.ai/code/session_01G5TSaEwbEQsKMjhzTB4rEd


Generated by Claude Code

ManagedByBadge labels how an object is built (Admin config, System-managed,
Platform schema, Read-only audit log, Identity). It is an authoring aid, so
it now reads useCanAuthorMetadata (the server's manage_metadata answer) and
draws nothing for a caller who cannot author. The gate lives in the badge,
so the record page, the list header and the form page follow one rule. An
unreported capability set keeps failing open, as the hook's doctrine says.

Pinned through the real record page and MePermissionsProvider: an end user
sees no badge, an authoring caller sees it, an object with no managedBy or
platform draws none, and an unreported answer fails open.

Claude-Session: https://claude.ai/code/session_01G5TSaEwbEQsKMjhzTB4rEd
Co-authored-by: Claude <noreply@anthropic.com>
…form page

The same pair the record page pins, at the two other sites that draw
ManagedByBadge: an end user sees no badge, an authoring caller sees it, and
an object with no managedBy draws none for either. Both mount the real view
under the real MePermissionsProvider.

Claude-Session: https://claude.ai/code/session_01G5TSaEwbEQsKMjhzTB4rEd
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 291 chunks) 3178.8 KB 3204.6 KB
Main entry chunk (gzip) 73.8 KB 350 KB
Entry file index-CTH_jcAg.js —
Status PASS —

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 20.05KB 7.41KB
app-shell (runtime-config.js) 22.59KB 7.89KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 41.19KB 11.12KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.11KB 7.97KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.28KB 2.60KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.50KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 588.51KB 141.71KB
core (index.js) 10.18KB 4.04KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 243.13KB 67.92KB
fields (index.js) 279.96KB 71.37KB
i18n (LocalizationContext.js) 2.92KB 1.42KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.52KB 2.39KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.35KB 12.88KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 36.87KB 9.88KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 40.26KB 11.71KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 15.35KB 5.51KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.82KB 2.38KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.04KB 3.92KB
plugin-calendar (index.js) 54.31KB 15.86KB
plugin-charts (index.js) 84.72KB 23.27KB
plugin-chatbot (index.js) 201.72KB 48.07KB
plugin-dashboard (index.js) 144.20KB 38.95KB
plugin-designer (index.js) 233.77KB 50.02KB
plugin-detail (index.js) 250.93KB 66.25KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 178.37KB 46.27KB
plugin-gantt (index.js) 178.63KB 45.19KB
plugin-grid (index.js) 255.09KB 70.91KB
plugin-kanban (index.js) 53.23KB 16.71KB
plugin-list (index.js) 123.03KB 31.28KB
plugin-map (index.js) 27.24KB 9.03KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.12KB 12.29KB
plugin-timeline (index.js) 39.21KB 11.85KB
plugin-tree (index.js) 15.07KB 5.33KB
plugin-view (index.js) 92.11KB 23.27KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 12.07KB 3.68KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 120.63KB 39.56KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.31KB 2.07KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.07KB 1.30KB
sdui-parser (index.js) 7.30KB 3.12KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 25.28KB 7.81KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 23.87KB 7.83KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (authoring-nodes.js) 0.20KB 0.19KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (cloud.js) 0.20KB 0.18KB
types (complex.js) 4.44KB 2.07KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (dashboard-widget-layout.js) 2.06KB 0.96KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 1.13KB 0.65KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 5.78KB 2.70KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (node-slots.js) 7.18KB 2.34KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 4.99KB 1.96KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.93KB 7.26KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.48KB 3.50KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

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

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 12, 2026 10:42
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 12, 2026 10:42
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 12, 2026
Merged via the queue into main with commit f38a428 Oct 12, 2026
44 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-12194-managed-by-badge-authoring branch October 12, 2026 11:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[finding] The record page draws ManagedByBadge (「身份」 for better-auth objects) outside the page tree, so a host page cannot leave it out

2 participants