Uh oh!
There was an error while loading. Please reload this page.
fix(platform-objects): drop the dead Setup › Advanced › Signing Keys (JWKS) nav entry (#7544) - #7909
Conversation
…(JWKS) nav entry (#7544) `Setup › Advanced › Signing Keys (JWKS)` could never load, for any persona. `sys_jwks` declares `enable.apiEnabled: false` / `apiMethods: []`, so the list request answers `OBJECT_API_DISABLED` (404) — and the console masked that as a generic "No identity records" empty state, so the surface read as "you have no signing keys" rather than "this page cannot work". The entry carried `requiredPermissions: ['manage_platform_settings']` and a comment claiming a non-admin "403s server-side", which reads as though an admin could list the keys. None could: `apiAccessDenialFromEnable` (rest-server.ts) is a pure function of the object's `enable` block — no user, no permissions, no context — so the 404 is identical for every persona, platform admin included. A permission gate on the entry and an API-disabled object are independent conditions, and no combination of the first prunes the second. The repair is the entry, never the object: `sys_jwks` rows are the environment's JWT signing keys (`private_key`), and opening a read path onto them over the generic data API would be a credential disclosure. `enable` is unchanged and now pinned. better-auth keeps reading the keys through its adapter under a system context, so token signing and verification are unaffected. This matches how the same class is handled two lines below: `sys_verification` and `sys_device_code` omit `list` and get no browse entry. `sys_jwks` was the only one of the repo's seven API-disabled objects that still had a nav entry. - The four `apps.setup.navigation.nav_jwks` labels move into the `DEAD_SETUP_NAV_IDS` tombstone, per the order that file states. The `sys_jwks` object labels in the generated bundles are untouched — the object still exists. - New invariant in `platform-objects.test.ts`: every contributed `type: 'object'` Setup entry must target an object that can actually serve a `list`, judged through the same derivation source the REST gate uses (#3391). It asserts the control too — `nav_api_keys` → `sys_api_key` still lists, so a fix that pruned both fails. Not addressed here (reported on #7544): nav gating has no declaration that can express "prune when the destination cannot serve" — `filterAppForUser` leaves `requiresObject` to the client and nothing consults `enable.apiEnabled`. Co-Authored-By: Claude <noreply@anthropic.com>
…not on being unknown (#7544) Listing `sys_jwks` in the lookup table makes a re-added entry fail the "cannot serve a list" assertion — the actual reason — instead of falling into the unclassified branch, which reports only that the test file has never heard of the object. Co-Authored-By: Claude <noreply@anthropic.com>
…aviour (#7544) `builtin-apps-nav-render` — the item whose run produced this card — told the next runner to confirm `nav_jwks` is "PRESENT for admin". It never could be: `sys_jwks` is `apiEnabled: false`, so its list answers `OBJECT_API_DISABLED` for every caller. Left as written, the next run would have filed the removal as a regression. The member-side step loses JWKS from its gated-entry list for the same reason: an entry absent for admins too proves nothing about whether the server prunes by permission. API Keys and the settings URLs still carry that step. Co-Authored-By: Claude <noreply@anthropic.com>
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 1 package(s): 2 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
huangyiirene
commented
Aug 12, 2026
ACCEPT — The ruling's premise was falsifiable and the dev tested all four halves rather than implementing against it. That is the outcome the three-partition brief exists to produce. Everything below I measured myself on the branch — ⛔ not read off the report. Review gates, each measured
The invariant test earns its placeI checked the one thing that could quietly rot: whether The Reverse-verification is real and in the right order — fix committed first, then the entry re-added → The two out-of-surface files: both kept, one was load-bearingYou flagged going beyond the named surface and offered to revert. ⛔ Don't.
⭐ Both splits are now filed — a sentence in a report is not a cardStanding rule on this seat: the existence test for a split is an issue number. Your two reports are cards, not paragraphs:
Before filing #7912 I re-ran your central measurement independently, because a PM transcribing a dev's mechanism into a card that a third dev implements against is exactly where an unverified claim gets laundered into a specification:
Confirmed. Your point 1 is the one I carried into #7912 as its load-bearing claim: re-pointing the entry at The #4408 body marks its own mechanism section as ⛔ a PM assumption, not a finding — I have not read that repo's fetch path — and names what to measure first, including whether ADR-0112's Landing19/24 checks green, 1 skipped, 0 failed; Test Core (3 shards), Dogfood Regression (3/3) and TypeScript Type Check still running. ⛔ Nothing to change. Excellent work — particularly reading the in-code comment as a primary source and finding that it documented an intent the object's own Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#7544
Setup › Advanced › Signing Keys (JWKS)could never load, for any persona.sys_jwksdeclaresenable.apiEnabled: false/apiMethods: [], so the list request answersOBJECT_API_DISABLED(404) — and the console masked that as a generic "No identity records" empty state, so the surface read as "you have no signing keys" rather than "this page cannot work".The premise was testable, and it held
The dispatch ruled prune the entry, ⛔ never "enable the surface", on a premise I was asked to falsify:
sys_jwksholds live JWT signing-key material and its API-disabled state is deliberate. Measured onorigin/main:sys-jwks.object.ts:56—private_key: Field.textarea({ description: 'JSON-serialized JWK private key (encrypted at rest)' })sys_oauth_*token/consent stores;apiMethods: []fails CLOSED by design (#3391);access.default: 'private'pinned byplatform-objects.test.ts"secure-by-default posture" and byauthz-conformance.matrix.tsplugin-authreaders (auth-manager.ts,auth-schema-config.ts,jwt-key-algorithm.ts) go through better-auth's adapter under a system context, not RESTapiAccessDenialFromEnable(rest-server.ts:1658) is a pure function ofenable— no user, no permissions, no context — andenforceApiAccesspasses it only object metadataSo the fork is settled by measurement, not only by ruling: the object is right and the entry was wrong.
The in-code comment was the actual bug report
The dispatch flagged
setup-nav.contributions.ts:137as a possible primary source. It contradicted itself:That describes a permission outcome, which reads as though an admin could list the keys. None could — the 404 precedes and ignores permissions entirely. The comment documented an intent the object's own
enableblock had already made unreachable, which is why the entry survived review. It is replaced with what actually happens.What landed
nav_jwksremoved fromSETUP_NAV_CONTRIBUTIONS, folded into the comment two lines below that already governs this exact class (sys_verification/sys_device_codeomitlist, so they get no browse entry).sys_jwkswas the only one of the repo's seven API-disabled objects that still had a nav entry.apps.setup.navigation.nav_jwkslabels moved intoDEAD_SETUP_NAV_IDS(setup-nav-dead-key-tombstone.test.ts), following the order that file states: nav item and label die in one commit.sys_jwksobject labels in the four*.objects.generated.tsbundles are untouched — the object still exists, so noos i18n extract --fill=defaultwas run and the approvals:sys_approval_request.approval_reject/approval_recalldeclare bothconfirmTextandparams, so one decision opens two sequential dialogs #7278 / platform-objects: 16 more actions declare bothconfirmTextandparams, so one click opens two sequential dialogs (same shape as #7278) #7309 trap is not in play.platform-objects.test.ts: every contributedtype: 'object'Setup entry must target an object that can actually serve alist, judged through the same single derivation source the REST gate uses (resolveEffectiveApiMethods/isApiOperationAllowed, 跟踪:UI 操作按钮与 apiMethods 白名单一致性契约落地(#3026 设计定稿) #3391) in the same order. Unclassifiable new targets fail loudly rather than dropping out of coverage.builtin-apps-nav-render— the item whose run produced this card — instructed the next runner to confirmnav_jwksis "PRESENT for admin". Left as written it would have filed this removal as a regression. Beyond the dispatch's named file surface; called out here deliberately.content/docs/ui/setup-app.mdxrepeated the same stale "non-admins are denied server-side" claim.⛔
sys_jwks'senableblock is unchanged, and a test now pins that it staysapiEnabled: false/apiMethods: []/access.default: 'private'. better-auth keeps signing and verifying tokens through its adapter.Verification
Reverse-verified (fix committed first, then the entry re-added — never against an uncommitted edit):
342 passed (342)nav_jwksre-added3 failed | 131 passedThe three reds are the three claims:
nav entries whose destination answers 4xx on list — see #7544: expected [ 'nav_jwks → sys_jwks' ] to deeply equal [], the id-specific assertion, and the tombstone'sexpected [ 'nav_jwks' ] to deeply equal [].One iteration was needed to earn that first message:
sys_jwksinitially fell into the unclassified branch, so the gate reported only that it had never heard of the object. Listing the removed object in the lookup table makes a re-added entry fail on why it is dead (commit 2).The negative direction is asserted.
nav_api_keys→sys_api_key— the card's own control, same machinery, no 4xx — must keep working: the invariant test proves it still lists, so a fix that pruned both would fail here.All gates run on the tree after merging
origin/main(d91fad5c, which touchedpackages/specincl.object.zod.tsandrest-server.ts— both packages this change reasons about, so §10's full re-run applied, not the scoped one):@objectstack/platform-objects@objectstack/cli@objectstack/runtime@objectstack/dogfoodcheck:app-nav-i18nsetupnav ids (was 54), all labelled in 4 localescheck:i18n/check:i18n-coveragecheck:nul-bytes/check-platform-checklistspec check:generatedtypecheck+eslint(changed files)⭐ Reported, not implemented: the general gap
The card's more valuable half — a
requiredPermissionsgate cannot prune an API-disabled object — is not fixed here, per the dispatch's ⛔. What I measured about it:filterAppForUser(rest-server.ts:2976-3056) gates_unpublished, app- and item-levelrequiredPermissions,requiresService(ADR-0057 D10), and collapses empty groups (#7380). Its own docblock names what it does not gate:Nothing in nav filtering consults
enable.apiEnabledat all. So re-pointing this entry at arequiresObjectgate would not have pruned it either — it would have shipped in the/metapayload exactly as before, which is also why the sibling defect (the Account app served whole to a member denied every backing object) survives despite usingrequiresObject. There is today no declaration a nav entry can carry that expresses "prune when the destination cannot serve", which is what makes deletion the only honest repair available rather than merely the chosen one.Closing that is a contract-face change (
domain:spec) and belongs in its own card.Separately, still open: the console masks a 404 as a generic empty state. That masking is untouched here and still applies to other objects — a page that cannot work will keep reading as a page with no records. Flagged as the dispatch asked; it is a
objectuisurface, not this repo's.Generated by Claude Code