# Data Model: Credential Filtering API & Parsed Output

**Branch**: `003-credential-filtering-api` | **Date**: 2026-01-21

## Entity Definitions

### 1. CredentialConstraint (New)

A filter condition that can be applied to any credential field during presentation request.

```rust
/// Represents a constraint on a credential field.
/// Used internally by policy builders to construct presentation_definition constraints.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum CredentialConstraint {
    /// Exact value match (JSON-LD const)
    Equals { field: String, value: serde_json::Value },

    /// One of allowed values (JSON-LD enum)
    OneOf { field: String, values: Vec<serde_json::Value> },

    /// Numeric minimum (JSON-LD minimum)
    Minimum { field: String, value: i64 },

    /// Numeric maximum (JSON-LD maximum)
    Maximum { field: String, value: i64 },

    /// Regex pattern match (JSON-LD pattern)
    Pattern { field: String, pattern: String },

    /// Domain suffix match for email fields
    /// Translates to pattern: `.*@{domain}$`
    EmailDomain { field: String, domain: String },
}
```

**Validation Rules**:
- `field` must be non-empty
- `domain` must not contain `@` or `/` characters
- `pattern` must be valid regex

### 2. DeviceAttestation (New)

Structured representation of device security attestation from the credential presentation.

```rust
/// Device attestation data from KB-JWT.
/// Provides hardware-level security information about the presenting device.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub struct DeviceAttestation {
    /// Device has a secure element (e.g., Apple Secure Enclave, Titan M)
    pub has_secure_element: bool,

    /// Device has a Trusted Execution Environment
    pub has_tee: bool,

    /// Device integrity verification result: "pass", "fail", "unknown"
    pub integrity_status: String,

    /// Specific integrity indicators (platform-specific)
    #[serde(default)]
    pub integrity_indicators: Vec<String>,

    /// Device is running in an emulator
    pub is_emulator: bool,

    /// App is being debugged
    pub is_debugged: bool,

    /// Bootloader is locked (Android)
    pub bootloader_locked: bool,

    /// Device storage is encrypted
    pub device_encrypted: bool,

    /// Hardware-derived fingerprint (if available)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub hardware_fingerprint: Option<String>,

    /// iOS App Attest is supported
    pub app_attest_supported: bool,

    /// iOS App Attest key ID
    #[serde(skip_serializing_if = "Option::is_none")]
    pub app_attest_key_id: Option<String>,

    /// Android key attestation security level
    #[serde(skip_serializing_if = "Option::is_none")]
    pub key_attestation_security_level: Option<String>,

    /// Device model
    #[serde(skip_serializing_if = "Option::is_none")]
    pub model: Option<String>,

    /// Operating system version
    #[serde(skip_serializing_if = "Option::is_none")]
    pub os_version: Option<String>,

    /// Timestamp when attestation was collected
    #[serde(skip_serializing_if = "Option::is_none")]
    pub collected_at: Option<i64>,
}
```

**Derive Traits**: `Debug`, `Clone`, `Serialize`, `Deserialize`, `Default`, `PartialEq`

### 3. DeviceSecurity (New)

High-level security context about the presenting device.

```rust
/// Device security context from KB-JWT.
/// Provides high-level security assessment for policy decisions.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub struct DeviceSecurity {
    /// Overall security level: "high", "medium", "low"
    pub security_level: String,

    /// Whether biometric authentication was used for this presentation
    pub biometric_used: bool,

    /// Platform identifier: "ios", "android", "web"
    pub platform: String,
}
```

**Derive Traits**: `Debug`, `Clone`, `Serialize`, `Deserialize`, `Default`, `PartialEq`

### 4. FieldVerificationMetadata (New)

Verification details for a specific claim/field.

```rust
/// Verification metadata for a specific credential field.
/// Describes how and when a field was verified.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct FieldVerificationMetadata {
    /// Whether the field value is verified
    pub verified: bool,

    /// Verification provider (e.g., "google", "twilio", "lastid")
    #[serde(skip_serializing_if = "Option::is_none")]
    pub provider: Option<String>,

    /// Verification method (e.g., "oauth", "sms_otp", "email_link")
    #[serde(skip_serializing_if = "Option::is_none")]
    pub method: Option<String>,

    /// Timestamp when verification occurred (Unix epoch seconds)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub verified_at: Option<i64>,
}
```

