lastid-sdk 0.3.0

Rust SDK for LastID IDP integration - request and verify credentials with type-safe policy builders
Documentation

LastID Rust SDK

Crates.io Documentation CI MSRV License

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

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:

at your option.

Contributing

See CONTRIBUTING.md for guidelines.