npm package: @kitzero/bridge-client
Description: TypeScript SDK for communicating with the KitZero Utility from a web application
Latest version: 0.1.0

# @kitzero/bridge-client

TypeScript SDK for communicating with the KitZero Utility from a web application. Handles discovery, WebSocket connection, authentication, capability execution, and typed events.

## Installation

```bash
npm install @kitzero/bridge-client
```

## Quick Start

```typescript
import { KitZeroBridge } from '@kitzero/bridge-client';

const bridge = new KitZeroBridge({
  workspaceId: 'ws_abc123',
  token: 'your-auth-token',
});

// Connect to the utility (discovers on localhost:9321)
await bridge.connect();

// Execute a capability
const handle = await bridge.executeCapability({
  pluginId: 'com.kitzero.pixhawk-tools',
  capabilityType: 'read',
  capabilityId: 'read-serial',
  inputs: {},
  runId: crypto.randomUUID(),
});

// Listen for progress
handle.onProgress((p) => console.log(`${p.pct}% — ${p.message}`));

// Wait for result
const result = await handle.result;
console.log('Evidence:', result.evidence);
console.log('Bundle ID:', result.evidenceBundleId);
```

## Discovery

Check if the utility is running before connecting:

```typescript
const health = await KitZeroBridge.discover(); // defaults to http://localhost:9321
console.log(health.status, health.version);
```

## Assertions

Execute a read, then pass the result to an assertion:

```typescript
// Step 1: Run the read
const readHandle = await bridge.executeCapability({
  pluginId: 'com.kitzero.pixhawk-tools',
  capabilityType: 'read',
  capabilityId: 'read-firmware-version',
  inputs: {},
  runId: crypto.randomUUID(),
});
const readResult = await readHandle.result;

// Step 2: Run the assertion with the read result as 'actual'
const assertHandle = await bridge.executeCapability({
  pluginId: 'com.kitzero.pixhawk-tools',
  capabilityType: 'assertion',
  capabilityId: 'assert-firmware-version',
  inputs: {
    actual: readResult.result,
    expectedValue: '4.6.1',
  },
  runId: crypto.randomUUID(),
});
const assertResult = await assertHandle.result;

if (assertResult.assertionResult?.passed) {
  console.log('Firmware version verified');
} else {
  console.log('Assertion failed:', assertResult.assertionResult?.message);
}
```

## Events

```typescript
// Device hot-plug
bridge.on('device:connected', (d) => console.log('Plugged in:', d.device));
bridge.on('device:disconnected', (d) => console.log('Removed:', d.deviceId));

// Evidence lifecycle
bridge.on('evidence:ready', (e) => console.log('Bundle:', e.evidenceBundleId));
bridge.on('evidence:uploadComplete', (e) => console.log('Uploaded:', e.receiptId));

// Connection state
bridge.on('connection:stateChange', ({ previous, current }) => {
  console.log(`${previous} -> ${current}`);
});

// Reconnection
bridge.on('connection:reconnecting', ({ attempt, delayMs }) => {
  console.log(`Reconnecting (attempt ${attempt}, ${delayMs}ms delay)`);
});
```

### Event Reference

| Event | Payload | Description |
|-------|---------|-------------|
| `connection:stateChange` | `{ previous, current }` | Connection state changed |
| `connection:error` | `{ error }` | Connection error |
| `connection:reconnecting` | `{ attempt, delayMs }` | Reconnection attempt |
| `auth:success` | `AuthHandshakeAckPayload` | Authenticated |
| `auth:error` | `AuthErrorPayload` | Auth failed |
| `device:connected` | `DeviceConnectedPayload` | USB device plugged in |
| `device:disconnected` | `DeviceDisconnectedPayload` | USB device removed |
| `execute:progress` | `ExecuteProgressPayload` | Execution progress |
| `execute:completed` | `ExecuteCompletedPayload` | Execution finished |
| `execute:failed` | `ExecuteFailedPayload` | Execution failed |
| `execute:telemetry` | `ExecuteTelemetryPayload` | Telemetry sample |
| `evidence:ready` | `EvidenceReadyPayload` | Evidence bundle created |
| `evidence:uploadComplete` | `EvidenceUploadCompletePayload` | Evidence uploaded |
| `evidence:uploadFailed` | `EvidenceUploadFailedPayload` | Upload failed |
| `system:heartbeat` | `SystemHeartbeatPayload` | Utility alive (15s) |
| `system:log` | `SystemLogPayload` | Utility log |
| `message` | `BridgeMessage` | Raw incoming message |

