Duration: 60 minutes
Prerequisites: Node.js, basic TypeScript knowledge, understanding of HTTP and authentication
Learning Goals:
Use AuthFetch when you need:
For general HTTP client configuration, use HTTP Client Configuration Guide instead:
AuthFetch is a specialized HTTP client that implements BRC-103 and BRC-104 authentication protocols for secure peer-to-peer communication in the BSV ecosystem. Unlike traditional API authentication (like JWT tokens), AuthFetch uses cryptographic signatures and certificate-based authentication.
In this tutorial, you’ll create:
WalletClient
import { AuthFetch, WalletClient } from '@bsv/sdk'
async function createAuthFetch() {
// Create wallet for authentication - connects to local wallet (e.g., MetaNet Desktop)
const wallet = new WalletClient('auto', 'localhost')
// Check if wallet is connected
try {
const authStatus = await wallet.isAuthenticated()
console.log('Wallet authenticated:', authStatus.authenticated)
const network = await wallet.getNetwork()
console.log('Connected to network:', network.network)
} catch (error) {
console.log('Wallet connection status:', error.message)
// This is expected if no wallet is running
}
// Create AuthFetch instance
const authFetch = new AuthFetch(wallet)
console.log('AuthFetch client created')
return authFetch
}
async function basicAuthenticatedRequest() {
const authFetch = await createAuthFetch()
try {
// Make authenticated request to a real, working endpoint
const response = await authFetch.fetch('https://httpbin.org/get', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'X-BSV-Tutorial': 'AuthFetch-Example'
}
})
if (response.ok) {
const data = await response.json()
console.log('Authenticated request successful!')
console.log('Request URL:', data.url)
console.log('Headers sent:', Object.keys(data.headers).length)
return data
} else {
console.error('Request failed:', response.status, response.statusText)
}
} catch (error) {
console.error('Authentication error:', error.message)
if (error.message.includes('No wallet available')) {
console.log(' Install and run MetaNet Desktop Wallet to test with real authentication')
console.log(' For now, this demonstrates the AuthFetch API structure')
}
}
}
// Test the basic functionality
basicAuthenticatedRequest().catch(console.error)
import { AuthFetch, WalletClient } from '@bsv/sdk'
async function createAuthFetchWithCertificates() {
const wallet = new WalletClient('auto', 'localhost')
// Define required certificates from peers
const requestedCertificates = {
certifiers: {
// Require identity certificates from trusted certifier
'identity-certifier-key': {
certificateTypes: ['identity-cert'],
fieldsRequired: ['name', 'email']
}
},
acquisitionProtocol: 'direct' as const
}
const authFetch = new AuthFetch(wallet, requestedCertificates)
console.log('AuthFetch with certificate requirements created')
return authFetch
}
async function testCertificateRequirements() {
const authFetch = await createAuthFetchWithCertificates()
try {
// Test with a real endpoint that will show our certificate headers (using a dummy URL for demo purposes)
const response = await authFetch.fetch('https://httpbin.org/headers', {
method: 'GET'
})
if (response.ok) {
const data = await response.json()
console.log('Certificate-enabled request successful!')
console.log('Headers sent to server:', data.headers)
// AuthFetch will include certificate-related headers when available
const certHeaders = Object.keys(data.headers).filter(h =>
h.toLowerCase().includes('cert') || h.toLowerCase().includes('auth')
)
console.log('Certificate/Auth headers:', certHeaders)
} else {
console.error('Request failed:', response.status)
}
} catch (error) {
console.error('Certificate request error:', error.message)
if (error.message.includes('No wallet available')) {
console.log(' Certificate exchange requires a connected wallet')
}
}
}
testCertificateRequirements().catch(console.error)
import { AuthFetch, WalletClient } from '@bsv/sdk'
class CertificateManager {
private authFetch: AuthFetch
constructor(wallet: WalletClient) {
this.authFetch = new AuthFetch(wallet)
}
async requestPeerCertificates(
peerBaseUrl: string,
certificateRequirements: any
): Promise<any[]> {
try {
console.log('Requesting certificates from peer:', peerBaseUrl)
const certificates = await this.authFetch.sendCertificateRequest(
peerBaseUrl,
certificateRequirements
)
console.log('Received certificates:', certificates.length)
return certificates
} catch (error) {
console.error('Certificate request failed:', error)
throw error
}
}
async verifyPeerIdentity(peerUrl: string): Promise<{
verified: boolean
identity: string | null
certificates: any[]
}> {
const certificateRequirements = {
certifiers: {
'trusted-identity-provider': {
certificateTypes: ['identity'],
fieldsRequired: ['name']
}
},
acquisitionProtocol: 'direct' as const
}
try {
const certificates = await this.requestPeerCertificates(
peerUrl,
certificateRequirements
)
// Verify certificates (simplified verification)
const verified = certificates.length > 0
const identity = verified ? certificates[0].subject : null
return { verified, identity, certificates }
} catch (error) {
console.error('Identity verification failed:', error)
return { verified: false, identity: null, certificates: [] }
}
}
}
async function demonstrateCertificateExchange() {
const wallet = new WalletClient('auto', 'localhost')
const certManager = new CertificateManager(wallet)
// Example peer URLs (replace with actual peer endpoints)
const peerUrls = [
'https://peer1.example.com',
'https://peer2.example.com'
]
for (const peerUrl of peerUrls) {
console.log(`\n=== Verifying peer: ${peerUrl} ===`)
try {
const verification = await certManager.verifyPeerIdentity(peerUrl)
if (verification.verified) {
console.log(' Peer verified successfully')
console.log('Identity:', verification.identity)
console.log('Certificates received:', verification.certificates.length)
} else {
console.log(' Peer verification failed')
}
} catch (error) {
console.log(' Peer unreachable or invalid')
}
}
}
demonstrateCertificateExchange().catch(console.error)
import { AuthFetch, WalletClient } from '@bsv/sdk'
class SecureAPIClient {
private authFetch: AuthFetch
private baseUrl: string
constructor(baseUrl: string, wallet?: WalletClient) {
this.baseUrl = baseUrl
this.authFetch = new AuthFetch(wallet || new WalletClient('auto', 'localhost'))
}
async get(endpoint: string, options: any = {}): Promise<any> {
return this.request('GET', endpoint, null, options)
}
async post(endpoint: string, data: any, options: any = {}): Promise<any> {
return this.request('POST', endpoint, data, options)
}
async put(endpoint: string, data: any, options: any = {}): Promise<any> {
return this.request('PUT', endpoint, data, options)
}
async delete(endpoint: string, options: any = {}): Promise<any> {
return this.request('DELETE', endpoint, null, options)
}
private async request(
method: string,
endpoint: string,
data: any = null,
options: any = {}
): Promise<any> {
const url = `${this.baseUrl}${endpoint}`
const requestOptions: any = {
method,
headers: {
'Content-Type': 'application/json',
...options.headers
}
}
if (data) {
requestOptions.body = JSON.stringify(data)
}
try {
console.log(`Making authenticated ${method} request to ${endpoint}`)
const response = await this.authFetch.fetch(url, requestOptions)
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
const contentType = response.headers.get('content-type')
if (contentType && contentType.includes('application/json')) {
return await response.json()
} else {
return await response.text()
}
} catch (error) {
console.error(`Request failed for ${method} ${endpoint}:`, error)
throw error
}
}
async healthCheck(): Promise<boolean> {
try {
await this.get('/health')
return true
} catch (error) {
return false
}
}
}
async function demonstrateSecureAPIClient() {
// Create secure API client using real, testable endpoints
const apiClient = new SecureAPIClient('https://httpbin.org')
try {
// Health check using a real endpoint
console.log('Testing API client with real endpoints...')
// Test GET request
const getResult = await apiClient.get('/get?test=true&client=secure')
console.log('✅ GET request successful')
console.log('Request URL:', getResult.url)
console.log('Query parameters received:', getResult.args)
// Test POST request with data
const postResult = await apiClient.post('/post', {
user: 'demo-user',
action: 'test-post',
timestamp: new Date().toISOString(),
authenticated: true
})
console.log('✅ POST request successful')
console.log('Data sent:', postResult.json)
console.log('Content-Type:', postResult.headers['Content-Type'])
// Test PUT request
const putResult = await apiClient.put('/put', {
resource: 'user-settings',
theme: 'dark',
notifications: true,
updated: new Date().toISOString()
})
console.log('✅ PUT request successful')
console.log('PUT data received:', putResult.json)
// Test DELETE request
const deleteResult = await apiClient.delete('/delete')
console.log('✅ DELETE request successful')
console.log('DELETE method confirmed:', deleteResult.url)
// Test custom headers
const headersResult = await apiClient.get('/headers')
console.log('✅ Headers test successful')
console.log('Custom headers sent:', Object.keys(headersResult.headers).length)
return {
get: getResult,
post: postResult,
put: putResult,
delete: deleteResult,
headers: headersResult
}
} catch (error) {
console.error('API operations failed:', error.message)
if (error.message.includes('No wallet available')) {
console.log('💡 Install MetaNet Desktop Wallet to test with real authentication')
console.log(' The API calls work, but authentication requires a connected wallet')
}
}
}
demonstrateSecureAPIClient().catch(console.error)
import { AuthFetch, WalletClient } from '@bsv/sdk'
interface PeerInfo {
url: string
identity: string | null
verified: boolean
lastContact: Date
}
class PeerNetwork {
private authFetch: AuthFetch
private peers: Map<string, PeerInfo> = new Map()
constructor(wallet?: WalletClient) {
this.authFetch = new AuthFetch(wallet || new WalletClient('auto', 'localhost'))
}
async addPeer(peerUrl: string): Promise<boolean> {
try {
console.log(`Adding peer: ${peerUrl}`)
// Verify peer identity
const verification = await this.verifyPeer(peerUrl)
const peerInfo: PeerInfo = {
url: peerUrl,
identity: verification.identity,
verified: verification.verified,
lastContact: new Date()
}
this.peers.set(peerUrl, peerInfo)
console.log(`Peer ${peerUrl} ${verification.verified ? 'verified' : 'unverified'}`)
return verification.verified
} catch (error) {
console.error(`Failed to add peer ${peerUrl}:`, error)
return false
}
}
private async verifyPeer(peerUrl: string): Promise<{
verified: boolean
identity: string | null
}> {
try {
// Simple ping to verify peer is reachable
const response = await this.authFetch.fetch(`${peerUrl}/ping`, {
method: 'GET'
})
if (response.ok) {
// In a real implementation, you would verify certificates here
return { verified: true, identity: 'peer-identity' }
} else {
return { verified: false, identity: null }
}
} catch (error) {
return { verified: false, identity: null }
}
}
async broadcastMessage(message: any): Promise<{
successful: string[]
failed: string[]
}> {
const successful: string[] = []
const failed: string[] = []
console.log(`Broadcasting message to ${this.peers.size} peers`)
const promises = Array.from(this.peers.entries()).map(async ([url, peerInfo]) => {
try {
const response = await this.authFetch.fetch(`${url}/message`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(message)
})
if (response.ok) {
successful.push(url)
// Update last contact
peerInfo.lastContact = new Date()
} else {
failed.push(url)
}
} catch (error) {
failed.push(url)
console.error(`Failed to send message to ${url}:`, error)
}
})
await Promise.all(promises)
console.log(`Broadcast complete: ${successful.length} successful, ${failed.length} failed`)
return { successful, failed }
}
async sendDirectMessage(peerUrl: string, message: any): Promise<any> {
const peer = this.peers.get(peerUrl)
if (!peer) {
throw new Error(`Peer ${peerUrl} not found`)
}
if (!peer.verified) {
throw new Error(`Peer ${peerUrl} not verified`)
}
try {
const response = await this.authFetch.fetch(`${peerUrl}/direct-message`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(message)
})
if (response.ok) {
peer.lastContact = new Date()
return await response.json()
} else {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
} catch (error) {
console.error(`Direct message to ${peerUrl} failed:`, error)
throw error
}
}
getPeerStatus(): { total: number; verified: number; unverified: number } {
const total = this.peers.size
let verified = 0
let unverified = 0
for (const peer of this.peers.values()) {
if (peer.verified) {
verified++
} else {
unverified++
}
}
return { total, verified, unverified }
}
listPeers(): PeerInfo[] {
return Array.from(this.peers.values())
}
}
async function demonstratePeerNetwork() {
const network = new PeerNetwork()
// Add peers (replace with actual peer URLs)
const peerUrls = [
'https://peer1.example.com',
'https://peer2.example.com',
'https://peer3.example.com'
]
console.log('=== Setting up peer network ===')
for (const peerUrl of peerUrls) {
await network.addPeer(peerUrl)
}
const status = network.getPeerStatus()
console.log('Network status:', status)
// Broadcast message
const broadcastMessage = {
type: 'announcement',
content: 'Hello from authenticated peer network!',
timestamp: new Date().toISOString()
}
console.log('\n=== Broadcasting message ===')
const broadcastResult = await network.broadcastMessage(broadcastMessage)
console.log('Broadcast result:', broadcastResult)
// Send direct message to first verified peer
const peers = network.listPeers()
const verifiedPeer = peers.find(p => p.verified)
if (verifiedPeer) {
console.log('\n=== Sending direct message ===')
try {
const directMessage = {
type: 'direct',
content: 'This is a direct authenticated message',
timestamp: new Date().toISOString()
}
const response = await network.sendDirectMessage(verifiedPeer.url, directMessage)
console.log('Direct message response:', response)
} catch (error) {
console.log('Direct message failed (expected in demo)')
}
}
return { status, broadcastResult, peers }
}
demonstratePeerNetwork().catch(console.error)
import { AuthFetch, WalletClient } from '@bsv/sdk'
class RobustAuthClient {
private authFetch: AuthFetch
private maxRetries: number = 3
private retryDelay: number = 1000
constructor(wallet?: WalletClient) {
this.authFetch = new AuthFetch(wallet || new WalletClient('auto', 'localhost'))
}
async authenticatedRequest(
url: string,
options: any = {},
retryCount: number = 0
): Promise<Response> {
try {
const response = await this.authFetch.fetch(url, options)
if (response.status === 401 && retryCount < this.maxRetries) {
console.log(`Authentication failed, retrying... (${retryCount + 1}/${this.maxRetries})`)
// Wait and retry - AuthFetch will handle session management automatically
await this.delay(this.retryDelay * (retryCount + 1))
return this.authenticatedRequest(url, options, retryCount + 1)
}
return response
} catch (error) {
if (retryCount < this.maxRetries) {
console.log(`Request failed, retrying... (${retryCount + 1}/${this.maxRetries})`)
await this.delay(this.retryDelay * (retryCount + 1))
return this.authenticatedRequest(url, options, retryCount + 1)
}
throw error
}
}
private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms))
}
async batchRequests(requests: Array<{
url: string
options?: any
}>): Promise<Array<{
success: boolean
response?: any
error?: string
}>> {
const results = await Promise.allSettled(
requests.map(req => this.authenticatedRequest(req.url, req.options))
)
return results.map((result, index) => {
if (result.status === 'fulfilled') {
return {
success: true,
response: result.value
}
} else {
return {
success: false,
error: result.reason.message
}
}
})
}
}
async function demonstrateRobustAuthentication() {
const robustClient = new RobustAuthClient()
console.log('=== Testing robust authentication ===')
// Single request with retry logic
try {
const response = await robustClient.authenticatedRequest('https://api.example.com/data')
console.log('Single request successful:', response.ok)
} catch (error) {
console.log('Single request failed after retries:', error.message)
}
// Batch requests
const batchRequests = [
{ url: 'https://api.example.com/endpoint1' },
{ url: 'https://api.example.com/endpoint2' },
{ url: 'https://api.example.com/endpoint3' }
]
console.log('\n=== Testing batch requests ===')
const batchResults = await robustClient.batchRequests(batchRequests)
batchResults.forEach((result, index) => {
console.log(`Request ${index + 1}:`, result.success ? 'SUCCESS' : `FAILED - ${result.error}`)
})
return batchResults
}
demonstrateRobustAuthentication().catch(console.error)
```typescript import { AuthFetch, WalletClient } from ‘@bsv/sdk’
enum AuthErrorType { NETWORK_ERROR = ‘network_error’, AUTHENTICATION_FAILED = ‘authentication_failed’, CERTIFICATE_INVALID = ‘certificate_invalid’, PEER_UNREACHABLE = ‘peer_unreachable’, SESSION_EXPIRED = ‘session_expired’ }
class AuthError extends Error { constructor( public type: AuthErrorType, message: string, public originalError?: Error ) { super(message) this.name = ‘AuthError’ } }
class AuthFetchWithErrorHandling { private authFetch: AuthFetch private debugMode: boolean = false
constructor(wallet?: WalletClient, debugMode: boolean = false) { this.authFetch = new AuthFetch(wallet || new WalletClient(‘auto’, ‘localhost’)) this.debugMode = debugMode }
async safeRequest(url: string, options: any = {}): Promise<{
success: boolean
data?: any
error?: AuthError
}> {
try {
if (this.debugMode) {
console.log([DEBUG] Making request to: ${url}
)
console.log([DEBUG] Options:
, JSON.stringify(options, null, 2))
}
const response = await this.authFetch.fetch(url, options)
if (this.debugMode) {
console.log(`[DEBUG] Response status: ${response.status}`)
console.log(`[DEBUG] Response headers:`, Object.fromEntries(response.headers.entries()))
}
if (!response.ok) {
const errorType = this.categorizeHttpError(response.status)
const errorMessage = await this.extractErrorMessage(response)
return {
success: false,
error: new AuthError(errorType, errorMessage)
}
}
const data = await this.parseResponse(response)
return { success: true, data }
} catch (error) {
if (this.debugMode) {
console.log(`[DEBUG] Request failed:`, error)
}
const authError = this.categorizeError(error)
return { success: false, error: authError }
} }
private categorizeHttpError(status: number): AuthErrorType { switch (status) { case 401: return AuthErrorType.AUTHENTICATION_FAILED case 403: return AuthErrorType.CERTIFICATE_INVALID case 408: case 504: return AuthErrorType.PEER_UNREACHABLE default: return AuthErrorType.NETWORK_ERROR } }
private async extractErrorMessage(response: Response): Promise
private async parseResponse(response: Response): Promise
private categorizeError(error: any): AuthError { if (error.name === ‘TypeError’ && error.message.includes(‘fetch’)) { return new AuthError( AuthErrorType.NETWORK_ERROR, ‘Network connection failed’, error ) }
if (error.message.includes('certificate')) {
return new AuthError(
AuthErrorType.CERTIFICATE_INVALID,
'Certificate validation failed',
error
)
}
if (error.message.includes('session')) {
return new AuthError(
AuthErrorType.SESSION_EXPIRED,
'Authentication session expired',
error
)
}
return new AuthError(
AuthErrorType.NETWORK_ERROR,
error.message || 'Unknown error occurred',
error
) }
async testConnectivity(urls: string[]): Promise<{ reachable: string[] unreachable: string[] errors: Record<string, string> }> { const reachable: string[] = [] const unreachable: string[] = [] const errors: Record<string, string> = {}
console.log(`Testing connectivity to ${urls.length} endpoints...`)
const results = await Promise.allSettled(
urls.map(url => this.safeRequest(`${url}/ping`))
)
results.forEach((result, index) => {
const url = urls[index]
if (result.status === 'fulfilled' && result.value.success) {
reachable.push(url)
console.log(` ${url} - reachable`)
} else {
unreachable.push(url)
const error = result.status === 'fulfilled'
? result.value.error?.message || 'Unknown error'
: result.reason.message
errors[url] = error
console.log(` ${url} - ${error}`)
}
})
return { reachable, unreachable, errors } } }
async function demonstrateErrorHandling() { const authClient = new AuthFetchWithErrorHandling(undefined, true) // Debug mode on
console.log(‘=== Testing error handling ===’)
// Test various scenarios const testUrls = [ ‘https://httpbin.org/status/200’, // Should succeed ‘https://httpbin.org/status/401’, // Authentication error ‘https://httpbin.org/status/403’, // Certificate error ‘https://httpbin.org/status/500’, // Server error ‘https://invalid-domain-12345.com’ // Network error ]
for (const url of testUrls) {
console.log(\n--- Testing: ${url} ---
)
const result = await authClient.safeRequest(url)
if (result.success) {
console.log(' Request successful')
} else {
console.log(` Request failed: ${result.error?.type} - ${result.error?.message}`)
} }
// Test connectivity to multiple endpoints console.log(‘\n=== Testing connectivity ===’) const connectivityTest = await authClient.testConnectivity([ ‘https://httpbin.org’, ‘https://jsonplaceholder.typicode.com’, ‘https://invalid-endpoint.example.com’ ])
console.log(‘Connectivity results:’, connectivityTest)
return connectivityTest }
demonstrateErrorHandling().catch(console.error)