# Feature Specification: Credential Filtering API & Parsed Output

**Feature Branch**: `003-credential-filtering-api`
**Created**: 2026-01-21
**Status**: Draft
**Input**: User description: "Enhance SDK with comprehensive credential filtering helpers, claim constraints, and full credential output parsing for flexible presentation requests"

## User Scenarios & Testing *(mandatory)*

### User Story 1 - Enterprise Domain Verification (Priority: P1)

An enterprise customer wants to verify that users presenting employment credentials work for a specific company. They need to constrain the credential presentation to only accept employment credentials issued by or verified against a specific enterprise domain (e.g., "acme.com") and optionally require that the issuer be a specific trusted enterprise DID.

**Why this priority**: Enterprise identity verification is a core use case for B2B scenarios. Without domain filtering, enterprises cannot validate that employees belong to their organization.

**Independent Test**: Can be fully tested by creating a policy that filters employment credentials by employer domain "acme.com" and verifying only matching credentials are accepted.

**Acceptance Scenarios**:

1. **Given** an SDK user building an employment verification flow, **When** they create a policy with `.with_employer_domain("acme.com")`, **Then** the presentation request only accepts credentials with matching employer domain.
2. **Given** an SDK user requiring a specific issuer, **When** they create a policy with `.with_issuer_did("did:lastid:enterprise-issuer")`, **Then** only credentials from that issuer are accepted.
3. **Given** an SDK user combining domain and issuer constraints, **When** they chain `.with_employer_domain("acme.com").with_issuer_did("did:lastid:acme")`, **Then** both constraints are enforced.

---

### User Story 2 - Subject Matching Constraints (Priority: P1)

A relying party wants to ensure the credential being presented belongs to a specific known subject (user). They need to request credentials where the subject DID matches a previously established identity binding.

**Why this priority**: Subject binding is essential for session continuity and re-authentication flows where the RP already knows the user's DID from a previous interaction.

**Independent Test**: Can be fully tested by creating a policy with `.with_subject_did("did:lastid:user123")` and verifying only that user's credentials are accepted.

**Acceptance Scenarios**:

1. **Given** a relying party with an existing user binding, **When** they create a policy with `.with_subject_did("did:lastid:user123")`, **Then** only credentials with that subject are accepted.
2. **Given** a credential presented with a non-matching subject, **When** validation occurs, **Then** the presentation is rejected with a clear error.

---

### User Story 3 - Email Domain Filtering on Persona Credentials (Priority: P1)

A SaaS application wants to verify that a user's persona credential contains an email from an approved domain (e.g., only accept users with "@bigcorp.com" emails) without requiring a full VerifiedEmail credential.

**Why this priority**: Many applications need to restrict access based on email domain without requiring formal email verification, using self-attested persona data with domain constraints.

**Independent Test**: Can be fully tested by creating a PersonaPolicy with `.with_email_domain("bigcorp.com")` and verifying only matching emails are accepted.

**Acceptance Scenarios**:

1. **Given** an SDK user restricting email domains, **When** they create a PersonaPolicy with `.with_email_domain("bigcorp.com")`, **Then** only credentials with emails ending in "@bigcorp.com" are accepted.
2. **Given** an SDK user requiring verified email domain, **When** they create a VerifiedEmailPolicy with `.with_domain_constraint("bigcorp.com")`, **Then** verified email credentials are required for that domain.

---

### User Story 4 - Full Credential Output Parsing (Priority: P1)

An SDK customer receives a verified credential response and needs to access all claims without complex JSON parsing. They want typed accessors for common fields like email, phone, name, verification timestamps, and device attestation metadata.

**Why this priority**: Developer experience is critical. Forcing customers to parse raw JSON for common fields creates friction and potential bugs.

**Independent Test**: Can be fully tested by verifying a credential and accessing claims via typed methods like `.email()`, `.phone_number()`, `.display_name()`, `.verified_at()`.

**Acceptance Scenarios**:

