Gate every relationship path on exposeRelationships - #11
Merged
evoactivity merged 2 commits intoAug 7, 2026
Conversation
isRelationExposed was documented as the single home for the relation-visibility rule, but two write paths never went through it. The relationship endpoints used getRelationOrFail, which honoured serializeAs and ignored exposeRelationships, and the deserializer had its own inline serializeAs check, so both a relationships controller and a resource POST or PATCH body could reach a hidden relation. getRelationOrFail now takes the registry and applies the rule. The parameter is required rather than optional so a new call site cannot skip the check, which is how the drift happened. The deserializer consults the same rule for relationship members. A hidden relation is indistinguishable from one that does not exist on every path: 404 on the endpoints, where the name is a URL segment, and the unknown-member 400 in write bodies. A 403 would confirm the relation exists. Closes #10
pnpm owns the lockfile and release-plan owns the changelog. Formatting either buries real changes in noise.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #10.
The problem
isRelationExposedis documented as the single home for the relation-visibility rule, shared by serialization and?include=validation. Two write paths never went through it:getRelationOrFail, which honouredserializeAs: nulland ignoredexposeRelationships. Registering a relationships controller quietly reopened a hidden relation, for reads and writes, on all five routes.serializeAscheck, so aPOSTorPATCHbody with arelationshipsmember naming a hidden relation was accepted.The fix
getRelationOrFailtakes the registry and appliesisRelationExposed. The parameter is required rather than optional, so a new call site cannot skip the check, which is how the drift happened in the first place. The function is internal, so the signature change is not a public API change. The deserializer consults the same rule for relationship members, replacing its inline check.The relation name is resolved before the exposure check, so a kebab URL segment is checked under its Lucid name rather than its URL form.
Hidden means indistinguishable from nonexistent
On every path, a hidden relation behaves exactly like one that was never defined. The endpoints return 404, where the name is a URL segment. Write bodies get the unknown-member 400. A 403 would confirm the relation exists, which is the thing being hidden.
Behaviour change
Anyone using
exposeRelationshipstogether with a relationships controller loses routes that were reachable by mistake. Worth a minor bump rather than a patch.Tests
16 new tests in
tests/unit/relationship_endpoint_exposure.spec.ts, written red first: all five relationship routes reject a hidden relation, hidden and unknown are indistinguishable on both status and title, and the guards, exposed relations still serve, resources withoutexposeRelationshipskeep every relation, unregistered models are unaffected. The to-many write tests use a manyToMany relation deliberately, since hasMany rejects replace and remove with 403 regardless and would mask the gate.Unit suite 128 passed, example app functional suite 52 passed, lint and typecheck clean.
Also adds a
.prettierignoreforpnpm-lock.yamlandCHANGELOG.mdin a separate commit, sopnpm formatstops burying real changes in reformatting noise.