npm package: @nority/bridge-sdk
Description: Public SDK for building Nority bridge implementations
Latest version: 0.3.0

# @nority/bridge-sdk

TypeScript SDK for building Nority bridge implementations. Works with Node.js and Bun.

A **bridge** connects an external agent runtime (e.g., OpenClaw, LangChain, custom LLM pipelines) to the Nority platform so that conversations flow between the agent and Nority users — with end-to-end encryption, automatic reconnection, and replay.

## Who is this for?

Third-party developers who want to build a bridge for their own agent runtime. You implement a single `AgentAdapter` interface; the SDK handles everything else:

- QR/deeplink pairing flow
- Bridge-scoped credential storage and Ed25519 challenge-response auth
- Authenticated WebSocket transport with jittered backoff reconnect
- Per-conversation X25519 key exchange and AES-256-GCM encryption
- Replay and deterministic history rebuild after reconnect
- Ping/pong heartbeat and close-code-aware lifecycle

## Installation

```bash
npm install @nority/bridge-sdk
```

<details>
<summary>Other package managers</summary>

```bash
pnpm add @nority/bridge-sdk
yarn add @nority/bridge-sdk
bun add @nority/bridge-sdk
```

</details>

## Quick start

### 1. Implement `AgentAdapter`

The adapter is the only code you write. It receives decrypted user messages and yields streamed agent events:

```ts
import type { AgentAdapter, AgentEvent, InvokeRequest } from "@nority/bridge-sdk";

class MyAdapter implements AgentAdapter {
  async *invokeAgent(
    signal: AbortSignal,
    req: InvokeRequest,
  ): AsyncIterable<AgentEvent> {
    // Call your agent runtime with req.inputs and req.history
    const response = await myAgent.run(req.inputs, { signal });

    for await (const chunk of response) {
      yield { type: "text_delta", content: chunk.text };
    }

    yield { type: "run_status", runStatus: "completed" };
    yield { type: "done" };
  }
}
```

### 2. Start the bridge

```ts
import { NorityBridge } from "@nority/bridge-sdk";

const bridge = new NorityBridge({
  backendUrl: "wss://api.nority.ai/v1/bridge",
  dataDir: "./bridge-data",
  agent: new MyAdapter(),
  onPairingInfo: (info) => {
    console.log(`Pair: ${info.deeplinkUrl} (expires ${info.expiresAt})`);
  },
});

await bridge.start();
// Bridge is now connected and handling conversations
```

On first run, the SDK generates an Ed25519 identity, enters the pairing flow, and calls `onPairingInfo` with a QR/deeplink URL. `start()` blocks until the user approves in the Nority app and the bridge connects.

On subsequent runs, the SDK loads the stored credential, mints a fresh JWT via challenge-response, reconnects, replays missed events, and resumes normal operation.

## Layered API

The SDK exposes three levels of abstraction. Most developers only need `NorityBridge`.

### `NorityBridge` — managed (recommended)

Handles everything: pairing, credential persistence, auth, transport, conversations. `start()` blocks until fully connected regardless of whether pairing was needed.

```ts
import { NorityBridge } from "@nority/bridge-sdk";

const bridge = new NorityBridge({
  backendUrl: "wss://api.nority.ai/v1/bridge",
  dataDir: "./bridge-data",
  agent: myAdapter,
  onPairingInfo: (info) => console.log(`Scan: ${info.deeplinkUrl}`),
});

await bridge.start();   // blocks until connected
await bridge.stop();    // graceful shutdown
```

### `PairingClient` — standalone pairing

Use this when you want to control the pairing ceremony yourself — embed the QR in your own UI, store the credential in your own database, or skip pairing entirely with pre-provisioned credentials.

```ts
import { PairingClient } from "@nority/bridge-sdk";

const pairing = new PairingClient({
  backendUrl: "wss://api.nority.ai/v1/bridge",
  dataDir: "./bridge-data",
  onPairingInfo: (info) => renderQRCode(info.deeplinkUrl),
});

const result = await pairing.pair();  // blocks until user approves
// result.credential — durable bridge credential
// result.identity   — Ed25519 keypair
// result.initialToken — first JWT (pass as seedJwt to BridgeConnection)
```

**Methods:**
- `pair()` — performs the pairing ceremony. Resolves when the user approves. Single-flight (throws if called while already pairing).
- `cancel()` — cancels in-progress pairing. Rejects the pending `pair()` promise. Idempotent.
- `regenerateToken()` — closes the WebSocket, opens a new one, sends a fresh `pair_request`. Returns updated `PairingInfo`.
- `getPairingInfo()` — returns current pairing info or `null`.

### `BridgeConnection` — standalone connection

Use this when you already have a credential (pre-provisioned from a dashboard, stored in your own secret manager, or obtained via `PairingClient`).