1. **Given** a verified credential response, **When** the developer calls `.email()`, **Then** they receive `Option<String>` with the normalized email value.
2. **Given** a verified credential with array claims, **When** the developer calls `.email()`, **Then** arrays are normalized to the first element.
3. **Given** a verified credential with device attestation, **When** the developer calls `.device_attestation()`, **Then** they receive a typed struct with all attestation fields.
4. **Given** a verified credential with field verification metadata, **When** the developer calls `.field_verification("email")`, **Then** they receive verification provider, method, and timestamp.

---

### User Story 5 - Claim Value Constraints (Priority: P2)

A relying party wants to constrain credentials based on claim values, such as requiring a minimum trust level, specific verification level, or pattern-matching on claim values.

**Why this priority**: Advanced filtering enables sophisticated policy enforcement beyond simple type matching.

**Independent Test**: Can be fully tested by creating policies with numeric constraints (`.with_minimum_trust_level(3)`) or pattern constraints (`.with_email_pattern(".*@(acme|corp)\\.com")`).

**Acceptance Scenarios**:

1. **Given** a TrustPolicy, **When** the developer specifies `.with_minimum_level(3)`, **Then** only trust credentials with level >= 3 are accepted.
2. **Given** a VerifiedPersonaPolicy, **When** the developer specifies `.with_verification_level(VerificationLevel::Enhanced)`, **Then** only enhanced-verified personas are accepted.
3. **Given** any policy, **When** the developer adds `.with_claim_constraint("field", value)`, **Then** that constraint is added to the presentation request.

---

### User Story 6 - Typed Credential Response Structs (Priority: P2)

An SDK customer wants credential-type-specific response structs that provide direct access to the fields defined in each credential schema (e.g., `PersonaCredential`, `EmploymentCredential`, `VerifiedEmailCredential`).

**Why this priority**: Type-specific structs eliminate the need for generic claim access and provide compile-time safety for known credential schemas.

**Independent Test**: Can be fully tested by parsing a verified response into `PersonaCredential::try_from(response)` and accessing `.display_name`, `.company`, etc. directly.

**Acceptance Scenarios**:

1. **Given** a verified Persona credential, **When** the developer calls `PersonaCredential::try_from(response)`, **Then** they get a struct with `display_name`, `email`, `phone`, `company`, `job_title`, `avatar_url`, `website`, `location`, `visibility`.
2. **Given** a verified Employment credential, **When** the developer calls `EmploymentCredential::try_from(response)`, **Then** they get a struct with `company`, `title`, `department`, `start_date`, `end_date`, `is_current`.
3. **Given** a credential type mismatch, **When** parsing fails, **Then** a descriptive error is returned.

---

### User Story 7 - Multiple Credentials in Presentation (Priority: P2)

A relying party receives a presentation containing multiple credential types (e.g., Base + Persona + VerifiedEmail). They need to access each credential separately and understand the chain relationships.

**Why this priority**: Multi-credential presentations are common for complex verification flows. Customers need structured access to the credential chain.

**Independent Test**: Can be fully tested by receiving a multi-credential response and iterating over `.credentials()` with access to each credential's type and claims.

**Acceptance Scenarios**:

1. **Given** a multi-credential presentation response, **When** the developer calls `.credentials()`, **Then** they receive an iterator over all presented credentials.
2. **Given** a chain with Base -> Persona -> VerifiedEmail, **When** the developer calls `.credential_by_type("LastID.Persona")`, **Then** they receive the Persona credential specifically.
3. **Given** a chain response, **When** the developer calls `.credential_chain()`, **Then** they receive the ordered list of credential types from root to leaf.

---

### User Story 8 - Documentation and Examples (Priority: P2)

A developer new to the SDK wants to learn how to use the new filtering capabilities and typed response parsing. They need comprehensive examples demonstrating real-world use cases and updated documentation explaining the new API surface.

**Why this priority**: Without documentation and examples, even well-designed APIs have poor adoption. Examples reduce time-to-first-success for new developers.

