Skip to content

fix: keep conditional rules on branches that carry a title - #74

Open
vishkaty wants to merge 1 commit into
Universal-Commerce-Protocol:mainfrom
vishkaty:fix/conditional-rules-on-annotated-branches
Open

fix: keep conditional rules on branches that carry a title#74
vishkaty wants to merge 1 commit into
Universal-Commerce-Protocol:mainfrom
vishkaty:fix/conditional-rules-on-annotated-branches

Conversation

@vishkaty

@vishkaty vishkaty commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

Observed

EcKeysCarryCrvXYSchema, the type for a published signing key, accepts a key
that declares an elliptic curve type and carries neither a curve nor
coordinates:

EcKeysCarryCrvXYSchema.parse({ kid: "key-1", kty: "EC" })   // accepted
EcKeysCarryCrvXYSchema.parse({ kid: "key-2", kty: "OKP" })  // accepted

profile.json states both obligations as conditional branches on
jwk_public_key, and a standards validator rejects both inputs, reporting that crv is a required property.

Root cause

Two things combined, and both are needed to reach the bug.

describeConditionalRule rejected any branch carrying a key other than if
or then. Every jwk_public_key branch carries a title, for example
"EC keys carry crv, x, y", so all five were judged unmodellable even though two
of them are plain required consequences the recorder already expresses.
Annotation keywords assert nothing, so they are now ignored when the branch
shape is checked.

recordConditionalRules then emptied the whole rule list whenever any branch
was unmodellable. That alone would still have dropped the two expressible
branches, because the other three pair a curve with an algorithm through a
properties consequence the recorder does not model. Unmodellable branches
are now skipped individually rather than taking the list down with them. Every
if/then only adds constraints, so keeping the expressible subset can
only enforce more, never reject something the schema permits.

Still not enforced, deliberately

The three curve to algorithm pairings. Their consequence shape is not modelled,
so a P-256 key declaring ES384 is accepted exactly as before. Widening the
consequence handler is a larger change and I did not want to bundle it.

Testing

Four cases in a new tests/profile-key-conditionals.test.js. Two fail before
this change and pass after, and reverting either half alone reproduces
those two failures, so both parts are load bearing. The other two are controls:
an unrelated key type carries neither obligation, and the members the base
schema already requires still hold.

Suite 151 passed, from 147. Regeneration against release/2026-08-25 is
idempotent, and the exported name set is identical to main. Only
EcKeysCarryCrvXYSchema changes, one hunk.

Regression surface checked rather than assumed: all seven JSON Web Keys the
specification publishes across its documentation and the profile response
scaffold still parse.

Workflow runs on fork pull requests are held behind the approval gate, so I ran
them on my fork for this exact commit. Tests and Lint both green:

Relationship to #73

Independent. #73 changes project-current-ucp-schemas.mjs and the discovery
profile shape; this changes inject-schema-constraints.mjs and the conditional
recorder. Different files, different defect class, no textual overlap. Happy to
sequence them in whichever order suits.

`EcKeysCarryCrvXYSchema`, the published signing key type, accepted
`{ kid, kty: "EC" }` with no curve and no coordinates, and the OKP equivalent.
profile.json states both as conditional branches on jwk_public_key.

Two things in the recorder combined to drop them.

`describeConditionalRule` rejected any branch carrying a key other than `if`
or `then`. Every jwk_public_key branch carries a `title`, so all five were
judged unmodellable even though two are plain `required` consequences the
recorder already knows how to express. Annotation keywords assert nothing, so
they are now ignored when the branch shape is checked.

`recordConditionalRules` then discarded the whole rule list when any branch was
unmodellable, which would still have dropped the two expressible branches
because the three that pair a curve with an algorithm use a `properties`
consequence the recorder does not model. Unmodellable branches are now skipped
individually. Every `if`/`then` only adds constraints, so keeping the
expressible subset can only enforce more, never reject something the schema
permits.

Still not enforced, and out of scope here: the three curve to algorithm
pairings, because their consequence shape is not modelled. A P-256 key
declaring ES384 is accepted as before.

Testing. Four cases, of which two fail before this change and pass after;
reverting either half alone reproduces the two failures. Suite 151 passed, from
147. Regeneration against release/2026-08-25 is idempotent and the exported
name set is unchanged. Only `EcKeysCarryCrvXYSchema` changes, one hunk. All
seven JSON Web Keys the specification publishes, and the profile response
scaffold, still parse, so the regression surface is empty.
@damaz91 damaz91 added the status:needs-triage Signal that the PR is ready for human triage label Sep 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

status:needs-triage Signal that the PR is ready for human triage

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants