External Bridge

Receive requests from browser extensions, CLI tools, and other external clients.

Overview

The External Bridge allows external clients to communicate with your HaexVault extension via a secure WebSocket connection. This enables powerful integrations like browser extensions for password autofill, CLI tools for automation, or server-side applications.

Browser Extensions

Autofill passwords, sync bookmarks, or access secure notes directly from your browser.

CLI Tools

Automate workflows, manage secrets, or integrate with scripts and CI/CD pipelines.

Server Applications

Build backend services that securely access your extension's data.

External clients must be authorized by the user before they can send requests. All communication is encrypted end-to-end.

How it Works

The External Bridge uses a WebSocket-based protocol with public key authentication:

  1. External client connects to HaexVault's WebSocket server
  2. Client sends handshake with public key, name, and requested extensions
  3. HaexVault shows authorization dialog - user can Allow Once, Allow Always, or Block
  4. Authorized clients can send requests targeting a specific extension
  5. If the extension is not running, HaexVault auto-starts it (minimized) to handle the request
  6. Extension processes the request and sends response back to the client
External Client                    HaexVault                     Extension
(Browser Extension)               (Desktop App)                   (Plugin)
       |                               |                              |
       |-- WebSocket Connect --------->|                              |
       |                               |                              |
       |<-- Connection Accepted -------|                              |
       |                               |                              |
       |-- Handshake (publicKey, ------| ← Shows Authorization        |
       |   clientName, extensions)     |   Dialog to User             |
       |                               |                              |
       |<-- Paired (authorized) -------|                              |
       |                               |                              |
       |-- Request (action, payload) ->| ← Auto-starts Extension      |
       |                               |   if not running (minimized) |
       |                               |                              |
       |                               |-- Event: external:request -->|
       |                               |                              |
       |                               |<-- Response (data) ----------|
       |                               |                              |
       |<-- Response (data) -----------|                              |
       |                               |                              |

Handling Requests

Your extension receives external requests through events. You can either listen to all requests or register handlers for specific actions.

Event Listener (handle all requests)

import { EXTERNAL_EVENTS } from '@haex-space/vault-sdk'
import type { ExternalRequest, ExternalResponse } from '@haex-space/vault-sdk'

// Define your extension's action constants
export const ACTIONS = {
  GET_CREDENTIALS: 'getCredentials',
  SAVE_CREDENTIAL: 'saveCredential',
  DELETE_CREDENTIAL: 'deleteCredential',
} as const

const { client } = useHaexVaultSdk()

// Listen for external requests
client.on(EXTERNAL_EVENTS.REQUEST, async (event) => {
  const request: ExternalRequest = event.data
  const { requestId, publicKey, action, payload } = request

  console.log(`External request from ${publicKey}: ${action}`)

  try {
    let responseData: unknown

    switch (action) {
      case ACTIONS.GET_CREDENTIALS:
        responseData = await getCredentialsForUrl(payload.url as string)
        break

      case ACTIONS.SAVE_CREDENTIAL:
        await saveCredential(payload)
        responseData = { saved: true }
        break

      default:
        throw new Error(`Unknown action: ${action}`)
    }

    // Send success response
    await client.respondToExternalRequest(requestId, {
      requestId,
      success: true,
      data: responseData
    })
  } catch (error) {
    // Send error response
    await client.respondToExternalRequest(requestId, {
      requestId,
      success: false,
      error: error instanceof Error ? error.message : 'Unknown error'
    })
  }
})

Register Handler (per action)

import type { ExternalRequest, ExternalResponse } from '@haex-space/vault-sdk'

// Define your extension's action constants
export const ACTIONS = {
  GET_CREDENTIALS: 'getCredentials',
  SAVE_CREDENTIAL: 'saveCredential',
} as const

const { client } = useHaexVaultSdk()