**Independent Test**: Can be fully tested by a new developer following the examples to successfully implement domain-filtered employment verification and typed response parsing without additional guidance.

**Acceptance Scenarios**:

1. **Given** a developer exploring the examples directory, **When** they look for filtering examples, **Then** they find dedicated examples for domain filtering, subject binding, and issuer constraints.
2. **Given** a developer reading an existing example, **When** it involves credential responses, **Then** it demonstrates typed accessors and parsed credential structs.
3. **Given** a developer browsing doc comments, **When** they view any new public method, **Then** they find usage examples and clear explanations.
4. **Given** a developer wanting to handle multi-credential chains, **When** they look for examples, **Then** they find a comprehensive example showing chain traversal and type-specific parsing.

---

### Edge Cases

- What happens when a constraint references a field not present in the credential? Return clear error indicating missing required field.
- What happens when domain constraints are applied to credentials without domain fields? Validation fails with descriptive error about constraint applicability.
- How does the system handle empty or malformed DIDs in subject/issuer constraints? Reject at policy build time with validation error.
- What happens when parsing response fails due to missing expected claims? `try_from` returns `Err` with specific missing field information.
- How does the system handle array vs scalar claim values? Normalize arrays to first element for scalar accessors; provide `.emails()` (plural) for array access.

## Requirements *(mandatory)*

### Functional Requirements

#### Policy Builder Enhancements

- **FR-001**: All policy builders MUST support `.with_issuer_did(did)` to constrain the credential issuer DID.
- **FR-002**: All policy builders MUST support `.with_subject_did(did)` to constrain the credential subject DID.
- **FR-003**: PersonaPolicy and VerifiedPersonaPolicy MUST support `.with_email_domain(domain)` to filter by email domain pattern.
- **FR-004**: EmploymentPolicy MUST support `.with_employer_did(did)` to require credentials issued by a specific employer/enterprise.
- **FR-005**: All policy builders MUST support `.with_claim_constraint(field_path, value)` for arbitrary claim filtering.
- **FR-006**: TrustPolicy MUST support `.with_minimum_level(level)` to enforce minimum trust level constraints.
- **FR-007**: AgeProofPolicy MUST support `.with_minimum_age(years)` as a convenience wrapper for age comparison.
- **FR-008**: All constraint methods MUST validate input at build time (e.g., DIDs must be valid DID format, domains must not contain '@').

#### Verified Credential Response Parsing

- **FR-009**: VerifiedCredential MUST provide typed accessors for all common claims: `email()`, `phone_number()`, `display_name()`, `given_name()`, `family_name()`, `company()`, `job_title()`.
- **FR-010**: VerifiedCredential MUST provide `verified_at()` returning the issuance timestamp.
- **FR-011**: VerifiedCredential MUST provide `device_attestation()` returning a typed `DeviceAttestation` struct when present.
- **FR-012**: VerifiedCredential MUST provide `device_security()` returning a typed `DeviceSecurity` struct when present.
- **FR-013**: VerifiedCredential MUST provide `field_verification(field_name)` returning verification metadata for specific fields.
- **FR-014**: VerifiedCredential MUST normalize array claims to scalar values for singular accessors (first element) and provide plural accessors for full arrays.

#### Type-Specific Credential Structs

- **FR-015**: SDK MUST provide `ParsedPersona` struct matching the LastID.Persona schema fields.
- **FR-016**: SDK MUST provide `ParsedEmployment` struct matching the LastID.Employment schema fields.
- **FR-017**: SDK MUST provide `ParsedVerifiedEmail` struct matching the LastID.VerifiedEmail schema fields.
- **FR-018**: SDK MUST provide `ParsedVerifiedPhone` struct matching the LastID.VerifiedPhone schema fields.
- **FR-019**: SDK MUST provide `ParsedVerifiedPersona` struct matching the LastID.VerifiedPersona schema fields.
- **FR-020**: SDK MUST provide `ParsedAgeProof` struct matching the LastID.AgeProof schema fields.
- **FR-021**: All parsed structs MUST implement `TryFrom<&VerifiedCredential>` with descriptive error messages.

