@nority/bridge-sdk 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -68,18 +68,92 @@ const bridge = new NorityBridge({
68
68
  backendUrl: "wss://api.nority.ai/v1/bridge",
69
69
  dataDir: "./bridge-data",
70
70
  agent: new MyAdapter(),
71
- onPairingToken: (info) => {
71
+ onPairingInfo: (info) => {
72
72
  console.log(`Pair: ${info.deeplinkUrl} (expires ${info.expiresAt})`);
73
73
  },
74
74
  });
75
75
 
76
76
  await bridge.start();
77
+ // Bridge is now connected and handling conversations
77
78
  ```
78
79
 
79
- On first run, the SDK generates an Ed25519 identity, enters the pairing flow, and calls `onPairingToken` with a QR/deeplink URL. After the user approves in the Nority app, the bridge receives a durable credential and connects automatically.
80
+ 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.
80
81
 
81
82
  On subsequent runs, the SDK loads the stored credential, mints a fresh JWT via challenge-response, reconnects, replays missed events, and resumes normal operation.
82
83
 
84
+ ## Layered API
85
+
86
+ The SDK exposes three levels of abstraction. Most developers only need `NorityBridge`.
87
+
88
+ ### `NorityBridge` — managed (recommended)
89
+
90
+ Handles everything: pairing, credential persistence, auth, transport, conversations. `start()` blocks until fully connected regardless of whether pairing was needed.
91
+
92
+ ```ts
93
+ import { NorityBridge } from "@nority/bridge-sdk";
94
+
95
+ const bridge = new NorityBridge({
96
+ backendUrl: "wss://api.nority.ai/v1/bridge",
97
+ dataDir: "./bridge-data",
98
+ agent: myAdapter,
99
+ onPairingInfo: (info) => console.log(`Scan: ${info.deeplinkUrl}`),
100
+ });
101
+
102
+ await bridge.start(); // blocks until connected
103
+ await bridge.stop(); // graceful shutdown
104
+ ```
105
+
106
+ ### `PairingClient` — standalone pairing
107
+
108
+ 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.
109
+
110
+ ```ts
111
+ import { PairingClient } from "@nority/bridge-sdk";
112
+
113
+ const pairing = new PairingClient({
114
+ backendUrl: "wss://api.nority.ai/v1/bridge",
115
+ dataDir: "./bridge-data",
116
+ onPairingInfo: (info) => renderQRCode(info.deeplinkUrl),
117
+ });
118
+
119
+ const result = await pairing.pair(); // blocks until user approves
120
+ // result.credential — durable bridge credential
121
+ // result.identity — Ed25519 keypair
122
+ // result.initialToken — first JWT (pass as seedJwt to BridgeConnection)
123
+ ```
124
+
125
+ **Methods:**
126
+ - `pair()` — performs the pairing ceremony. Resolves when the user approves. Single-flight (throws if called while already pairing).
127
+ - `cancel()` — cancels in-progress pairing. Rejects the pending `pair()` promise. Idempotent.
128
+ - `regenerateToken()` — closes the WebSocket, opens a new one, sends a fresh `pair_request`. Returns updated `PairingInfo`.
129
+ - `getPairingInfo()` — returns current pairing info or `null`.
130
+
131
+ ### `BridgeConnection` — standalone connection
132
+
133
+ Use this when you already have a credential (pre-provisioned from a dashboard, stored in your own secret manager, or obtained via `PairingClient`).
134
+
135
+ ```ts
136
+ import { BridgeConnection } from "@nority/bridge-sdk";
137
+
138
+ const connection = new BridgeConnection({
139
+ backendUrl: "wss://api.nority.ai/v1/bridge",
140
+ dataDir: "./bridge-data",
141
+ credential: process.env.BRIDGE_CREDENTIAL,
142
+ privateKeyPem: fs.readFileSync("./bridge-key.pem", "utf-8"),
143
+ agent: myAdapter,
144
+ seedJwt: cachedToken, // optional — skip first challenge-response
145
+ });
146
+
147
+ await connection.connect(); // blocks until connected
148
+ await connection.disconnect(); // graceful shutdown
149
+ ```
150
+
151
+ **Methods:**
152
+ - `connect()` — authenticates via challenge-response, opens authenticated WebSocket, sends replay request. Resolves when connected.
153
+ - `disconnect()` — closes connection, stops reconnection, aborts active agent invocations. Idempotent.
154
+ - `getState()` — returns current state (`idle | connecting | connected | reconnecting | blocked | failed | stopped`).
155
+ - `createProactiveTurnFrame(conversationId, request)` — creates an `assistant_turn_start` frame for bridge-initiated turns.
156
+
83
157
  ## Key concepts
84
158
 
85
159
  | Concept | Description |
@@ -141,16 +215,56 @@ idle → pairing → connecting → connected
141
215
 
142
216
  ## Configuration
143
217
 
218
+ ### `BridgeConfig` (for `NorityBridge`)
219
+
144
220
  ```ts
145
221
  interface BridgeConfig {
146
- backendUrl: string; // Nority backend WebSocket URL
147
- dataDir: string; // Directory for durable state (identity, keys)
148
- agent: AgentAdapter; // Your adapter implementation
149
- onPairingToken?: (info) => void; // Called when pairing token is ready
150
- logger?: BridgeLogger; // Structured logging
151
- webSocketFactory?: WebSocketFactory; // Custom WebSocket creation
152
- heartbeatIntervalMs?: number; // Default: 15000
153
- idleTimeoutMs?: number; // Default: 45000
222
+ backendUrl: string; // Nority backend WebSocket URL
223
+ dataDir: string; // Directory for durable state (identity, keys)
224
+ agent: AgentAdapter; // Your adapter implementation
225
+ onPairingInfo?: (pairing: PairingInfo) => void; // Called when pairing token is ready
226
+ logger?: BridgeLogger; // Structured logging
227
+ webSocketFactory?: WebSocketFactory; // Custom WebSocket creation
228
+ fetchImplementation?: typeof fetch; // Custom fetch (for auth challenge-response)
229
+ clock?: () => Date; // Custom clock (for testing)
230
+ pairingTokenFactory?: () => string; // Custom pairing token generator
231
+ backoffRandom?: () => number; // Custom RNG for reconnect backoff jitter
232
+ heartbeatIntervalMs?: number; // Default: 15000
233
+ idleTimeoutMs?: number; // Default: 45000
234
+ }
235
+ ```
236
+
237
+ ### `PairingClientConfig`
238
+
239
+ ```ts
240
+ interface PairingClientConfig {
241
+ backendUrl: string;
242
+ dataDir: string;
243
+ onPairingInfo?: (info: PairingInfo) => void;
244
+ logger?: BridgeLogger;
245
+ webSocketFactory?: WebSocketFactory;
246
+ clock?: () => Date;
247
+ pairingTokenFactory?: () => string;
248
+ }
249
+ ```
250
+
251
+ ### `BridgeConnectionConfig`
252
+
253
+ ```ts
254
+ interface BridgeConnectionConfig {
255
+ backendUrl: string;
256
+ dataDir: string;
257
+ credential: string;
258
+ privateKeyPem: string;
259
+ agent: AgentAdapter;
260
+ logger?: BridgeLogger;
261
+ seedJwt?: AccessTokenRecord;
262
+ fetchImplementation?: typeof fetch;
263
+ webSocketFactory?: WebSocketFactory;
264
+ clock?: () => Date;
265
+ backoffRandom?: () => number;
266
+ heartbeatIntervalMs?: number;
267
+ idleTimeoutMs?: number;
154
268
  }
155
269
  ```
156
270
 
package/dist/agent.d.ts CHANGED
@@ -88,7 +88,7 @@ export interface BridgeConfig {
88
88
  backendUrl: string;
89
89
  dataDir: string;
90
90
  agent: AgentAdapter;
91
- onPairingToken?: (pairing: PairingInfo) => void;
91
+ onPairingInfo?: (pairing: PairingInfo) => void;
92
92
  logger?: BridgeLogger;
93
93
  fetchImplementation?: typeof fetch;
94
94
  webSocketFactory?: WebSocketFactory;
@@ -1 +1 @@
1
- {"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../src/agent.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../src/agent.ts"],"names":[],"mappings":"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"}
package/dist/bridge.d.ts CHANGED
@@ -1,27 +1,25 @@
1
1
  import type { BridgeConfig, PairingInfo } from "./agent.js";
2
2
  import type { ProactiveTurnRequest } from "./runtime/proactive.js";
3
- import { type BridgeFrame } from "./protocol/frames.js";
3
+ import type { BridgeFrame } from "./protocol/frames.js";
4
4
  /**
5
5
  * Bridge lifecycle states.
6
6
  */
7
7
  export type BridgeState = "idle" | "pairing" | "connecting" | "connected" | "reconnecting" | "blocked" | "failed" | "stopped";
8
8
  /**
9
- * NorityBridge is the main entry point for bridge implementations.
10
- * Manages pairing, auth refresh, reconnection, and durable bridge identity.
9
+ * NorityBridge is the managed entry point for bridge implementations.
10
+ * Composes PairingClient + BridgeConnection: if no stored credential,
11
+ * runs pairing first; otherwise connects directly.
12
+ *
13
+ * start() blocks until the bridge is fully connected — regardless of
14
+ * whether pairing was needed. The onPairingInfo callback fires during
15
+ * start() if pairing is required, enabling the caller to display a QR code.
11
16
  */
12
17
  export declare class NorityBridge {
13
18
  private readonly config;
14
- private readonly logger;
15
19
  private readonly stateStore;
16
- private readonly accessTokenManager;
17
- private readonly conversationRuntime;
18
20
  private state;
19
- private durableState;
20
- private pairingInfo;
21
- private pairingTimer;
22
- private pairingSocket;
23
- private transport;
24
- private readonly pendingFrameTasks;
21
+ private pairingClient;
22
+ private connection;
25
23
  constructor(config: BridgeConfig);
26
24
  getState(): BridgeState;
27
25
  getPairingInfo(): PairingInfo | null;
@@ -29,16 +27,8 @@ export declare class NorityBridge {
29
27
  start(): Promise<void>;
30
28
  stop(): Promise<void>;
31
29
  regeneratePairingToken(): Promise<PairingInfo>;
32
- private beginPairingFlow;
33
- private restartPairingFlow;
34
- private handlePairingMessage;
35
- private ensureAuthenticatedTransport;
36
- private handleTransportStateChange;
37
- private getDurableState;
38
- private clearPairingTimer;
30
+ private connectWithCredential;
31
+ private syncConnectionState;
39
32
  private setState;
40
- private handleTransportFrame;
41
- private trackFrameTask;
42
- private sendTransportFrame;
43
33
  }
44
34
  //# sourceMappingURL=bridge.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"bridge.d.ts","sourceRoot":"","sources":["../src/bridge.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAgB,WAAW,EAAE,MAAM,YAAY,CAAC;AAW1E,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAMnE,OAAO,EAAe,KAAK,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAGrE;;GAEG;AACH,MAAM,MAAM,WAAW,GACnB,MAAM,GACN,SAAS,GACT,YAAY,GACZ,WAAW,GACX,cAAc,GACd,SAAS,GACT,QAAQ,GACR,SAAS,CAAC;AASd;;;GAGG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAe;IACtC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAe;IACtC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAuB;IAClD,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAA2B;IAC9D,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAsB;IAC1D,OAAO,CAAC,KAAK,CAAuB;IACpC,OAAO,CAAC,YAAY,CAAkC;IACtD,OAAO,CAAC,WAAW,CAA4B;IAC/C,OAAO,CAAC,YAAY,CAA8C;IAClE,OAAO,CAAC,aAAa,CAA8B;IACnD,OAAO,CAAC,SAAS,CAAgD;IACjE,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAA4B;gBAElD,MAAM,EAAE,YAAY;IAkBhC,QAAQ,IAAI,WAAW;IAIvB,cAAc,IAAI,WAAW,GAAG,IAAI;IAIpC,wBAAwB,CACtB,cAAc,EAAE,MAAM,EACtB,OAAO,EAAE,oBAAoB,GAC5B,WAAW,CAAC,sBAAsB,CAAC;IAOhC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAiBtB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAoBrB,sBAAsB,IAAI,OAAO,CAAC,WAAW,CAAC;YAatC,gBAAgB;YAKhB,kBAAkB;YAkDlB,oBAAoB;YAuDpB,4BAA4B;IAqC1C,OAAO,CAAC,0BAA0B;IAqClC,OAAO,CAAC,eAAe;IAOvB,OAAO,CAAC,iBAAiB;IAOzB,OAAO,CAAC,QAAQ;YAIF,oBAAoB;YAYpB,cAAc;IAS5B,OAAO,CAAC,kBAAkB;CAQ3B"}
1
+ {"version":3,"file":"bridge.d.ts","sourceRoot":"","sources":["../src/bridge.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAM5D,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AACnE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAExD;;GAEG;AACH,MAAM,MAAM,WAAW,GACnB,MAAM,GACN,SAAS,GACT,YAAY,GACZ,WAAW,GACX,cAAc,GACd,SAAS,GACT,QAAQ,GACR,SAAS,CAAC;AAEd;;;;;;;;GAQG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAe;IACtC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAuB;IAClD,OAAO,CAAC,KAAK,CAAuB;IACpC,OAAO,CAAC,aAAa,CAA8B;IACnD,OAAO,CAAC,UAAU,CAAiC;gBAEvC,MAAM,EAAE,YAAY;IAMhC,QAAQ,IAAI,WAAW;IAIvB,cAAc,IAAI,WAAW,GAAG,IAAI;IAIpC,wBAAwB,CACtB,cAAc,EAAE,MAAM,EACtB,OAAO,EAAE,oBAAoB,GAC5B,WAAW,CAAC,sBAAsB,CAAC;IAShC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAwCtB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAcrB,sBAAsB,IAAI,OAAO,CAAC,WAAW,CAAC;YAStC,qBAAqB;IA4BnC,OAAO,CAAC,mBAAmB;IAW3B,OAAO,CAAC,QAAQ;CAGjB"}
package/dist/bridge.js CHANGED
@@ -1,311 +1,145 @@
1
1
  import { validateConfig } from "./config.js";
2
- import { createPairingExpiry, createPairingToken, createDeeplinkUrl, PAIRING_TOKEN_TTL_SECONDS, } from "./auth/pairing.js";
3
- import { BridgeAccessTokenManager } from "./auth/reconnect.js";
2
+ import { PairingClient } from "./pairing/client.js";
3
+ import { BridgeConnection } from "./connection.js";
4
4
  import { FileBridgeStateStore } from "./storage/state-store.js";
5
- import { ConversationRuntime } from "./conversation/runtime.js";
6
- import { AuthenticatedWebSocketTransport, } from "./transport/websocket.js";
7
- import { createFrame } from "./protocol/frames.js";
8
- import { getEd25519PublicKeyBase64 } from "./crypto/identity.js";
9
- const noopLogger = {
10
- debug: () => { },
11
- info: () => { },
12
- warn: () => { },
13
- error: () => { },
14
- };
15
5
  /**
16
- * NorityBridge is the main entry point for bridge implementations.
17
- * Manages pairing, auth refresh, reconnection, and durable bridge identity.
6
+ * NorityBridge is the managed entry point for bridge implementations.
7
+ * Composes PairingClient + BridgeConnection: if no stored credential,
8
+ * runs pairing first; otherwise connects directly.
9
+ *
10
+ * start() blocks until the bridge is fully connected — regardless of
11
+ * whether pairing was needed. The onPairingInfo callback fires during
12
+ * start() if pairing is required, enabling the caller to display a QR code.
18
13
  */
19
14
  export class NorityBridge {
20
15
  config;
21
- logger;
22
16
  stateStore;
23
- accessTokenManager;
24
- conversationRuntime;
25
17
  state = "idle";
26
- durableState = null;
27
- pairingInfo = null;
28
- pairingTimer = null;
29
- pairingSocket = null;
30
- transport = null;
31
- pendingFrameTasks = new Set();
18
+ pairingClient = null;
19
+ connection = null;
32
20
  constructor(config) {
33
21
  validateConfig(config);
34
22
  this.config = config;
35
- this.logger = config.logger ?? noopLogger;
36
23
  this.stateStore = new FileBridgeStateStore(config.dataDir);
37
- this.accessTokenManager = new BridgeAccessTokenManager({
38
- backendUrl: config.backendUrl,
39
- fetchImplementation: config.fetchImplementation,
40
- clock: config.clock,
41
- });
42
- this.conversationRuntime = new ConversationRuntime({
43
- dataDir: config.dataDir,
44
- agent: config.agent,
45
- logger: this.logger,
46
- clock: config.clock,
47
- });
48
24
  }
49
25
  getState() {
50
26
  return this.state;
51
27
  }
52
28
  getPairingInfo() {
53
- return this.pairingInfo === null ? null : { ...this.pairingInfo };
29
+ return this.pairingClient?.getPairingInfo() ?? null;
54
30
  }
55
31
  createProactiveTurnFrame(conversationId, request) {
56
- return this.conversationRuntime.createProactiveTurnFrame(conversationId, request);
32
+ if (this.connection === null) {
33
+ throw new Error("NorityBridge.createProactiveTurnFrame: connection must exist");
34
+ }
35
+ return this.connection.createProactiveTurnFrame(conversationId, request);
57
36
  }
58
37
  async start() {
59
38
  if (this.state !== "idle" && this.state !== "stopped") {
60
39
  throw new Error(`NorityBridge.start: cannot start bridge in state ${this.state}`);
61
40
  }
62
- this.durableState = await this.stateStore.loadOrCreateIdentity();
63
- await this.conversationRuntime.loadPersistedConversations();
64
- if (!this.durableState.bridgeCredential) {
41
+ const storedState = await this.stateStore.loadOrCreateIdentity();
42
+ if (storedState.bridgeCredential) {
43
+ await this.connectWithCredential(storedState);
44
+ }
45
+ else {
65
46
  this.setState("pairing");
66
- await this.beginPairingFlow();
67
- return;
47
+ this.pairingClient = new PairingClient({
48
+ backendUrl: this.config.backendUrl,
49
+ dataDir: this.config.dataDir,
50
+ onPairingInfo: this.config.onPairingInfo,
51
+ logger: this.config.logger,
52
+ webSocketFactory: this.config.webSocketFactory,
53
+ clock: this.config.clock,
54
+ pairingTokenFactory: this.config.pairingTokenFactory,
55
+ });
56
+ const result = await this.pairingClient.pair();
57
+ this.pairingClient = null;
58
+ await this.stateStore.save({
59
+ agentId: result.agentId,
60
+ bridgeCredential: result.credential,
61
+ privateKeyPem: result.identity.privateKeyPem,
62
+ publicKeyPem: result.identity.publicKeyPem,
63
+ });
64
+ await this.connectWithCredential({
65
+ agentId: result.agentId,
66
+ bridgeCredential: result.credential,
67
+ privateKeyPem: result.identity.privateKeyPem,
68
+ publicKeyPem: result.identity.publicKeyPem,
69
+ }, result.initialToken);
68
70
  }
69
- await this.ensureAuthenticatedTransport(false);
70
71
  }
71
72
  async stop() {
72
- this.clearPairingTimer();
73
- const pairingSocket = this.pairingSocket;
74
- this.pairingSocket = null;
75
- if (pairingSocket !== null) {
76
- pairingSocket.close(1000, "pairing stopped");
73
+ if (this.pairingClient !== null) {
74
+ this.pairingClient.cancel();
75
+ this.pairingClient = null;
77
76
  }
78
- this.conversationRuntime.stop();
79
- if (this.transport !== null) {
80
- await this.transport.disconnect();
81
- this.transport = null;
77
+ if (this.connection !== null) {
78
+ await this.connection.disconnect();
79
+ this.connection = null;
82
80
  }
83
- await Promise.allSettled(Array.from(this.pendingFrameTasks));
84
81
  this.setState("stopped");
85
82
  }
86
83
  async regeneratePairingToken() {
87
- if (this.state !== "pairing") {
84
+ if (this.state !== "pairing" || this.pairingClient === null) {
88
85
  throw new Error(`NorityBridge.regeneratePairingToken: bridge must be in pairing state, got ${this.state}`);
89
86
  }
90
- await this.restartPairingFlow();
91
- if (this.pairingInfo === null) {
92
- throw new Error("NorityBridge.regeneratePairingToken: pairingInfo must exist");
93
- }
94
- return { ...this.pairingInfo };
95
- }
96
- async beginPairingFlow() {
97
- const currentState = this.getDurableState();
98
- await this.restartPairingFlow(currentState);
99
- }
100
- async restartPairingFlow(state = this.getDurableState()) {
101
- const existingSocket = this.pairingSocket;
102
- this.pairingSocket = null;
103
- if (existingSocket !== null) {
104
- existingSocket.close(1000, "pairing token rotated");
105
- }
106
- this.clearPairingTimer();
107
- const token = this.config.pairingTokenFactory?.() ?? createPairingToken();
108
- if (!token) {
109
- throw new Error("NorityBridge.restartPairingFlow: pairing token must not be empty");
110
- }
111
- this.pairingInfo = {
112
- token,
113
- deeplinkUrl: createDeeplinkUrl(token),
114
- expiresAt: createPairingExpiry({
115
- clock: this.config.clock,
116
- tokenTtlSeconds: PAIRING_TOKEN_TTL_SECONDS,
117
- }),
118
- };
119
- this.config.onPairingToken?.({ ...this.pairingInfo });
120
- this.pairingTimer = setTimeout(() => {
121
- void this.restartPairingFlow(state);
122
- }, PAIRING_TOKEN_TTL_SECONDS * 1000);
123
- const webSocketFactory = this.config.webSocketFactory;
124
- if (webSocketFactory === undefined) {
125
- throw new Error("NorityBridge.restartPairingFlow: webSocketFactory is required for pairing");
126
- }
127
- const socket = webSocketFactory.create(this.config.backendUrl, {});
128
- this.pairingSocket = socket;
129
- await waitForOpen(socket);
130
- socket.addEventListener("message", (event) => {
131
- void this.handlePairingMessage(event.data);
87
+ return this.pairingClient.regenerateToken();
88
+ }
89
+ async connectWithCredential(storedState, seedJwt) {
90
+ if (!storedState.bridgeCredential) {
91
+ throw new Error("NorityBridge: bridgeCredential is required");
92
+ }
93
+ this.connection = new BridgeConnection({
94
+ backendUrl: this.config.backendUrl,
95
+ dataDir: this.config.dataDir,
96
+ credential: storedState.bridgeCredential,
97
+ privateKeyPem: storedState.privateKeyPem,
98
+ agent: this.config.agent,
99
+ seedJwt,
100
+ logger: this.config.logger,
101
+ fetchImplementation: this.config.fetchImplementation,
102
+ webSocketFactory: this.config.webSocketFactory,
103
+ clock: this.config.clock,
104
+ backoffRandom: this.config.backoffRandom,
105
+ heartbeatIntervalMs: this.config.heartbeatIntervalMs,
106
+ idleTimeoutMs: this.config.idleTimeoutMs,
132
107
  });
133
- const pairRequest = createFrame("pair_request", {
134
- pairing_token: token,
135
- bridge_public_key: getEd25519PublicKeyBase64(state.publicKeyPem),
136
- });
137
- socket.send(JSON.stringify(pairRequest));
108
+ await this.connection.connect();
109
+ this.syncConnectionState();
138
110
  }
139
- async handlePairingMessage(rawData) {
140
- let frame;
141
- try {
142
- frame = JSON.parse(rawData);
143
- }
144
- catch (error) {
145
- this.logger.warn("NorityBridge.handlePairingMessage: invalid JSON", error);
111
+ syncConnectionState() {
112
+ if (this.connection === null) {
146
113
  return;
147
114
  }
148
- if (frame.type !== "paired") {
149
- return;
150
- }
151
- const payload = frame.payload;
152
- if (!payload.agent_id ||
153
- !payload.bridge_credential ||
154
- !payload.access_jwt ||
155
- !payload.expires_at) {
156
- throw new Error("NorityBridge.handlePairingMessage: paired payload missing required fields");
157
- }
158
- const currentState = this.getDurableState();
159
- const nextState = {
160
- ...currentState,
161
- agentId: payload.agent_id,
162
- bridgeCredential: payload.bridge_credential,
163
- };
164
- await this.stateStore.save(nextState);
165
- this.durableState = nextState;
166
- this.accessTokenManager.seed({
167
- accessJwt: payload.access_jwt,
168
- agentId: payload.agent_id,
169
- expiresAt: payload.expires_at,
170
- });
171
- this.clearPairingTimer();
172
- const pairingSocket = this.pairingSocket;
173
- this.pairingSocket = null;
174
- if (pairingSocket !== null) {
175
- pairingSocket.close(1000, "pairing completed");
176
- }
177
- await this.ensureAuthenticatedTransport(false);
178
- }
179
- async ensureAuthenticatedTransport(forceRefresh) {
180
- const currentState = this.getDurableState();
181
- if (!currentState.bridgeCredential) {
182
- throw new Error("NorityBridge.ensureAuthenticatedTransport: bridgeCredential is required");
183
- }
184
- if (this.transport === null) {
185
- this.transport = new AuthenticatedWebSocketTransport({
186
- backendUrl: this.config.backendUrl,
187
- webSocketFactory: this.config.webSocketFactory,
188
- heartbeatIntervalMs: this.config.heartbeatIntervalMs,
189
- idleTimeoutMs: this.config.idleTimeoutMs,
190
- random: this.config.backoffRandom,
191
- clock: this.config.clock,
192
- logger: this.logger,
193
- onFrame: async (frame) => {
194
- await this.trackFrameTask(this.handleTransportFrame(frame));
195
- },
196
- onStateChange: (transportState) => {
197
- this.handleTransportStateChange(transportState);
198
- },
199
- getAccessToken: async (refreshRequested = false) => {
200
- const token = await this.accessTokenManager.getAccessToken({
201
- credential: currentState.bridgeCredential ?? "",
202
- privateKeyPem: currentState.privateKeyPem,
203
- forceRefresh: refreshRequested,
204
- });
205
- return token.accessJwt;
206
- },
207
- });
208
- }
209
- await this.transport.connect(forceRefresh);
210
- }
211
- handleTransportStateChange(transportState) {
212
- switch (transportState) {
213
- case "connecting":
214
- this.setState("connecting");
215
- break;
216
- case "connected":
217
- this.setState("connected");
218
- if (this.transport !== null) {
219
- const replayRequest = this.conversationRuntime.createReplayRequestFrame();
220
- if (replayRequest !== null) {
221
- this.transport.send(replayRequest);
222
- }
223
- }
224
- break;
225
- case "reconnecting":
226
- this.setState("reconnecting");
227
- break;
228
- case "blocked":
229
- this.setState("blocked");
230
- break;
231
- case "failed":
232
- this.setState("failed");
233
- break;
234
- case "stopped":
235
- if (this.state !== "stopped") {
236
- this.setState("stopped");
237
- }
238
- break;
239
- case "idle":
240
- break;
241
- default:
242
- throw new Error(`NorityBridge.handleTransportStateChange: unsupported transport state ${transportState}`);
243
- }
244
- }
245
- getDurableState() {
246
- if (this.durableState === null) {
247
- throw new Error("NorityBridge: durable bridge state has not been loaded");
248
- }
249
- return this.durableState;
250
- }
251
- clearPairingTimer() {
252
- if (this.pairingTimer !== null) {
253
- clearTimeout(this.pairingTimer);
254
- this.pairingTimer = null;
115
+ const connState = this.connection.getState();
116
+ const mapped = mapConnectionState(connState);
117
+ if (mapped !== null) {
118
+ this.setState(mapped);
255
119
  }
256
120
  }
257
121
  setState(nextState) {
258
122
  this.state = nextState;
259
123
  }
260
- async handleTransportFrame(frame) {
261
- if (this.transport === null) {
262
- throw new Error("NorityBridge.handleTransportFrame: transport must exist while handling frames");
263
- }
264
- await this.conversationRuntime.handleFrame(frame, (outboundFrame) => {
265
- this.sendTransportFrame(outboundFrame);
266
- });
267
- }
268
- async trackFrameTask(task) {
269
- this.pendingFrameTasks.add(task);
270
- try {
271
- await task;
272
- }
273
- finally {
274
- this.pendingFrameTasks.delete(task);
275
- }
276
- }
277
- sendTransportFrame(outboundFrame) {
278
- if (this.transport === null) {
279
- throw new Error("NorityBridge.sendTransportFrame: transport must exist while sending outbound frames");
280
- }
281
- this.transport.send(outboundFrame);
282
- }
283
124
  }
284
- function waitForOpen(socket) {
285
- if (socket.readyState === 1) {
286
- return Promise.resolve();
125
+ function mapConnectionState(state) {
126
+ switch (state) {
127
+ case "connecting":
128
+ return "connecting";
129
+ case "connected":
130
+ return "connected";
131
+ case "reconnecting":
132
+ return "reconnecting";
133
+ case "blocked":
134
+ return "blocked";
135
+ case "failed":
136
+ return "failed";
137
+ case "stopped":
138
+ return "stopped";
139
+ case "idle":
140
+ return null;
141
+ default:
142
+ return null;
287
143
  }
288
- return new Promise((resolve, reject) => {
289
- const handleOpen = () => {
290
- cleanup();
291
- resolve();
292
- };
293
- const handleError = () => {
294
- cleanup();
295
- reject(new Error("NorityBridge.waitForOpen: websocket error during pairing"));
296
- };
297
- const handleClose = () => {
298
- cleanup();
299
- reject(new Error("NorityBridge.waitForOpen: websocket closed before pairing socket opened"));
300
- };
301
- const cleanup = () => {
302
- socket.removeEventListener("open", handleOpen);
303
- socket.removeEventListener("error", handleError);
304
- socket.removeEventListener("close", handleClose);
305
- };
306
- socket.addEventListener("open", handleOpen);
307
- socket.addEventListener("error", handleError);
308
- socket.addEventListener("close", handleClose);
309
- });
310
144
  }
311
145
  //# sourceMappingURL=bridge.js.map