```ts
import { BridgeConnection } from "@nority/bridge-sdk";

const connection = new BridgeConnection({
  backendUrl: "wss://api.nority.ai/v1/bridge",
  dataDir: "./bridge-data",
  credential: process.env.BRIDGE_CREDENTIAL,
  privateKeyPem: fs.readFileSync("./bridge-key.pem", "utf-8"),
  agent: myAdapter,
  seedJwt: cachedToken,  // optional — skip first challenge-response
});

await connection.connect();    // blocks until connected
await connection.disconnect(); // graceful shutdown
```

**Methods:**
- `connect()` — authenticates via challenge-response, opens authenticated WebSocket, sends replay request. Resolves when connected.
- `disconnect()` — closes connection, stops reconnection, aborts active agent invocations. Idempotent.
- `getState()` — returns current state (`idle | connecting | connected | reconnecting | blocked | failed | stopped`).
- `createProactiveTurnFrame(conversationId, request)` — creates an `assistant_turn_start` frame for bridge-initiated turns.

## Key concepts

| Concept | Description |
|---------|-------------|
| **Bridge credential** | Long-lived secret stored locally. Never the user's JWT. |
| **Bridge access JWT** | Short-lived token for WebSocket auth. Cached ephemerally. |
| **Pairing token** | 120-second single-use token displayed as QR/deeplink. Auto-regenerates. |
| **Conversation key** | Per-conversation AES-256-GCM symmetric key derived via X25519 ECDH + HKDF-SHA3-256. |
| **Replay** | After reconnect, the SDK requests missed events and rebuilds conversation history deterministically. |

## Agent event types

Your adapter yields `AgentEvent` values. The SDK encrypts and forwards them to the backend:

| Type | Purpose |
|------|---------|
| `text_delta` | Streamed text content |
| `tool_call_start` | Tool invocation started |
| `tool_call_update` | Partial tool result |
| `tool_result` | Final tool result |
| `thinking_start` | Reasoning block started |
| `thinking_delta` | Streamed reasoning content |
| `thinking_complete` | Reasoning block finished |
| `run_status` | Agent lifecycle (`started`, `completed`, `failed`) |
| `media` | Media reference (multimodal-safe) |
| `error` | Agent-reported error |
| `done` | Terminal event — must be the last event yielded |

## Content model

The SDK uses a multimodal-safe content model:

```ts
type ContentPart =
  | { kind: "text"; text: string }
  | { kind: "media"; media: MediaReference };

interface ChatTurn {
  role: "user" | "assistant";
  parts: ContentPart[];
}

interface InvokeRequest {
  conversationId: string;
  inputs: ContentPart[];    // Current user turn
  history: ChatTurn[];      // Previous conversation turns
}
```

## Bridge lifecycle states

```
idle → pairing → connecting → connected
                     ↕
              reconnecting → blocked → failed
                                  ↓
                               stopped
```

## Configuration

### `BridgeConfig` (for `NorityBridge`)

```ts
interface BridgeConfig {
  backendUrl: string;                          // Nority backend WebSocket URL
  dataDir: string;                             // Directory for durable state (identity, keys)
  agent: AgentAdapter;                         // Your adapter implementation
  onPairingInfo?: (pairing: PairingInfo) => void; // Called when pairing token is ready
  logger?: BridgeLogger;                       // Structured logging
  webSocketFactory?: WebSocketFactory;         // Override the built-in ws-based WebSocket
  fetchImplementation?: typeof fetch;          // Custom fetch (for auth challenge-response)
  clock?: () => Date;                          // Custom clock (for testing)
  pairingTokenFactory?: () => string;          // Custom pairing token generator
  backoffRandom?: () => number;                // Custom RNG for reconnect backoff jitter
  heartbeatIntervalMs?: number;                // Default: 15000
  idleTimeoutMs?: number;                      // Default: 45000
}
```

### `PairingClientConfig`

```ts
interface PairingClientConfig {
  backendUrl: string;
  dataDir: string;
  onPairingInfo?: (info: PairingInfo) => void;
  logger?: BridgeLogger;
  webSocketFactory?: WebSocketFactory;         // Override the built-in ws-based WebSocket
  clock?: () => Date;
  pairingTokenFactory?: () => string;
}
```

### `BridgeConnectionConfig`

```ts
interface BridgeConnectionConfig {
  backendUrl: string;
  dataDir: string;
  credential: string;
  privateKeyPem: string;
  agent: AgentAdapter;
  logger?: BridgeLogger;
  seedJwt?: AccessTokenRecord;
  fetchImplementation?: typeof fetch;
  webSocketFactory?: WebSocketFactory;         // Override the built-in ws-based WebSocket
  clock?: () => Date;
  backoffRandom?: () => number;
  heartbeatIntervalMs?: number;
  idleTimeoutMs?: number;
}
```

## Requirements

- **Node.js** >= 20 or **Bun** >= 1.0
- The adapter-to-agent transport is entirely provider-specific — the SDK makes no assumptions about how your adapter talks to its agent runtime

## License

MIT