**Derive Traits**: `Debug`, `Clone`, `Serialize`, `Deserialize`, `PartialEq`

### 5. VerifiedResponse (New)

Enhanced response type for multi-credential presentations.

```rust
/// Response from credential verification containing one or more credentials.
/// Provides structured access to credential chains and device attestation.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VerifiedResponse {
    /// All verified credentials from the presentation
    pub credentials: Vec<VerifiedCredential>,

    /// Credential types in chain order (root to leaf)
    pub credential_chain: Vec<String>,

    /// Device attestation data (if present in KB-JWT)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub device_attestation: Option<DeviceAttestation>,

    /// Device security context (if present in KB-JWT)
    #[serde(skip_serializing_if = "Option::is_none")]
    pub device_security: Option<DeviceSecurity>,

    /// Field-level verification metadata
    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
    pub field_verification: HashMap<String, FieldVerificationMetadata>,
}
```

**Relationships**:
- Contains 1..n `VerifiedCredential`
- Contains 0..1 `DeviceAttestation`
- Contains 0..1 `DeviceSecurity`
- Contains 0..n `FieldVerificationMetadata` (keyed by field name)

### 6. Parsed Credential Structs (New)

Type-specific structs for each credential type, implementing `TryFrom<&VerifiedCredential>`.

#### ParsedPersona

```rust
/// Parsed LastID.Persona credential with typed fields.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ParsedPersona {
    pub display_name: Option<String>,
    pub email: Option<String>,
    pub phone: Option<String>,
    pub company: Option<String>,
    pub job_title: Option<String>,
    pub avatar_url: Option<String>,
    pub website: Option<String>,
    pub location: Option<String>,
    pub visibility: Option<String>,
}
```

#### ParsedEmployment

```rust
/// Parsed LastID.Employment credential with typed fields.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ParsedEmployment {
    pub company: Option<String>,
    pub title: Option<String>,
    pub department: Option<String>,
    pub start_date: Option<String>,
    pub end_date: Option<String>,
    pub is_current: Option<bool>,
    pub responsibilities: Option<String>,
}
```

#### ParsedVerifiedEmail

```rust
/// Parsed LastID.VerifiedEmail credential with typed fields.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ParsedVerifiedEmail {
    pub email: String, // Required - verification wouldn't exist without it
    pub verified_at: Option<i64>,
    pub verification_method: Option<String>,
    pub provider: Option<String>,
}
```

#### ParsedVerifiedPhone

```rust
/// Parsed LastID.VerifiedPhone credential with typed fields.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ParsedVerifiedPhone {
    pub phone_number: String, // Required
    pub verified_at: Option<i64>,
    pub verification_method: Option<String>,
    pub provider: Option<String>,
}
```

#### ParsedVerifiedPersona

```rust
/// Parsed LastID.VerifiedPersona credential with typed fields.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ParsedVerifiedPersona {
    pub display_name: Option<String>,
    pub given_name: Option<String>,
    pub family_name: Option<String>,
    pub email: Option<String>,
    pub phone: Option<String>,
    pub verification_level: Option<String>,
    pub verified_at: Option<i64>,
}
```

#### ParsedAgeProof

```rust
/// Parsed LastID.AgeProof credential with typed fields.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ParsedAgeProof {
    pub age: Option<i64>,
    pub age_verified: bool,
    pub verification_date: Option<String>,
    pub minimum_age_met: Option<i64>, // The minimum age threshold that was verified
}
```

## Entity Relationships

