Skip to content

fix(platform-objects): drop the dead Setup › Advanced › Signing Keys (JWKS) nav entry (#7544) - #7909

Merged
huangyiirene merged 4 commits into
mainfrom
claude/issue-7544-jwks-dead-nav
Aug 12, 2026
Merged

fix(platform-objects): drop the dead Setup › Advanced › Signing Keys (JWKS) nav entry (#7544)#7909
huangyiirene merged 4 commits into
mainfrom
claude/issue-7544-jwks-dead-nav

Conversation

@huangyiirene

Copy link
Copy Markdown
Collaborator

Fixes#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 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_jwks holds live JWT signing-key material and its API-disabled state is deliberate. Measured on origin/main:

ClaimMeasurementVerdict
Holds private key materialsys-jwks.object.ts:56private_key: Field.textarea({ description: 'JSON-serialized JWK private key (encrypted at rest)' })✅ confirmed
API-disabled deliberatelySame shape as the six sys_oauth_* token/consent stores; apiMethods: [] fails CLOSED by design (#3391); access.default: 'private' pinned by platform-objects.test.ts "secure-by-default posture" and by authz-conformance.matrix.ts✅ confirmed
No declared surface reads it over the APIThe three plugin-auth readers (auth-manager.ts, auth-schema-config.ts, jwt-key-algorithm.ts) go through better-auth's adapter under a system context, not REST✅ confirmed
The entry is dead for everyoneapiAccessDenialFromEnable (rest-server.ts:1658) is a pure function of enable — no user, no permissions, no context — and enforceApiAccess passes it only object metadata✅ confirmed

So 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:137 as a possible primary source. It contradicted itself:

nav_jwks is capability-gated (like nav_api_keys): sys_jwks is access.default:'private' (ADR-0066 ④ — signing keys), so a non-admin's list request 403s server-side

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 enable block had already made unreachable, which is why the entry survived review. It is replaced with what actually happens.

What landed

sys_jwks's enable block is unchanged, and a test now pins that it stays apiEnabled: 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):

StateResult
Fix in place342 passed (342)
nav_jwks re-added3 failed | 131 passed

The 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's expected [ 'nav_jwks' ] to deeply equal [].

One iteration was needed to earn that first message: sys_jwks initially 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_keyssys_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 touched packages/spec incl. object.zod.ts and rest-server.ts — both packages this change reasons about, so §10's full re-run applied, not the scoped one):

GateResult
@objectstack/platform-objects342 passed
@objectstack/cli1190 passed
@objectstack/runtime2086 passed
@objectstack/dogfood588 passed, 3 skipped
check:app-nav-i18nOK — boots the real composition: 53 merged setup nav ids (was 54), all labelled in 4 locales
check:i18n / check:i18n-coverageOK — 9 packages in sync; 660 baselined, none new
check:nul-bytes / check-platform-checklistOK
spec check:generated13/13 up to date
typecheck + eslint (changed files)clean

⭐ Reported, not implemented: the general gap

The card's more valuable half — a requiredPermissions gate 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-level requiredPermissions, requiresService (ADR-0057 D10), and collapses empty groups (#7380). Its own docblock names what it does not gate:

NOT gated here: visible (CEL) at any level, and requiresObject — both are still evaluated client-side only. That asymmetry is deliberate and pinned in rest.test.ts.

Nothing in nav filtering consults enable.apiEnabled at all. So re-pointing this entry at a requiresObject gate would not have pruned it either — it would have shipped in the /meta payload exactly as before, which is also why the sibling defect (the Account app served whole to a member denied every backing object) survives despite using requiresObject. 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 objectui surface, not this repo's.


Generated by Claude Code

…(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>
@vercel

vercelBot commented Aug 12, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 12, 2026 3:48am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/platform-objects.

2 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/plugins/packages.mdx(via @objectstack/platform-objects)
  • content/docs/ui/setup-app.mdx(via @objectstack/platform-objects)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@huangyiireneClaude

Copy link
Copy Markdown
CollaboratorAuthor

ACCEPTdomain:metadata seat, PM review. Binds to head 5e63357. ⚠️ Re-verify if the head moves.

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

Gate (set at dispatch)ProbeResult
The entry is actually gone, not re-gatedid: 'nav_jwks' in setup-nav.contributions.tsmain 1 → branch 0
CONTROL — nav_api_keys must surviveid: 'nav_api_keys', same filemain 1 → branch 1
⛔ The object is untouchedgit diff on src/objects/**empty ✅
⚠️The --fill=default trap (#7278/#7309)git diff on *objects.generated.tsempty ✅
…and no English seeded into a non-en bundle+ lines in es-ES.ts / ja-JP.tsnone — pure deletions ✅
content/docs/releases/ untouchedgit diff --name-onlyempty ✅
Changeset.changeset/setup-nav-jwks-dead-entry.md, platform-objects: patchpresent ✅
Fixes #7544 is the first body lineread directly

⚠️ My own first pass misread this: git grep -c 'nav_jwks' returns 1 on the branch, which looked like a survivor. It is the replacement comment. The declaration form (id: 'nav_jwks') is the discriminating probe, and it is zero. Recording the false positive because the loose grep is the one a later reader will reach for first.

The invariant test earns its place

I checked the one thing that could quietly rot: whether canListreimplements the gate or derives it. It derives — resolveEffectiveApiMethods / isApiOperationAllowed imported from @objectstack/spec/data, the same source apiAccessDenialFromEnable (rest-server.ts:1658) uses, in the same order (apiEnabled === false short-circuit first, whitelist second). It also carries a vacuity control (entries.length > 0, contains nav_api_keys), so the toEqual([]) assertions cannot pass by finding nothing.

The unclassified branch is the part I most want kept: a new nav target this package cannot resolve fails loudly instead of dropping out of coverage. That is what turns a one-instance fix into a standing gate.

Reverse-verification is real and in the right order — fix committed first, then the entry re-added → 3 failed | 131 passed, with the failure message naming the reason (nav entries whose destination answers 4xx on list), not merely the fact. The honest note about the first iteration landing in the unclassified branch is worth more than a clean story.

The two out-of-surface files: both kept, one was load-bearing

You flagged going beyond the named surface and offered to revert. ⛔ Don't.

docs/qa/platform-checklist/areas/platform-core.json instructed the next runner to confirm nav_jwks is "PRESENT for admin". Left as written, the next QA run files this removal as a regression and re-opens #7544 — the fix would have been undone by its own verification procedure. Correcting it is not scope creep; omitting it would have been the defect.content/docs/ui/setup-app.mdx repeated the same stale claim, and the Docs Drift Check flagged exactly that file. I checked the second file it flagged, content/docs/plugins/packages.mdx: zero hits for jwks / signing key, so nothing stale is left there. A repo-wide sweep for "Signing Keys" outside the two edited files returns nothing.

⭐ Both splits are now filed — a sentence in a report is not a card

Standing 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:

ProbeResult
apiEnabled in rest-server.ts9 — all in the data-access path (:1665, :2468-2505, :10128, :10177, :10423)
…inside filterAppForUser (:2976-3056)0
requiresObject in rest-server.ts1 — the docblock line saying it is not gated there

Confirmed. Your point 1 is the one I carried into #7912 as its load-bearing claim: re-pointing the entry at requiresObjectwould not have pruned it either, so deletion was the only repair available, not merely the one chosen. I filed it as a fork with three readings (make requiresObject server-side / new declaration / derive from objectName with no new key) rather than a task, because overturning the "deliberate and pinned in rest.test.ts" asymmetry is a decision to take in the open.

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 code even survives the transport.

Landing

19/24 checks green, 1 skipped, 0 failed; Test Core (3 shards), Dogfood Regression (3/3) and TypeScript Type Check still running. ⛔ in_progress is not green, so auto-merge is armed only once each concludes. Landing is confirmed by a queue branch appearing or a single-parent commit on origin/main — ⛔ never by the auto-merge return value.

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 enable block had already made unreachable. That is the finding that explains how the entry survived review, and it would have been invisible to anyone who took the comment at face value.


Generated by Claude Code

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Setup › Advanced › Signing Keys (JWKS) is a dead nav entry — sys_jwks is API-disabled and a requiredPermissions gate cannot prune it

2 participants

@huangyiirene@claude