## API Reference

### `new KitZeroBridge(options)`

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `workspaceId` | `string` | **required** | Workspace ID |
| `token` | `string` | **required** | Auth token (JWT) |
| `baseUrl` | `string` | `'http://localhost:9321'` | Utility base URL |
| `webAppVersion` | `string` | — | Web app version string |
| `sessionId` | `string` | auto-generated | Session identifier |
| `requestTimeoutMs` | `number` | `30000` | Request timeout |
| `autoReconnect` | `boolean` | `true` | Auto-reconnect on disconnect |
| `maxReconnectAttempts` | `number` | `Infinity` | Max reconnect attempts |
| `reconnectBaseDelayMs` | `number` | `1000` | Initial backoff delay |
| `reconnectMaxDelayMs` | `number` | `30000` | Max backoff delay |

### Methods

| Method | Returns | Description |
|--------|---------|-------------|
| `connect()` | `Promise<AuthHandshakeAckPayload>` | Connect and authenticate |
| `disconnect()` | `void` | Disconnect |
| `listDevices()` | `Promise<DeviceInfo[]>` | List connected devices |
| `listPlugins()` | `Promise<PluginInfo[]>` | List installed plugins |
| `executeCapability(params)` | `Promise<CapabilityExecutionHandle>` | Execute a capability |
| `getEvidence(bundleId)` | `Promise<EvidenceGetResponsePayload>` | Retrieve evidence bundle |
| `uploadFile(data, filename, mimeType?)` | `Promise<string>` | Upload file, returns fileId |
| `installPlugin(params)` | `Promise<InstallPluginResultPayload>` | Install plugin |
| `uninstallPlugin(pluginId)` | `Promise<UninstallPluginResultPayload>` | Uninstall plugin |
| `syncPlugins(required)` | `Promise<InstallPluginResultPayload[]>` | Ensure plugins installed |

### Properties

| Property | Type | Description |
|----------|------|-------------|
| `connectionState` | `ConnectionState` | `'disconnected' \| 'connecting' \| 'authenticating' \| 'connected'` |
| `isConnected` | `boolean` | True when state is `'connected'` |
| `utilityInfo` | `AuthHandshakeAckPayload \| null` | Utility info after auth |

### `CapabilityExecutionHandle`

Returned by `executeCapability()`:

| Property/Method | Type | Description |
|-----------------|------|-------------|
| `runId` | `string` | The execution run ID |
| `result` | `Promise<ExecuteCompletedPayload>` | Resolves on completion, rejects on failure |
| `onProgress(listener)` | `() => void` | Subscribe to progress, returns unsubscribe |
| `onTelemetry(listener)` | `() => void` | Subscribe to telemetry samples |
| `onEvidenceReady(listener)` | `() => void` | Subscribe to evidence ready |
| `stop(outcome, reason?)` | `void` | Send stop signal (for monitors) |
| `isDone` | `boolean` | True when execution finished |

## Error Handling

```typescript
import {
  BridgeAuthError,
  BridgeTimeoutError,
  BridgeDisconnectedError,
} from '@kitzero/bridge-client';

try {
  await bridge.connect();
} catch (err) {
  if (err instanceof BridgeAuthError) {
    console.error('Auth failed — check token');
  } else if (err instanceof BridgeTimeoutError) {
    console.error('Connection timed out');
  } else if (err instanceof BridgeDisconnectedError) {
    console.error('Utility not running');
  }
}
```

## License

Proprietary - KitZero