```
┌─────────────────────────────────────────────────────────────────┐
│                        VerifiedResponse                          │
│  ┌─────────────────┐  ┌─────────────────┐  ┌─────────────────┐ │
│  │  credentials[]  │  │device_attestation│  │ device_security │ │
│  │ VerifiedCred[]  │  │DeviceAttestation │  │  DeviceSecurity │ │
│  └────────┬────────┘  └─────────────────┘  └─────────────────┘ │
│           │                                                      │
│  ┌────────▼────────────────────────────────────────────────┐    │
│  │               field_verification: HashMap               │    │
│  │  "email" → FieldVerificationMetadata                    │    │
│  │  "phone" → FieldVerificationMetadata                    │    │
│  └─────────────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│                      VerifiedCredential                          │
│  (existing, enhanced with typed accessors)                       │
│                                                                  │
│  .email()          → Option<&str>                               │
│  .emails()         → Vec<&str>                                  │
│  .phone_number()   → Option<&str>                               │
│  .display_name()   → Option<&str>                               │
│  .company()        → Option<&str>                               │
│  .job_title()      → Option<&str>                               │
│  .verified_at()    → Option<DateTime<Utc>>                      │
│                                                                  │
│  TryFrom conversions to:                                        │
│  ├── ParsedPersona                                              │
│  ├── ParsedEmployment                                           │
│  ├── ParsedVerifiedEmail                                        │
│  ├── ParsedVerifiedPhone                                        │
│  ├── ParsedVerifiedPersona                                      │
│  └── ParsedAgeProof                                             │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│                Policy Builders (Enhanced)                        │
│                                                                  │
│  All policies gain:                                             │
│  .with_issuer_did(did)                                          │
│  .with_subject_did(did)                                         │
│  .with_claim_constraint(field, value)                           │
│                                                                  │
│  PersonaPolicy / VerifiedPersonaPolicy:                         │
│  .with_email_domain(domain)                                     │
│                                                                  │
│  EmploymentPolicy:                                              │
│  .with_employer_did(did)                                        │
│                                                                  │
│  TrustPolicy:                                                   │
│  .with_minimum_level(level)                                     │
│                                                                  │
│  AgeProofPolicy:                                                │
│  .with_minimum_age(years)                                       │
└─────────────────────────────────────────────────────────────────┘
```

## State Transitions

### Policy Builder State Machine

```
┌──────────────┐     .with_*()      ┌──────────────┐
│  Incomplete  │ ──────────────────► │  Incomplete  │
│    (new)     │ ◄────────────────── │ (with data)  │
└──────────────┘                     └──────┬───────┘
                                            │
                                     .with_callback()
                                            │
                                            ▼
                                     ┌──────────────┐
                                     │   Complete   │
                                     │ (buildable)  │
                                     └──────┬───────┘
                                            │
                                      .to_request()
                                            │
                                            ▼
                                 ┌────────────────────┐
                                 │CredentialPolicy   │
                                 │    Request        │
                                 └────────────────────┘
```

All new constraint methods (`.with_issuer_did()`, `.with_subject_did()`, etc.) operate on the `Incomplete` state and return `Self`, allowing them to be chained in any order before `.with_callback()`.

## Validation Rules

### Input Validation (Build-Time)

| Method | Validation | Error |
|--------|------------|-------|
| `.with_issuer_did(did)` | Must start with `did:` | `PolicyValidation: Invalid DID format` |
| `.with_subject_did(did)` | Must start with `did:` | `PolicyValidation: Invalid DID format` |
| `.with_email_domain(domain)` | No `@`, `/`, empty | `PolicyValidation: Invalid domain format` |
| `.with_employer_did(did)` | Must start with `did:` | `PolicyValidation: Invalid DID format` |
| `.with_minimum_level(n)` | `n >= 0` | `PolicyValidation: Level must be non-negative` |
| `.with_minimum_age(years)` | `years > 0 && years <= 150` | `PolicyValidation: Age must be 1-150` |

### TryFrom Validation (Parse-Time)

| Struct | Validation | Error |
|--------|------------|-------|
| All parsed structs | `credential_type` must match expected | `CredentialParse: Expected {type}, got {actual}` |
| `ParsedVerifiedEmail` | `email` field must exist | `CredentialParse: Missing required field: email` |
| `ParsedVerifiedPhone` | `phone_number` field must exist | `CredentialParse: Missing required field: phone_number` |
