# Data Model: SDK Review Hardening

**Branch**: `002-sdk-review-hardening` | **Date**: 2026-01-17

## Overview

This document defines the new and modified data structures required to address the hardening requirements from the multi-perspective review.

---

## New Newtypes (FR-004)

### Did

Decentralized Identifier with validated format.

```rust
/// A validated Decentralized Identifier (DID).
///
/// DIDs follow the `did:method:identifier` format.
/// This newtype ensures validation on construction.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(try_from = "String", into = "String")]
pub struct Did(String);

impl Did {
    /// Creates a new DID after validating the format.
    ///
    /// # Errors
    /// Returns error if the string doesn't match `did:*:*` pattern.
    pub fn new(value: impl Into<String>) -> Result<Self, LastIDError>;

    /// Returns the DID as a string slice.
    pub fn as_str(&self) -> &str;

    /// Returns the DID method (e.g., "lastid" from "did:lastid:abc").
    pub fn method(&self) -> &str;
}
```

**Validation Rules**:
- Must start with `did:`
- Must have at least two colons (method separator + identifier)
- Method must be non-empty alphanumeric

---

### ClientId

OAuth client identifier with non-empty validation.

```rust
/// A validated OAuth client identifier.
///
/// Client IDs must be non-empty strings.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(try_from = "String", into = "String")]
pub struct ClientId(String);

impl ClientId {
    /// Creates a new ClientId after validating it's non-empty.
    ///
    /// # Errors
    /// Returns error if the string is empty or whitespace-only.
    pub fn new(value: impl Into<String>) -> Result<Self, LastIDError>;

    pub fn as_str(&self) -> &str;
}
```

**Validation Rules**:
- Must not be empty
- Must not be whitespace-only

---

### EndpointUrl

IDP endpoint URL with HTTPS enforcement.

```rust
/// A validated endpoint URL for the Identity Provider.
///
/// URLs must use HTTPS in production. HTTP is only allowed for localhost.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(try_from = "String", into = "String")]
pub struct EndpointUrl(String);

impl EndpointUrl {
    /// Creates a new endpoint URL with HTTPS validation.
    ///
    /// # Errors
    /// Returns error if URL doesn't use HTTPS (except localhost for dev).
    pub fn new(value: impl Into<String>) -> Result<Self, LastIDError>;

    pub fn as_str(&self) -> &str;

    /// Returns true if this is a localhost URL (dev mode).
    pub fn is_localhost(&self) -> bool;
}
```

**Validation Rules**:
- Must start with `https://` OR
- Must start with `http://localhost` or `http://127.0.0.1` (dev only)
- No other `http://` URLs allowed

---

## Modified Structures

### SDKConfig (Updated)

```rust
pub struct SDKConfig {
    // Changed from String to EndpointUrl
    pub idp_endpoint: EndpointUrl,

    // Changed from String to ClientId
    pub client_id: ClientId,

    // Changed from Option<String> to Option<EndpointUrl>
    pub trust_registry_endpoint: Option<EndpointUrl>,

    // Existing fields unchanged
    pub timeout_seconds: u64,
    pub cache: CacheConfig,
    pub polling: PollingConfig,
    pub retry: RetryPolicy,
    pub websocket: WebSocketConfig,

    // NEW: Network configuration
    pub network: NetworkConfig,

    // NEW: Clock skew tolerance
    pub clock_skew_seconds: u64,
}
```

---

### NetworkConfig (New)

```rust
/// Network configuration for enterprise environments.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NetworkConfig {
    /// HTTP/HTTPS proxy URL (e.g., "http://proxy.corp:8080")
    pub proxy_url: Option<String>,

    /// Proxy authentication (basic or bearer)
    pub proxy_auth: Option<ProxyAuth>,

    /// Hosts to bypass proxy (e.g., ["localhost", "*.internal.corp"])
    pub no_proxy: Vec<String>,

    /// TCP connection timeout in seconds (default: 10)
    pub connect_timeout_seconds: u64,

    /// Response read timeout in seconds (default: 30)
    pub read_timeout_seconds: u64,

    /// Connection pool idle timeout in seconds (default: 30)
    pub pool_idle_timeout_seconds: u64,

    /// Max idle connections per host (default: 5)
    pub pool_max_idle_per_host: usize,

    /// Correlation ID header name (default: "X-Request-ID")
    pub correlation_id_header: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ProxyAuth {
    Basic { username: String, password: SecretString },
    Bearer { token: SecretString },
}
```

**Defaults**:
```rust
impl Default for NetworkConfig {
    fn default() -> Self {
        Self {
            proxy_url: None,
            proxy_auth: None,
            no_proxy: vec![],
            connect_timeout_seconds: 10,
            read_timeout_seconds: 30,
            pool_idle_timeout_seconds: 30,
            pool_max_idle_per_host: 5,
            correlation_id_header: "X-Request-ID".to_string(),
        }
    }
}
```

---

### IssuerInfo (Updated)

```rust
pub struct IssuerInfo {
    // Changed from String to Did
    pub enterprise_did: Did,

    pub organization_name: String,

    // Changed from String to EndpointUrl
    pub endpoint_url: EndpointUrl,

    pub public_key: ES256PublicKey,
    pub status: IssuerStatus,
    pub permitted_types: Vec<String>,
}
```