#### Multi-Credential Response Handling

- **FR-022**: VerifiedResponse MUST provide `.credentials()` iterator for accessing all presented credentials.
- **FR-023**: VerifiedResponse MUST provide `.credential_by_type(type_str)` for type-based lookup.
- **FR-024**: VerifiedResponse MUST provide `.credential_chain()` returning credential types in chain order (root to leaf).
- **FR-025**: VerifiedResponse MUST provide `.primary_credential()` returning the leaf/target credential of the request.

#### Documentation and Examples

- **FR-026**: SDK MUST include a new example `enterprise_domain_filter.rs` demonstrating employment verification with domain and issuer DID constraints.
- **FR-027**: SDK MUST include a new example `subject_binding.rs` demonstrating re-authentication flows with subject DID matching.
- **FR-028**: SDK MUST include a new example `typed_response_parsing.rs` demonstrating typed accessors, `ParsedPersona`, `ParsedEmployment`, and device attestation access.
- **FR-029**: SDK MUST include a new example `multi_credential_chain.rs` demonstrating credential chain traversal, type-specific lookup, and parsing multiple credentials from a single presentation.
- **FR-030**: SDK MUST update existing examples (`employment_verification.rs`, `persona_credential.rs`, `verified_email_credential.rs`) to demonstrate typed response parsing where applicable.
- **FR-031**: All new public methods MUST include doc comments with at least one usage example in the `# Example` section.
- **FR-032**: All new public structs (`DeviceAttestation`, `DeviceSecurity`, `FieldVerificationMetadata`, `ParsedPersona`, etc.) MUST include module-level documentation explaining their purpose and relationship to credential verification.
- **FR-033**: SDK MUST update the crate-level documentation (`lib.rs`) to reference the new filtering and parsing capabilities with links to relevant examples.

### Key Entities

- **CredentialConstraint**: A filter condition applied to a credential field (issuer DID, subject DID, claim value, pattern match).
- **ParsedCredential**: Base trait for all type-specific credential structs with common methods.
- **DeviceAttestation**: Structured representation of device security attestation data (secure element, TEE, integrity status, biometric, etc.).
- **DeviceSecurity**: Structured representation of device security context (security level, biometric used, platform).
- **FieldVerificationMetadata**: Verification details for a specific claim (provider, method, timestamp).
- **VerifiedResponse**: Enhanced response type containing credential chain, device attestation, and parsed claims.

## Success Criteria *(mandatory)*

### Measurable Outcomes

- **SC-001**: Developers can create a domain-constrained employment policy in 3 method calls or fewer.
- **SC-002**: Developers can access the email from a verified credential in a single method call without JSON parsing.
- **SC-003**: 100% of LastID credential types have corresponding typed parsing structs.
- **SC-004**: All constraint methods fail fast at build time (not verification time) for invalid inputs.
- **SC-005**: Multi-credential responses provide direct access to any credential in the chain by type.
- **SC-006**: Device attestation data is accessible via typed struct without any manual JSON traversal.
- **SC-007**: Existing SDK tests continue to pass without modification (backward compatibility maintained).
- **SC-008**: New filtering methods follow the existing fluent API pattern (`.with_*` naming, type-state pattern, `#[must_use]`).
- **SC-009**: 100% of new public methods have doc comments with usage examples.
- **SC-010**: All new examples compile and run successfully with `cargo run --example <name>`.
- **SC-011**: A developer new to the SDK can implement domain-filtered employment verification by following the examples within 15 minutes.

## Assumptions

- Domain constraints use suffix matching (e.g., "acme.com" matches "user@acme.com" and "user@sales.acme.com").
- Issuer DID constraints use exact string matching.
- Subject DID constraints use exact string matching.
- The IDP already supports these constraints via the `constraints` field in `presentation_definition`.
- Array normalization (taking first element) is acceptable for scalar accessors; developers needing all values use plural accessors.
