Repository navigation
feat(app-shell,data-objectstack): a refused dashboard or list view opened by URL says it was refused (objectui#12112) - #12162
Conversation
…it was refused (objectui#12112) The console reads `/meta/dashboard` and `/meta/view` as lists, and since objectstack#22731 those lists carry only what the caller may open. A dashboard or view withheld by `requiredPermissions` was therefore read as "not found" (dashboard) or silently replaced by the default view (list view). A miss is now asked by name through the adapter's new `probeMetaItemAccess` (the generalised `probeAppAccess` door); a `403 PERMISSION_DENIED` renders a permission refusal in the server's own sentence, every other answer keeps today's face. Claude-Session: https://claude.ai/code/session_01TYgwmFK1q4KJ6Qq2WRLzsD Co-authored-by: Claude <noreply@anthropic.com>
Brings in objectui#12140 (158c75d) and objectui#12142 (dc6d28e); neither touches this branch's files. Claude-Session: https://claude.ai/code/session_01TYgwmFK1q4KJ6Qq2WRLzsD Co-authored-by: Claude <noreply@anthropic.com>
…re; docs and changeset (objectui#12112) The adapter pin reads a refused dashboard and a refused list view through the real client over a stubbed fetch. The list page pin now mounts real i18n and covers split navigation. The console guide and the adapter README say what a refused dashboard or list view URL shows. Claude-Session: https://claude.ai/code/session_01TYgwmFK1q4KJ6Qq2WRLzsD Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
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
Size Limits
|
… public probe (objectui#12112) `MetaItemAccess` and `ObjectStackAdapter.probeMetaItemAccess` widen the package's published surface, so the declaration is minor, the level this repository gives an additive published widening. `@object-ui/app-shell` stays patch. Claude-Session: https://claude.ai/code/session_01TYgwmFK1q4KJ6Qq2WRLzsD Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
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
Size Limits
|
Contract reviewServed-tier: Read at 2026-10-11T21:55Z, through the REST API only: card objectui#12112 (body, triage ① Derived judgmentsPublic surface.
Accept set (what each page now answers).
Check-runs on the head, read at 2026-10-11T21:55Z (their conclusions are the gate verdicts): success for Lint, Type-adjacent and doc gates ( ② Semver level
③ Boundary flagsDev report
Reviewer flags, read in the diff, neither a FAIL:
Prose faces ( Implemented-by: VERDICT: PASS |
Fixes #12112
Clause-②: yes
The console half of objectstack-ai/objectstack#22611's ruling A (director's ruling
6095014058), as triage6103491895scoped it. Dispatched by thedomain:ui#2seat; claim6109832410; implementing sessionhttps://claude.ai/code/session_01TYgwmFK1q4KJ6Qq2WRLzsD.What changes
Since objectstack-ai/objectstack#22731 (
5081090b71) the server lists only the dashboards and list views a caller may open, and refuses a by-name read of one they do not hold with403 PERMISSION_DENIEDand the sentenceYou do not have permission to open the 'NAME' dashboard.(orview). The console read only the lists.DashboardView). A dashboard missing from the caller's list is now asked by name once the list has loaded. APERMISSION_DENIEDanswer shows the refusal: the title "You don't have access" (dashboard.widgetForbiddenTitle, an existing key) and, under it, the server's own sentence, which names the dashboard. Every other answer (not_found,unreachable,granted) keeps today's "Dashboard Not Found". While the list is still loading, the page now shows its skeleton instead of a premature "not found".ObjectView). A requested view id that misses the merged view list is asked by name, by its canonicalOBJECT.KEYname (a short name is the key). APERMISSION_DENIEDanswer replaces the list area, in both layouts (the plain list and split navigation), with "You don't have access" (list.loadErrorForbiddenTitle, an existing key) and the server's sentence; no switcher tab is marked active. Every other answer keeps today's fall-back to the default view.@object-ui/data-objectstack).ObjectStackAdapter.probeMetaItemAccess(type, name)is the existingprobeAppAccessdoor, widened to a type parameter: the sameclient.meta.getItemread, the same verdicts, read off thePERMISSION_DENIEDcode and never off the status. It also returns the refusal'smessagewith adeniedverdict.probeAppAccessnow delegates to it and answers exactly as before (its pin file is unchanged and green).useMetaItemRefusal(packages/app-shell/src/hooks/useMetaItemRefusal.ts) asks once per missed name, holds the answer with the name it is about, caches nothing across mounts, and asks nothing in draft preview or when the host adapter has no probe (the same two skipsAppContentmakes for its app probe).Clause-②: yes:@object-ui/data-objectstackexports a new interface,MetaItemAccess, andObjectStackAdaptergains a public method,probeMetaItemAccess; no translation key is added. The refusal's sentence is the server's, shown untranslated, asmetadataReadWarningToastalready does for server text.Mechanism readings (the dispatch's hypotheses, measured)
DashboardViewresolves the dashboard from the/meta/dashboardLIST (preferLocal(dashboards, …)), so a refused dashboard is absent and fell into the not-found branch. Proven on BASE by the new pin, run before the view was edited:Unable to find an element by: [data-testid="dashboard-access-denied"], withempty.dashboardNotFoundandempty.dashboardNotFoundDescriptionin the DOM. The same pin run showed the not-found face also renders while the list is stillloading.403was lost.@objectstack/clientkeeps it: its fetch stampserror.codefrombody.error.code,error.httpStatusanderror.message. The console flattened it in two places:MetadataProvider.getItemresolvesnullon any failure (souseMetadataItem'serrorslot is never filled), and the adapter'sgetView/getApp/getPageresolvenulltoo.probeAppAccesswas the one by-name door that read the code, for apps only, so that is the door this widens rather than adding a second fetch path.data-active-view="clm_contract.default"and the console warningRequested view "clm_contract.legal_review" not found …; falling back to the default view.studio.accessholder without a view's capability:/meta/viewitem (an item, or a container's expanded entry) is pruned from/meta/viewfor every caller (createMetaListReadGate'sviewarm has no D4 exemption) and is never in the switcher;listViewsentry is served whole inside/meta/objectto a caller who may edit the object (isObjectSchemaMaskExempt), so the console's switcher lists it (mergeViewsIntoObjectskeeps the object's entries,buildViewTabsdraws them) and it opens. There is no by-name/meta/viewitem for such an entry (the server's own pin fixture,meta-view-dashboard-audience-gate.test.ts, haslegal_deskonly inside the object), so a by-name read answers absence, not a refusal. The triage premise "the by-name read refuses it" does not hold for this path. The spec describe says the same: an object's own list views are pruned "for a user who may not edit the object".D4 (measured)holds that the entry is listed, opens, and nothing is asked or refused.dashboard.widgetForbiddenTitleandlist.loadErrorForbiddenTitle("You don't have access", present in all ten packs). The item is named by the server's sentence.Pins
DashboardView.refusal-12112.test.tsx: a refused dashboard URL (with and without a sentence); CONTROL a held dashboard opens as today and asks nothing; CONTROLnot_found/unreachable/grantedkeep "not found"; CONTROL a host adapter with no probe; CONTROL draft preview asks nothing; nothing is asked or said while the list is loading.ObjectView.viewRefusal-12112.test.tsx: a refused list view by name (plain and split navigation), a short name asked canonically; CONTROL a held view opens by name and asks nothing; CONTROL a missing view falls back to the default; the D4 reading.metaItemAccessProbe-12112.test.ts: the adapter over the real client and a stubbed fetch: a refused dashboard and a refused view (denied+ sentence, by-name address), the item-less 200, a served item, a 403 without the code, empty inputs, andprobeAppAccessas the same probe.Verification
All on this branch. "HEAD" below is
1104e4a(the merge ofmainatfaf4ce9) unless a line names8e35d3c(the tree before that merge; the merged commits touch none of this branch's files). Heavy runs went through the shared verify lock.empty.dashboardNotFound; the refused view rendereddata-active-view="clm_contract.default".turbo run build --filter=@object-ui/app-shell^... --concurrency=2, 28/28 tasks, on1104e4a.1104e4a:@object-ui/data-objectstacktsc --noEmitexit 0 (its file set includes the new pin, pertsc --showConfig);@object-ui/app-shelltsc --noEmit && tsc -p tsconfig.test.jsonexit 0 (the test config includessrc/**/*.test.tsx, so both new view pins are type-checked).1104e4a: the three new pin files,appAccessProbe.test.ts, the four existingDashboardView.*pins,AppContent.deniedVsUnpublishedandAppContent.missingRecheckKeying,column-identity.ratchetandone-authority-per-exported-name-6273: 12 files, 98 tests passed.8e35d3c, each leg throughablation-replace.mjs(anchor hit 1 to 0, blob changed, restore proven: blob equals HEAD andgit diff HEADempty), with an EXIT/INT/TERM restore trap:deniedbranch made unreachable: 5 red (both dashboard refusal cases, the view refusal in plain and split navigation, the short-name case); every CONTROL green.PERMISSION_DENIEDbranch made unreachable: 5 red (the dashboard and viewdeniedcases,probeAppAccessas the same probe, and twoprobeAppAccessdeniedcases inappAccessProbe.test.ts, which shows the app probe now goes through the widened door).8e35d3c:packages/app-shell/1,272 files (1 skipped), 12,314 tests passed and 9 skipped (8 shards; shards 5 to 8 exceeded the shard timeout under load and were re-run as twelve 53-file chunks, whose file lists were checked identical to vitest's own sequencer's);packages/data-objectstack/83 files, 1,109 tests;scripts/__tests__/4 shards, 182 files (2 skipped), 5,489 tests passed and 2 skipped.faf4ce9merge; the post-merge reading is the 12-file set above plus both type-checks. CI runs the full suites.1104e4a:check-changeset-presence,check:changeset-claims,check:pending-changeset-literals,check:new-line-citations(0 new citations),check:control-bytes,check:test-path-roots, the threecheck:vi-mock-*,check:phantom-deps,check:i18n-keys,check:i18n-dead-keys,check:unreferenced-sourcesanddocs:check-links: all exit 0.eslint(inline config honoured) on the seven touched source and test files: 0 errors; 306 warnings, 0 of them on an added line.check:doc-snippets: PRECONDITION NOT MET (exit 2), it needs a 34-package build; CI's doc-snippet workflow builds it. It now covers the newtsfence in the adapter README.check:readme-exports: exit 1 for 94 self-imports in 8 packages outside the built closure ("could not be judged"). The judged part, the adapter README included, reads 0 fabricated and 0 wrong-path.Acceptance notes
MetadataProvider.getItemresolvesnullon every failure, and itsextractItemreturns the whole envelope as the item when the by-name answer carries noitemkey. That is the server's200for a name with nothing behind it. A colduseMetadataItem(type, absentName)would therefore hold an envelope as if it were the document. Nothing in this change readsgetItem.Generated by Claude Code