---

### VerifiedCredential (Updated)

```rust
pub struct VerifiedCredential {
    // Changed from String to Did
    pub subject_did: Did,

    // Changed from String to Did
    pub issuer_did: Did,

    pub credential_type: String,
    pub issued_at: DateTime<Utc>,
    pub expires_at: DateTime<Utc>,
    pub claims: serde_json::Value,
}
```

---

## New Error Variants

### LastIDError Additions

```rust
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]  // NEW: SemVer safety
pub enum LastIDError {
    // Existing variants...

    /// Invalid DID format
    #[error("Invalid DID format: {0}")]
    InvalidDid(String),

    /// Invalid client ID (empty)
    #[error("Client ID cannot be empty")]
    EmptyClientId,

    /// HTTP URL not allowed in production
    #[error("HTTP URLs are not allowed; use HTTPS (except localhost for dev): {0}")]
    InsecureUrl(String),

    /// Clock skew exceeded tolerance
    #[error("Clock skew exceeded {tolerance_seconds}s tolerance: server={server_time}, local={local_time}")]
    ClockSkewExceeded {
        tolerance_seconds: u64,
        server_time: DateTime<Utc>,
        local_time: DateTime<Utc>,
    },

    /// DPoP token binding mismatch
    #[error("Token binding mismatch: expected thumbprint {expected}, got {actual}")]
    TokenBindingMismatch {
        expected: String,
        actual: String,
    },
}
```

---

## Updated Enums with `#[non_exhaustive]`

All public enums must add this attribute:

```rust
#[non_exhaustive]
pub enum RequestStatus { ... }

#[non_exhaustive]
pub enum IssuerStatus { ... }

#[non_exhaustive]
pub enum HttpError { ... }

#[non_exhaustive]
pub enum TrustRegistryError { ... }

#[non_exhaustive]
pub enum WebSocketError { ... }

#[non_exhaustive]
pub enum TrustLevel { ... }

#[non_exhaustive]
pub enum PersonaField { ... }

#[non_exhaustive]
pub enum EmploymentStatus { ... }

#[non_exhaustive]
pub enum AgeComparison { ... }

#[non_exhaustive]
pub enum VerificationLevel { ... }
```

---

## Named Constants (FR-014)

```rust
// src/constants.rs (new file)

/// Default HTTP request timeout in seconds
pub const DEFAULT_TIMEOUT_SECONDS: u64 = 30;

/// Default connection timeout in seconds
pub const DEFAULT_CONNECT_TIMEOUT_SECONDS: u64 = 10;

/// Default read timeout in seconds
pub const DEFAULT_READ_TIMEOUT_SECONDS: u64 = 30;

/// Default retry-after when server doesn't specify
pub const DEFAULT_RETRY_AFTER_SECONDS: u64 = 60;

/// Default cache TTL in seconds
pub const DEFAULT_CACHE_TTL_SECONDS: u64 = 60;

/// Default polling max duration in seconds
pub const DEFAULT_POLLING_MAX_DURATION_SECONDS: u64 = 300;

/// Default clock skew tolerance in seconds
pub const DEFAULT_CLOCK_SKEW_SECONDS: u64 = 60;

/// DPoP proof maximum lifetime in seconds
pub const DPOP_PROOF_MAX_LIFETIME_SECONDS: u64 = 300;

/// Default connection pool idle timeout in seconds
pub const DEFAULT_POOL_IDLE_TIMEOUT_SECONDS: u64 = 30;

/// Default max idle connections per host
pub const DEFAULT_POOL_MAX_IDLE_PER_HOST: usize = 5;

/// WebSocket connect timeout in seconds
pub const DEFAULT_WEBSOCKET_CONNECT_TIMEOUT_SECONDS: u64 = 10;

/// WebSocket receive timeout in seconds
pub const DEFAULT_WEBSOCKET_RECEIVE_TIMEOUT_SECONDS: u64 = 300;
```

---

## State Transitions

### RequestStatus (Updated for FR-010)

Timeout is no longer a valid status - it becomes an error:

```rust
#[non_exhaustive]
pub enum RequestStatus {
    /// Request created, waiting for user action
    Pending { request_id: RequestId },

    /// User completed action, verification in progress
    Processing { request_id: RequestId },

    /// Verification complete, credential ready
    Completed { request_id: RequestId },

    /// User rejected or cancelled
    Rejected { request_id: RequestId, reason: Option<String> },

    /// Request expired (different from timeout)
    Expired { request_id: RequestId },

    // REMOVED: Timeout - now returns Err(LastIDError::Timeout(elapsed))
}
```

---

## Entity Relationships

```
┌─────────────┐     validates      ┌─────────────────┐
│ EndpointUrl │◄──────────────────│   SDKConfig     │
└─────────────┘                    └────────┬────────┘
                                            │
┌─────────────┐     identifies     ┌────────▼────────┐
│  ClientId   │◄──────────────────│  TokenManager   │
└─────────────┘                    └────────┬────────┘
                                            │
┌─────────────┐     signs          ┌────────▼────────┐
│    Did      │◄──────────────────│ DPoPKeyPair     │
└─────────────┘                    └─────────────────┘
        │
        │ identifies
        ▼
┌─────────────────┐
│VerifiedCredential│
└─────────────────┘
```
