1 unstable release
Uses new Rust 2024
| 0.3.0 | Mar 27, 2026 |
|---|
#750 in Authentication
2MB
17K
SLoC
Contains (Mach-o exe, 4MB) lastid_sdk.node
LastID Rust SDK
Rust SDK for integrating with the LastID Identity Provider (IDP) 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)
[dependencies]
lastid-sdk = "0.1.0"
tokio = { version = "1", features = ["full"] }
JavaScript / TypeScript (WASM)
npm install @lastid/sdk
Or build from source:
# 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)
[dependencies]
lastid-sdk = { version = "0.1.0", default-features = false, features = ["wasm"] }
wasm-bindgen-futures = "0.4"
Quick Start (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)
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:
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:
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:
export LASTID_ENDPOINT="https://human.lastid.co"
export LASTID_POLLING_TIMEOUT="300"
Precedence: Env vars > Explicit config > Discovered TOML > Defaults
Documentation
- API Reference
- Configuration Example - Complete configuration reference
Feature Flags
# 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:
[dependencies]
lastid-sdk = { version = "0.1.0", features = ["keypair-serialization"] }
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
# 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 for vulnerability reporting.
License
Licensed under either of:
- Apache License, Version 2.0 (LICENSE-APACHE)
- MIT License (LICENSE-MIT)
at your option.
Contributing
See CONTRIBUTING.md for guidelines.
Dependencies
~7–27MB
~343K SLoC