// Register handlers for specific actions (cleaner alternative to event listener)
client.onExternalRequest(ACTIONS.GET_CREDENTIALS, async (request: ExternalRequest) => {
  const { url } = request.payload as { url: string }
  const credentials = await getCredentialsForUrl(url)

  return {
    requestId: request.requestId,
    success: true,
    data: credentials
  }
})

client.onExternalRequest(ACTIONS.SAVE_CREDENTIAL, async (request: ExternalRequest) => {
  await saveCredential(request.payload)

  return {
    requestId: request.requestId,
    success: true,
    data: { saved: true }
  }
})

Connection States

External clients go through several connection states. These are primarily used by client implementations (browser extensions), but understanding them helps when debugging.

import {
  ExternalConnectionState,
  ExternalConnectionErrorCode,
  isExternalClientConnected,
  canExternalClientSendRequests
} from '@haex-space/vault-sdk'

// Connection states (used by external clients like browser extensions)
ExternalConnectionState.DISCONNECTED    // Not connected to HaexVault
ExternalConnectionState.CONNECTING      // Attempting to connect
ExternalConnectionState.CONNECTED       // WebSocket connected, not authorized
ExternalConnectionState.PENDING_APPROVAL // Waiting for user approval
ExternalConnectionState.PAIRED          // Authorized and ready

// Error codes for i18n
ExternalConnectionErrorCode.NONE                  // No error
ExternalConnectionErrorCode.CLIENT_NOT_AUTHORIZED // Not authorized
ExternalConnectionErrorCode.CLIENT_BLOCKED        // User blocked this client
ExternalConnectionErrorCode.CONNECTION_FAILED     // HaexVault not running
ExternalConnectionErrorCode.CONNECTION_TIMEOUT    // Timeout
ExternalConnectionErrorCode.CONNECTION_CLOSED     // Unexpected disconnect
ExternalConnectionErrorCode.DECRYPTION_FAILED     // Invalid key
ExternalConnectionErrorCode.INVALID_MESSAGE       // Corrupted message

// Helper functions
isExternalClientConnected(state)      // true if connected/pending/paired
canExternalClientSendRequests(state)  // true only if PAIRED

Authorization

Users have full control over which external clients can access their extensions:

Allow Once

Grants temporary access for the current session. Authorization is cleared when HaexVault restarts.

Allow Always

Permanently authorizes the client. Stored in the database and persists across restarts.

Block

Permanently blocks the client. The client cannot request authorization again until unblocked in settings.

Blocked clients can be unblocked in HaexVault's settings. Always inform users if their connection was blocked.

Type Definitions

TypeScript interfaces for External Bridge communication:

// External request received by your extension
interface ExternalRequest {
  requestId: string                    // Unique ID for response correlation
  publicKey: string                    // Client's public key (identifier)
  action: string                       // Action to perform (your constants)
  payload: Record<string, unknown>     // Request data
}

// Response sent back to external client
interface ExternalResponse {
  requestId: string                    // Must match request
  success: boolean                     // Whether request succeeded
  data?: unknown                       // Response data (if success)
  error?: string                       // Error message (if failed)
}

// Authorized client (stored in database)
interface AuthorizedClient {
  id: string
  clientId: string                     // Public key fingerprint
  clientName: string                   // Human-readable name
  publicKey: string                    // Full public key (base64)
  extensionId: string                  // Which extension this client can access
  authorizedAt: string | null          // ISO 8601 timestamp
  lastSeen: string | null              // Last connection time
}

// Blocked client (stored in database)
interface BlockedClient {
  id: string
  clientId: string
  clientName: string
  publicKey: string
  blockedAt: string | null
}

// Pending authorization (in-memory, shown in approval dialog)
interface PendingAuthorization {
  clientId: string
  clientName: string
  publicKey: string
  requestedExtensions: RequestedExtension[]
}

interface RequestedExtension {
  name: string                         // e.g., "haex-pass"
  extensionPublicKey: string           // Extension's manifest public key
}