#4120: Selection CredentialSelector — filter candidates by PD field ID values#4121
#4120: Selection CredentialSelector — filter candidates by PD field ID values#4121stevenvegt wants to merge 2 commits intofeature/4088-credential-selectorfrom
Conversation
…D values NewSelectionSelector creates a CredentialSelector that filters candidates using named field ID values from the credential_selection API parameter. Selection keys are validated against PD field IDs at construction time. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
0 new issues
|
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Coverage Impact This PR will not change total coverage. Modified Files with Diff Coverage (1)
🤖 Increase coverage with AI coding...🚦 See full report on Qlty Cloud » 🛟 Help
|
There was a problem hiding this comment.
Pull request overview
Adds a new Presentation Exchange CredentialSelector factory (NewSelectionSelector) that narrows already-matching credential candidates using credential_selection key/value pairs that reference PD constraint field IDs. This supports deterministic credential selection when multiple VCs satisfy the same input descriptor.
Changes:
- Introduce
NewSelectionSelectorinvcr/pe/selector.go, including construction-time validation of selection keys against PD field IDs and per-descriptor fallback behavior. - Add comprehensive unit tests for selection behavior, including AND semantics, fallback behavior, and expected error cases.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
vcr/pe/selector.go |
Implements NewSelectionSelector and the field-value filtering logic for candidates. |
vcr/pe/selector_test.go |
Adds tests covering single match, zero/multiple matches, unknown key validation, fallback, AND semantics, and multi-descriptor behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| var matched []vc.VerifiableCredential | ||
| for _, candidate := range candidates { | ||
| if descriptor.Constraints == nil { | ||
| continue | ||
| } | ||
| isMatch, values, err := matchConstraint(descriptor.Constraints, candidate) | ||
| if err != nil || !isMatch { | ||
| continue | ||
| } | ||
| if matchesSelections(values, selections) { | ||
| matched = append(matched, candidate) |
There was a problem hiding this comment.
NewSelectionSelector re-runs matchConstraint for each candidate even though the selector contract says all candidates already match the input descriptor (they’re produced by PresentationDefinition.matchConstraints). This duplicates JSON remarshal + full constraint evaluation and can be a noticeable overhead when wallets contain many VCs. Consider resolving only the selected field values (e.g., build a fieldID→Field map at construction and use matchField/getValueAtPath on a single remarshal), without re-checking the entire constraint set.
| continue | ||
| } | ||
| isMatch, values, err := matchConstraint(descriptor.Constraints, candidate) | ||
| if err != nil || !isMatch { |
There was a problem hiding this comment.
Errors from matchConstraint are currently swallowed (if err != nil || !isMatch { continue }). If matchConstraint ever fails (e.g., due to unexpected credential encoding/path issues), this will silently drop candidates and surface as ErrNoCredentials/ErrMultipleCredentials, masking the real problem. Return the error immediately when err != nil so the caller gets a hard failure with the root cause.
| if err != nil || !isMatch { | |
| if err != nil { | |
| return nil, fmt.Errorf("input descriptor '%s': %w", descriptor.Id, err) | |
| } | |
| if !isMatch { |
| return func(descriptor InputDescriptor, candidates []vc.VerifiableCredential) (*vc.VerifiableCredential, error) { | ||
| selections, ok := descriptorSelections[descriptor.Id] | ||
| if !ok { | ||
| return fallback(descriptor, candidates) | ||
| } |
There was a problem hiding this comment.
NewSelectionSelector may call fallback(descriptor, candidates) without guarding against a nil fallback. Since this is a public factory, passing a nil fallback would panic at runtime. Consider defaulting fallback to FirstMatchSelector when nil (or returning an error at construction time).
|
|
||
| // NewSelectionSelector creates a CredentialSelector that filters candidates using | ||
| // named field ID values from the credential_selection parameter. | ||
| func NewSelectionSelector(selection map[string]string, pd PresentationDefinition, fallback CredentialSelector) (CredentialSelector, error) { |
There was a problem hiding this comment.
Sounds like it selects a selection, but it just selects credentials? So why not NewCredentialSelector?
| continue | ||
| } | ||
| isMatch, values, err := matchConstraint(descriptor.Constraints, candidate) | ||
| if err != nil || !isMatch { |
There was a problem hiding this comment.
is it worth logging the error, if we're not returning it? In which cases cant his occur?
| if descriptor.Constraints == nil { | ||
| continue | ||
| } | ||
| isMatch, values, err := matchConstraint(descriptor.Constraints, candidate) |
There was a problem hiding this comment.
We have multiple constraint types in Presentation Definitions. We generally use constant (value must be exactly X) or pattern (value must comply to regex pattern Y). Do we want to support both?
Does that work properly with matchesSelections() which compares the given value to the matched value? Because it sounds like we only want to support constant.
| if str != sel.expected { | ||
| return false | ||
| } | ||
| } else if fmt.Sprintf("%v", resolved) != sel.expected { |
There was a problem hiding this comment.
why are we doing this? Support for bools and numbers? Does that actually work?
Why not just comparison through ==?

Summary
NewSelectionSelectorfactory invcr/pe/selector.gothat creates aCredentialSelectorfiltering candidates by PD field ID valuesmatchConstraintlogic, filters by equalityFirstMatchSelectorErrNoCredentials(soft failure via Refactor PD matching to return all candidates per input descriptor #4088), multiple matches →ErrMultipleCredentialsCloses #4120
Part of #4067
Test plan
ErrNoCredentialsErrMultipleCredentials🤖 Generated with Claude Code