# BSV Auth Transports Module
> Transport layer implementations for BRC-31 authentication messages
## Overview
This module provides transport layer implementations for sending and receiving authentication messages in the BRC-31 (Authrite) protocol. It defines the `Transport` trait for pluggable transports and includes three implementations: HTTP-based (`SimplifiedFetchTransport`), WebSocket-based (`WebSocketTransport`), and mock (`MockTransport`). The HTTP transport implements BRC-104 for authenticated HTTP communication.
## Files
| `mod.rs` | Module root with exports and usage examples | ~49 |
| `http.rs` | Transport trait, HTTP transport, mock transport, BRC-104 payload types | ~1229 |
| `websocket_transport.rs` | WebSocket transport (requires `websocket` feature) | ~599 |
## Key Exports
### Transport Trait
The core abstraction for message transport:
```rust
#[async_trait]
pub trait Transport: Send + Sync {
/// Sends an authentication message.
async fn send(&self, message: &AuthMessage) -> Result<()>;
/// Registers a callback for incoming messages.
fn set_callback(&self, callback: Box<TransportCallback>);
/// Clears the registered callback.
fn clear_callback(&self);
}
```
### TransportCallback
Type alias for async callback functions:
```rust
pub type TransportCallback =
dyn Fn(AuthMessage) -> Pin<Box<dyn Future<Output = Result<()>> + Send>>
+ Send
+ Sync;
```
### SimplifiedFetchTransport
HTTP-based transport for production use:
```rust
pub struct SimplifiedFetchTransport {
base_url: String,
#[cfg(feature = "http")]
client: reqwest::Client,
callback: Arc<StdRwLock<Option<Box<TransportCallback>>>>,
}
impl SimplifiedFetchTransport {
pub fn new(base_url: &str) -> Self
pub fn base_url(&self) -> &str
pub fn message_to_headers(&self, message: &AuthMessage) -> Vec<(String, String)>
pub fn headers_to_message_fields(&self, header_map: &[(String, String)])
-> Result<(Option<String>, Option<String>, Option<Vec<u8>>)>
}
```
Features:
- Sends handshake messages (InitialRequest, InitialResponse, CertificateRequest, CertificateResponse) as JSON POST to `/.well-known/auth`
- Sends General messages via BRC-104: deserializes payload as `HttpRequest`, makes actual HTTP request with auth headers, wraps response in `HttpResponse` payload
- Automatically strips trailing slashes from base URL
- Uses `reqwest` client (requires `http` feature)
- Uses `std::sync::RwLock` for callback storage (synchronous access, no tokio::spawn needed)
- Invokes registered callback with response messages
- Implements `Debug` trait with redacted internal fields
### WebSocketTransport
WebSocket-based transport for full-duplex communication (requires `websocket` feature):
```rust
pub struct WebSocketTransport {
base_url: String,
sink: Arc<Mutex<Option<WsSink>>>,
on_data_callbacks: Arc<RwLock<Vec<Box<TransportCallback>>>>,
read_deadline_secs: u64,
}
impl WebSocketTransport {
pub fn new(options: WebSocketTransportOptions) -> Result<Self>
pub fn base_url(&self) -> &str
pub fn read_deadline_secs(&self) -> u64
}
```
Configuration:
```rust
pub struct WebSocketTransportOptions {
pub base_url: String, // Must start with ws:// or wss://
pub read_deadline_secs: Option<u64>, // Defaults to 30 if None or 0
}
```
Features:
- Lazy connection: WebSocket connection established on first `send()` call
- Background receive loop: spawns a tokio task that reads incoming messages and dispatches to all registered callbacks
- JSON-serialized `AuthMessage` payloads over WebSocket text frames (also accepts binary frames)
- Connection dropped on send/receive errors (matches Go SDK behavior)
- Requires at least one callback registered before sending (returns `TransportError` otherwise)
- URL validation: rejects non-`ws://`/`wss://` schemes and empty hosts
- Supports multiple callbacks (unlike HTTP transport which stores a single callback)
- Ping/Pong handled automatically by tungstenite
- Malformed incoming JSON messages silently skipped (matches Go SDK behavior)
- Implements `Debug` trait with base_url and read_deadline_secs
### MockTransport
Testing transport for unit tests:
```rust
#[derive(Default)]
pub struct MockTransport {
sent_messages: Arc<RwLock<Vec<AuthMessage>>>,
response_queue: Arc<RwLock<Vec<AuthMessage>>>,
callback: Arc<RwLock<Option<Box<TransportCallback>>>>,
}
impl MockTransport {
pub fn new() -> Self
pub async fn queue_response(&self, message: AuthMessage)
pub async fn get_sent_messages(&self) -> Vec<AuthMessage>
pub async fn clear_sent(&self)
pub async fn receive_message(&self, message: AuthMessage) -> Result<()>
}
```
Features:
- Records all sent messages for assertion
- Queues response messages to be returned in order (FIFO)
- Simulates incoming messages via `receive_message()`
- Thread-safe with `Arc<RwLock<...>>` for concurrent access (uses `tokio::sync::RwLock`)
- Uses `tokio::spawn` for `set_callback`/`clear_callback` (async write to tokio RwLock from sync methods)
- Implements `Default` and `Debug` traits
### HttpRequest
Deserialized HTTP request from General message payload (BRC-104):
```rust
pub struct HttpRequest {
pub request_id: [u8; 32], // Request correlation ID
pub method: String, // HTTP method (GET, POST, etc.)
pub path: String, // URL path (e.g., "/api/users")
pub search: String, // URL query string (e.g., "?foo=bar")
pub headers: Vec<(String, String)>, // HTTP headers
pub body: Vec<u8>, // Request body
}
impl HttpRequest {
pub fn from_payload(payload: &[u8]) -> Result<Self>
pub fn to_payload(&self) -> Vec<u8>
pub fn url_postfix(&self) -> String // Returns path + search
}
```
Payload format: `[request_id: 32][method: varint+str][path: varint+str][search: varint+str][headers: varint+pairs][body: varint+bytes]`
### HttpResponse
HTTP response to be serialized as General message payload (BRC-104):
```rust
pub struct HttpResponse {
pub request_id: [u8; 32], // Request ID from response header
pub status: u16, // HTTP status code
pub headers: Vec<(String, String)>, // x-bsv-* and authorization headers
pub body: Vec<u8>, // Response body
}
impl HttpResponse {
pub fn from_payload(payload: &[u8]) -> Result<Self>
pub fn to_payload(&self) -> Vec<u8>
}
```
Payload format: `[request_id: 32][status: varint][headers: varint+pairs][body: varint+bytes]`
### BRC-104 HTTP Headers
The `headers` module defines constants for authenticated HTTP communication:
```rust
pub mod headers {
pub const VERSION: &str = "x-bsv-auth-version";
pub const IDENTITY_KEY: &str = "x-bsv-auth-identity-key";
pub const NONCE: &str = "x-bsv-auth-nonce";
pub const YOUR_NONCE: &str = "x-bsv-auth-your-nonce";
pub const SIGNATURE: &str = "x-bsv-auth-signature";
pub const MESSAGE_TYPE: &str = "x-bsv-auth-message-type";
pub const REQUEST_ID: &str = "x-bsv-auth-request-id";
pub const REQUESTED_CERTIFICATES: &str = "x-bsv-auth-requested-certificates";
}
```
| `x-bsv-auth-version` | Protocol version | `"0.1"` |
| `x-bsv-auth-identity-key` | Sender's public key | hex |
| `x-bsv-auth-nonce` | Sender's nonce | base64 |
| `x-bsv-auth-your-nonce` | Peer's previous nonce | base64 |
| `x-bsv-auth-signature` | Message signature | base64 or hex |
| `x-bsv-auth-message-type` | Message type | string |
| `x-bsv-auth-request-id` | Request correlation ID | base64 |
| `x-bsv-auth-requested-certificates` | Certificate requirements | JSON |
## Usage
### Creating an HTTP Transport
```rust
use bsv_rs::auth::transports::SimplifiedFetchTransport;
let transport = SimplifiedFetchTransport::new("https://example.com");
assert_eq!(transport.base_url(), "https://example.com");
```
### Creating a WebSocket Transport
```rust
use bsv_rs::auth::transports::{WebSocketTransport, WebSocketTransportOptions};
let transport = WebSocketTransport::new(WebSocketTransportOptions {
base_url: "wss://example.com/ws".to_string(),
read_deadline_secs: Some(60),
}).unwrap();
assert_eq!(transport.base_url(), "wss://example.com/ws");
assert_eq!(transport.read_deadline_secs(), 60);
```
### Creating HTTP Request Payloads
```rust
use bsv_rs::auth::transports::HttpRequest;
let request = HttpRequest {
request_id: [42u8; 32],
method: "POST".to_string(),
path: "/api/v1/users".to_string(),
search: String::new(),
headers: vec![
("content-type".to_string(), "application/json".to_string()),
],
body: b"{\"name\":\"Alice\"}".to_vec(),
};
let payload = request.to_payload();
let decoded = HttpRequest::from_payload(&payload).unwrap();
assert_eq!(decoded.url_postfix(), "/api/v1/users");
```
### Using MockTransport in Tests
```rust
use bsv_rs::auth::transports::{MockTransport, Transport};
use bsv_rs::auth::types::{AuthMessage, MessageType};
#[tokio::test]
async fn test_auth_flow() {
let transport = MockTransport::new();
// Set up callback to capture received messages
let received = Arc::new(RwLock::new(Vec::new()));
let received_clone = received.clone();
transport.set_callback(Box::new(move |msg| {
let received = received_clone.clone();
Box::pin(async move {
received.write().await.push(msg);
Ok(())
})
}));
// Wait for callback to be set (tokio::spawn in set_callback)
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
// Queue a response and send a message
let response = AuthMessage::new(MessageType::InitialResponse, key);
transport.queue_response(response).await;
transport.send(&request).await.unwrap();
assert_eq!(transport.get_sent_messages().await.len(), 1);
}
```
## Internal Methods
### SimplifiedFetchTransport Internal Methods
| `auth_url()` | Returns `{base_url}/.well-known/auth` |
| `message_to_headers()` | Converts AuthMessage to HTTP header pairs |
| `headers_to_message_fields()` | Parses HTTP headers into nonce, your_nonce, signature |
| `invoke_callback()` | Invokes the registered callback with a message |
### WebSocketTransport Internal Methods
| `connect()` | Establishes WebSocket connection, spawns background receive loop |
### Varint Encoding (Internal)
The module uses Bitcoin-style varint encoding for payload serialization:
| `read_varint(bytes)` | Reads a varint, returns `(i64_value, bytes_consumed)` |
| `write_varint(value)` | Writes an i64 value as varint bytes |
Varint sizes (Bitcoin-style):
- `0-252`: 1 byte (value directly)
- `253-65535`: 3 bytes (0xFD prefix + 2 bytes LE)
- `65536-4294967295`: 5 bytes (0xFE prefix + 4 bytes LE)
- Larger values: 9 bytes (0xFF prefix + 8 bytes LE)
- `-1` (empty/missing convention): 9 bytes (0xFF + 8 bytes of 0xFF)
## Message Routing
`SimplifiedFetchTransport` routes messages based on `MessageType`:
| `InitialRequest` | POST to `/.well-known/auth` as JSON |
| `InitialResponse` | POST to `/.well-known/auth` as JSON |
| `CertificateRequest` | POST to `/.well-known/auth` as JSON |
| `CertificateResponse` | POST to `/.well-known/auth` as JSON |
| `General` | BRC-104: Parse payload as `HttpRequest`, make HTTP request with auth headers, wrap response in `HttpResponse` payload |
`WebSocketTransport` sends all message types as JSON text frames over WebSocket.
### General Message Flow (BRC-104, HTTP only)
1. Deserialize `AuthMessage.payload` as `HttpRequest`
2. Build HTTP request to `{base_url}{url_postfix}` with method from payload
3. Add auth headers (version, identity_key, nonce, your_nonce, signature, request_id)
4. Include original headers (`x-bsv-*`, `authorization`, `content-type`) excluding `x-bsv-auth-*`
5. Send request with body from payload
6. Parse response: extract request ID from headers (falls back to original if absent)
7. Filter response headers: include `x-bsv-*` (excluding `x-bsv-auth-*`) and `authorization`, sorted alphabetically
8. Build `HttpResponse` payload with status, filtered headers, body
9. Create response `AuthMessage`: identity key from response header if present, otherwise uses request's identity key (auth headers on response are optional)
10. Extract optional nonce, your_nonce, signature from response headers; check for certificate request in response message type header
11. Invoke callback with response message
## Feature Flags
```toml
# HTTP transport requires the http feature
bsv-rs = { version = "0.3", features = ["auth", "http"] }
# WebSocket transport requires the websocket feature (adds tokio-tungstenite, futures-util)
bsv-rs = { version = "0.3", features = ["auth", "websocket"] }
```
Without the `http` feature, `SimplifiedFetchTransport::send()` returns an error:
```rust
Error::AuthError("HTTP transport requires the 'http' feature".into())
```
The `websocket` feature is opt-in and not included in `full`. It gates:
- The `websocket_transport` submodule in `mod.rs`
- The `WebSocketTransport` and `WebSocketTransportOptions` re-exports
## Dependencies
- `async_trait` - Async trait support for `Transport`
- `std::sync::RwLock` - Synchronous callback storage for `SimplifiedFetchTransport`
- `tokio::sync::RwLock` - Async state storage for `MockTransport` and `WebSocketTransport`
- `tokio::sync::Mutex` - Async mutex for WebSocket write half
- `reqwest` - HTTP client (optional, requires `http` feature)
- `tokio-tungstenite` - WebSocket client (optional, requires `websocket` feature)
- `futures-util` - Stream/Sink extensions for WebSocket (optional, requires `websocket` feature)
- `serde_json` - JSON serialization for certificate requests and auth messages
## Error Handling
| `AuthError("HTTP transport requires the 'http' feature")` | Sending without `http` feature |
| `AuthError("HTTP request failed: {}")` | Network error from reqwest |
| `AuthError("Auth endpoint returned {status}: {body}")` | Non-2xx handshake HTTP response |
| `AuthError("Failed to read auth response: {}")` | Failed to read handshake response body |
| `AuthError("Failed to parse auth response: {} - body: {}")` | Invalid JSON in handshake response |
| `AuthError("General message must have payload")` | General message missing payload |
| `AuthError("Failed to read response: {}")` | Failed to read General message HTTP response body |
| `AuthError("Payload too short for ...")` | Malformed payload during deserialization |
| `AuthError("Invalid ... UTF-8: {}")` | Non-UTF8 string in payload |
| `AuthError("Empty varint")` | Empty bytes when reading varint |
| `AuthError("Incomplete varint (fd/fe/ff)")` | Truncated varint encoding |
| `AuthError("Response payload too short for request ID")` | HttpResponse payload < 32 bytes |
| `AuthError("Invalid request ID length")` | Response request ID not 32 bytes |
| `AuthError("Failed to acquire callback lock")` | Poisoned std::sync::RwLock on callback |
| `TransportError("base_url is required...")` | WebSocket created with empty URL |
| `TransportError("WebSocket URL must start with ws://...")` | Invalid WebSocket URL scheme |
| `TransportError("WebSocket URL must include a host")` | WebSocket URL has no host component |
| `TransportError("no handler registered")` | WebSocket send before setting callback |
| `TransportError("failed to connect to WebSocket: {}")` | WebSocket connection failure |
| `TransportError("failed to marshal auth message: {}")` | JSON serialization error |
| `TransportError("failed to send WebSocket message: {}")` | WebSocket write error |
| `TransportError("WebSocket connection not available")` | Connection dropped after error |
## Testing
Run transport tests:
```bash
# Without HTTP (mock transport only)
cargo test --features auth transports
# With HTTP transport
cargo test --features auth,http transports
# With WebSocket transport
cargo test --features auth,websocket transports
```
## Thread Safety
`SimplifiedFetchTransport` uses `std::sync::RwLock` (not tokio) for callback storage, so `set_callback()` and `clear_callback()` operate synchronously without spawning tasks. The `invoke_callback()` method acquires a read lock briefly to get the future, then drops the lock before awaiting.
`MockTransport` uses `tokio::sync::RwLock` for all internal state. Since `set_callback()` and `clear_callback()` are synchronous trait methods, it uses `tokio::spawn` to write to the async RwLock. When testing, a small delay may be needed after setting callbacks.
`WebSocketTransport` uses `tokio::sync::Mutex` for the write-half sink and `tokio::sync::RwLock` for callbacks. Like `MockTransport`, `set_callback()` and `clear_callback()` use `tokio::spawn`. It supports multiple concurrent callbacks (stored as `Vec<Box<TransportCallback>>`).
## Exports
From `mod.rs`:
```rust
// Always exported
pub use http::{
headers, HttpRequest, HttpResponse, MockTransport, SimplifiedFetchTransport, Transport,
TransportCallback,
};
// Exported with websocket feature
#[cfg(feature = "websocket")]
pub use websocket_transport::{WebSocketTransport, WebSocketTransportOptions};
```
## Related Documentation
- `../CLAUDE.md` - Auth module overview
- `../types.rs` - AuthMessage and MessageType definitions
- `../../wallet/CLAUDE.md` - Wallet module (WalletInterface)
- `../../primitives/CLAUDE.md` - PublicKey, base64/hex encoding