# LastID Rust SDK

[![Crates.io](https://img.shields.io/crates/v/lastid-sdk.svg)](https://crates.io/crates/lastid-sdk)
[![Documentation](https://docs.rs/lastid-sdk/badge.svg)](https://docs.rs/lastid-sdk)
[![CI](https://github.com/GetTrustedApp/lastid-rust-sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/GetTrustedApp/lastid-rust-sdk/actions/workflows/ci.yml)
[![MSRV](https://img.shields.io/badge/MSRV-1.89-blue.svg)](https://blog.rust-lang.org/2025/10/09/Rust-1.89.0.html)
[![License](https://img.shields.io/badge/license-MIT%2FApache--2.0-blue.svg)](LICENSE-MIT)

Rust SDK for integrating with the [LastID Identity Provider (IDP)](https://lastid.co) to request and verify credentials from users.

**Dual-target**: Works on both native (tokio) and browser (WASM) from a single crate.

## Features

- **Type-safe policy builders** with compile-time validation
- **8 credential types**: Base, Persona, VerifiedEmail, VerifiedPhone, VerifiedPersona, Employment, Trust, AgeProof
- **DPoP authentication** (RFC 9449) for proof-of-possession
- **Trust registry validation** with 60-second caching
- **Dual-target support**: Native (tokio) + WASM (browser)
- **Structured errors** with error codes for programmatic handling
- **TypeScript definitions** for excellent IDE support

## Installation

### Rust (Native)

```toml
[dependencies]
lastid-sdk = "0.1.0"
tokio = { version = "1", features = ["full"] }
```

### JavaScript / TypeScript (WASM)

```bash
npm install @lastid/sdk
```

Or build from source:

```bash
# Install wasm-pack if you haven't
cargo install wasm-pack

# Build WASM package with TypeScript bindings
wasm-pack build --target web --features wasm

# Output in pkg/ directory:
# - lastid_sdk.js       (JavaScript bindings)
# - lastid_sdk.d.ts     (TypeScript definitions - auto-generated)
# - lastid_sdk_bg.wasm  (WASM binary)
```

### Rust (WASM target)

```toml
[dependencies]
lastid-sdk = { version = "0.1.0", default-features = false, features = ["wasm"] }
wasm-bindgen-futures = "0.4"
```

## Quick Start (Rust)

```rust
use lastid_sdk::{ClientBuilder, BaseCredentialPolicy, RequestStatus};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Initialize client
    let client = ClientBuilder::new()
        .with_auto_config()?
        .build()?;

    // Build policy (type-safe)
    let policy = BaseCredentialPolicy::new()
        .with_callback("https://your-app.com/callback");

    // Request credential - returns full response with request_uri for QR codes
    let response = client.request_credential(policy).await?;
    println!("Request ID: {}", response.request_id);
    println!("QR Code URI: {}", response.request_uri());

    // Poll for result
    loop {
        match client.poll_request(&response.request_id).await? {
            RequestStatus::Pending { .. } => {
                tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
            }
            RequestStatus::Fulfilled { presentation, .. } => {
                let verified = client.verify_presentation(&presentation).await?;
                println!("Subject DID: {}", verified.subject_did);
                break;
            }
            _ => break,
        }
    }

    Ok(())
}
```

## Quick Start (JavaScript / TypeScript)

```typescript
import init, {
  WasmConfig,
  WasmClient,
  BaseCredentialPolicy,
  generateState,
  generateNonce
} from '@lastid/sdk';

// Initialize WASM module
await init();

// Create configuration
const config = new WasmConfig(
  'https://human.lastid.co',  // IDP endpoint
  'your-client-id'            // OAuth client ID
);

// Create client
const client = new WasmClient(config);

// Build policy with fluent API
const policy = new BaseCredentialPolicy()
  .withState(generateState())
  .withNonce(generateNonce())
  .withCallback(window.location.href);

try {
  // Request credential
  const response = await client.requestCredential(policy);
  console.log('Request ID:', response.requestId);
  console.log('QR Code URI:', response.requestUri);

  // Subscribe for real-time updates (WebSocket with polling fallback)
  const status = await client.subscribeForCompletion(response.requestId);

  if (status.status === 'fulfilled') {
    // Verify the presentation
    const credential = await client.verifyPresentation(status.presentation!);
    console.log('Subject DID:', credential.subjectDid);
    console.log('Claims:', credential.claims);
  }
} catch (error) {
  // Structured error handling
  if (error.code === 'NETWORK_ERROR' && error.isRetryable) {
    console.log(`Retry in ${error.suggestedRetryMs}ms`);
  } else if (error.code === 'POLICY_ERROR') {
    console.error('Policy validation failed:', error.details);
  }
}
```

### Error Handling (TypeScript)

The SDK provides structured errors with codes for programmatic handling:

```typescript
import type { LastIDError, ErrorCode } from '@lastid/sdk';

try {
  await client.requestCredential(policy);
} catch (error) {
  const e = error as LastIDError;

  switch (e.code) {
    case 'CONFIG_ERROR':
      // Invalid configuration
      break;
    case 'NETWORK_ERROR':
      // Connection failed - check e.isRetryable
      if (e.isRetryable) {
        await delay(e.suggestedRetryMs ?? 1000);
        // retry...
      }
      break;
    case 'POLICY_ERROR':
      // Invalid policy - check e.details
      console.error(e.details);
      break;
    case 'RATE_LIMIT_ERROR':
      // Too many requests - wait suggestedRetryMs
      break;
    case 'VERIFICATION_ERROR':
      // Credential verification failed
      break;
    case 'TRUST_ERROR':
      // Issuer not in trust registry
      break;
    default:
      console.error('Unexpected error:', e.message);
  }
}
```

### Available Error Codes

| Code | Description | Retryable |
|------|-------------|-----------|
| `CONFIG_ERROR` | Invalid endpoint, missing client_id | No |
| `NETWORK_ERROR` | Connection failed, timeout, DNS | Yes (1s) |
| `AUTH_ERROR` | Invalid credentials, token expired | No |
| `POLICY_ERROR` | Missing fields, invalid constraints | No |
| `VERIFICATION_ERROR` | Signature invalid, expired, revoked | No |
| `TRUST_ERROR` | Issuer not found, suspended | No |
| `NOT_FOUND_ERROR` | Request not found (404) | No |
| `EXPIRED_ERROR` | Credential request timed out | No |
| `DENIED_ERROR` | User denied the request | No |
| `RATE_LIMIT_ERROR` | Too many requests | Yes (30s) |
| `INTERNAL_ERROR` | Unexpected SDK error | No |

## Configuration

Create `lastid.toml` in your project root:

```toml
idp_endpoint = "https://human.lastid.co"

[retry]
max_attempts = 3
initial_delay_ms = 1000

[polling]
initial_interval_ms = 2000
timeout_seconds = 300

[cache]
enabled = true
ttl_seconds = 60
```

Or use environment variables:

```bash
export LASTID_ENDPOINT="https://human.lastid.co"
export LASTID_POLLING_TIMEOUT="300"
```

**Precedence**: Env vars > Explicit config > Discovered TOML > Defaults

## Documentation

- [API Reference](https://docs.rs/lastid-sdk)
- [Configuration Example](lastid.toml.example) - Complete configuration reference

## Feature Flags

```toml
# Default features
default = ["base-policy"]

# All credential policies
full = ["base-policy", "persona-policy", "verified-email-policy",
        "verified-phone-policy", "verified-persona-policy",
        "employment-policy", "trust-policy", "age-proof-policy", "tracing"]

# WASM support (browser)
wasm = ["wasm-bindgen", "wasm-bindgen-futures", "web-sys", "js-sys"]

# WebSocket support for real-time status updates
websocket = ["futures-util", "tokio-tungstenite"]

# Enable DPoP keypair serialization for serverless persistence (SEC-002)
# WARNING: Serialized keypairs contain private keys - ensure encrypted storage
keypair-serialization = []
```

### Serverless Keypair Persistence

For serverless deployments where you need to persist the DPoP keypair across invocations, enable the `keypair-serialization` feature:

```toml
[dependencies]
lastid-sdk = { version = "0.1.0", features = ["keypair-serialization"] }
```

```rust
use lastid_sdk::crypto::DPoPKeyPair;

// Generate and serialize (first invocation)
let keypair = DPoPKeyPair::generate()?;
let serialized = serde_json::to_string(&keypair)?;
// Store `serialized` in your secrets manager (AWS KMS, Vault, etc.)

// Deserialize (subsequent invocations)
let restored: DPoPKeyPair = serde_json::from_str(&serialized)?;
```

**Security Warning**: The serialized keypair contains the private key. Always store encrypted (e.g., AWS Secrets Manager, HashiCorp Vault).

## Development

```bash
# Run tests (native)
cargo test --all-features

# Run tests (WASM) - requires wasm-pack
wasm-pack test --headless --chrome --features wasm

# Lint
cargo clippy --all-features -- -D warnings

# Format
cargo fmt --check

# Security audit
cargo deny check
```

## Security

See [SECURITY.md](SECURITY.md) for vulnerability reporting.

## License

Licensed under either of:

- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE))
- MIT License ([LICENSE-MIT](LICENSE-MIT))

at your option.

## Contributing

See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
