# LastID Rust SDK
[](https://crates.io/crates/lastid-sdk)
[](https://docs.rs/lastid-sdk)
[](https://github.com/GetTrustedApp/lastid-rust-sdk/actions/workflows/ci.yml)
[](https://blog.rust-lang.org/2025/10/09/Rust-1.89.0.html)
[](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
| `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.