@bifrost-ai/protocol 0.1.1-build.1783029473245

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 ADDED
@@ -0,0 +1,177 @@
1
+ # @bifrost-ai/protocol
2
+
3
+ Signed WebSocket RPC transport for runner ↔ orchestrator communication in Bifrost v2.
4
+
5
+ This package is the **only** wire interface between runners and the orchestrator. There is no in-process direct-call variant — a co-located runner still dials the orchestrator over WebSocket.
6
+
7
+ Design background: [docs/protocol.md](../../docs/protocol.md) · Issue [#33](https://github.com/devzeebo/bifrost/issues/33)
8
+
9
+ ## Purpose
10
+
11
+ v2 splits task execution onto remote runners. This package provides:
12
+
13
+ 1. **ed25519 signing and verification** using Node built-in `crypto`
14
+ 2. **Deterministic JSON canonicalization** so signers and verifiers agree on signing material
15
+ 3. **WebSocket frame encoding/decoding** with envelope validation
16
+ 4. **Peer abstractions** for orchestrator (server) and runner (client) roles
17
+
18
+ Cryptography is intentionally folded into this package rather than living in a separate `crypto` package.
19
+
20
+ ## Public API
21
+
22
+ ### Key management (`keys.ts`)
23
+
24
+ ```typescript
25
+ generateKeyPair(keyId?: string): PeerIdentity
26
+ loadKeyPair({ privateKeyPem, publicKeyPem, keyId }): PeerIdentity
27
+ exportPublicKeyPem(publicKey): string
28
+ exportPrivateKeyPem(privateKey): string
29
+ fingerprintPublicKey(publicKey): string // sha256(SPKI) → base64url, 16 chars
30
+ ```
31
+
32
+ `PeerIdentity` is `{ keyId, publicKey, privateKey }`. When `keyId` is omitted from `generateKeyPair`, it defaults to the public key fingerprint.
33
+
34
+ ### Signing (`sign.ts`)
35
+
36
+ ```typescript
37
+ signPayload(payload, identity, timestamp?): SignedEnvelope
38
+ verifyEnvelope(envelope, trustedPublicKeys): boolean
39
+ buildSigningMaterial(unsignedEnvelope): string
40
+ ```
41
+
42
+ `signPayload` wraps a `FramePayload` in a `SignedEnvelope`. `verifyEnvelope` checks the signature against `trustedPublicKeys.get(envelope.keyId)`. Returns `false` on any mismatch — wrong algorithm, unknown keyId, bad base64, or signature failure.
43
+
44
+ ### Canonicalization (`canonicalize.ts`)
45
+
46
+ ```typescript
47
+ canonicalize(value: unknown): string // JSON.stringify with sorted object keys
48
+ ```
49
+
50
+ Recursively sorts object keys before `JSON.stringify`. This ensures that `{ b: 1, a: 2 }` and `{ a: 2, b: 1 }` produce identical signing material. Arrays preserve element order.
51
+
52
+ Signing material is the canonical JSON of:
53
+
54
+ ```json
55
+ { "algorithm": "ed25519", "keyId": "...", "payload": { ... }, "timestamp": 1234567890 }
56
+ ```
57
+
58
+ ### Frames (`frames.ts`)
59
+
60
+ ```typescript
61
+ encodeEnvelope(envelope: SignedEnvelope): string // JSON.stringify
62
+ decodeEnvelope(raw: string): SignedEnvelope | null
63
+ isFramePayload(value: unknown): value is FramePayload
64
+ ```
65
+
66
+ `decodeEnvelope` returns `null` for malformed JSON, missing fields, or invalid payload kinds. It does not verify signatures — that happens in the connection layer.
67
+
68
+ ### Connection (`connection.ts`)
69
+
70
+ ```typescript
71
+ createProtocolConnection(socket, { identity, trustedPublicKeys, onClose? }): ProtocolConnection
72
+ toConnectedPeer(peerId, connection): ConnectedPeer
73
+ ```
74
+
75
+ `createProtocolConnection` wraps a raw `ws` WebSocket:
76
+
77
+ - **Inbound:** decode envelope → verify signature → validate payload kind → dispatch to subscribers matching their filter.
78
+ - **Outbound:** sign payload with local identity → encode envelope → `socket.send`.
79
+ - Invalid or untrusted frames are silently dropped (no error frame).
80
+ - `onClose` fires once when the socket closes or errors.
81
+
82
+ ### Peers
83
+
84
+ **Orchestrator** (`orchestrator.ts`):
85
+
86
+ ```typescript
87
+ createOrchestratorPeer({ identity, trustedPublicKeys, host?, port? }): Promise<OrchestratorPeer>
88
+ ```
89
+
90
+ - Creates a `WebSocketServer`. `port: 0` binds an ephemeral port.
91
+ - Assigns each connection an opaque `peerId` (UUID).
92
+ - `onPeerConnect` fires for new and already-connected peers.
93
+ - `onPeerDisconnect` fires when a peer's socket closes.
94
+ - `send(peerId, payload)` throws if peerId is unknown.
95
+
96
+ **Runner** (`runner.ts`):
97
+
98
+ ```typescript
99
+ createRunnerPeer({ identity, trustedPublicKeys, url }): Promise<RunnerPeer>
100
+ ```
101
+
102
+ - Dials the orchestrator URL.
103
+ - Resolves when the WebSocket `open` event fires.
104
+ - Same subscribe/send/close API as `ProtocolConnection`.
105
+
106
+ ## Frame payload types
107
+
108
+ ```typescript
109
+ type RpcRequest = { kind: "rpc.request"; id: string; method: string; params: unknown };
110
+ type RpcResponse = {
111
+ kind: "rpc.response";
112
+ id: string;
113
+ result?: unknown;
114
+ error?: { code; message };
115
+ };
116
+ type RpcStreamEvent = {
117
+ kind: "rpc.stream";
118
+ id: string;
119
+ seq: number;
120
+ event: "data" | "end" | "error";
121
+ data?;
122
+ error?;
123
+ };
124
+ type Heartbeat = { kind: "heartbeat"; runnerId: string };
125
+ ```
126
+
127
+ `rpc.stream` supports ordered streaming responses. The orchestrator package does not use streaming today, but the protocol supports it for future methods.
128
+
129
+ ## Trust model
130
+
131
+ Both sides maintain a `trustedPublicKeys: Map<keyId, KeyObject>`:
132
+
133
+ - The **orchestrator** trusts runner public keys (loaded from static config).
134
+ - The **runner** trusts the orchestrator's public key.
135
+
136
+ Every inbound frame is verified against this map. There is no handshake or key exchange at connection time — keys must be pre-shared. An unknown `keyId` causes the frame to be dropped.
137
+
138
+ ## Wire format example
139
+
140
+ A heartbeat from a runner looks like this on the wire (one WebSocket text frame):
141
+
142
+ ```json
143
+ {
144
+ "payload": { "kind": "heartbeat", "runnerId": "runner" },
145
+ "signature": "base64...",
146
+ "keyId": "runner",
147
+ "algorithm": "ed25519",
148
+ "timestamp": 1719900000000
149
+ }
150
+ ```
151
+
152
+ ## Module layout
153
+
154
+ ```
155
+ src/
156
+ canonicalize.ts Deterministic JSON serialization
157
+ connection.ts WebSocket ↔ signed frame bridge
158
+ frames.ts Encode/decode/validate envelopes
159
+ keys.ts ed25519 keypair generation and PEM export
160
+ orchestrator.ts WebSocket server peer
161
+ runner.ts WebSocket client peer
162
+ sign.ts Sign and verify envelopes
163
+ types.ts Frame and peer type definitions
164
+ ```
165
+
166
+ ## Tests
167
+
168
+ - `sign.spec.ts` — round-trip, tamper, wrong key, non-canonical material
169
+ - `canonicalize.spec.ts` — key ordering stability
170
+ - `loopback.spec.ts` — real WebSocket server+client: RPC round-trip, streaming, disconnect, tampered frame rejection
171
+
172
+ Run with `vp test` from this package directory.
173
+
174
+ ## Dependencies
175
+
176
+ - `ws` — WebSocket implementation
177
+ - `node:crypto` — ed25519 key generation, sign, verify (no external crypto libraries)
@@ -0,0 +1,138 @@
1
+ import { KeyObject } from "node:crypto";
2
+ import { WebSocket as WebSocket$1 } from "ws";
3
+
4
+ //#region src/canonicalize.d.ts
5
+ declare function canonicalize(value: unknown): string;
6
+ //#endregion
7
+ //#region src/types.d.ts
8
+ type RpcRequest = {
9
+ kind: "rpc.request";
10
+ id: string;
11
+ method: string;
12
+ params: unknown;
13
+ };
14
+ type RpcResponse = {
15
+ kind: "rpc.response";
16
+ id: string;
17
+ result?: unknown;
18
+ error?: {
19
+ code: string;
20
+ message: string;
21
+ };
22
+ };
23
+ type RpcStreamEvent = {
24
+ kind: "rpc.stream";
25
+ id: string;
26
+ seq: number;
27
+ event: "data" | "end" | "error";
28
+ data?: unknown;
29
+ error?: {
30
+ code: string;
31
+ message: string;
32
+ };
33
+ };
34
+ type Heartbeat = {
35
+ kind: "heartbeat";
36
+ runnerId: string;
37
+ };
38
+ type FramePayload = RpcRequest | RpcResponse | RpcStreamEvent | Heartbeat;
39
+ declare const SIGNING_ALGORITHM: "ed25519";
40
+ type SignedEnvelope<T = FramePayload> = {
41
+ payload: T;
42
+ signature: string;
43
+ keyId: string;
44
+ algorithm: typeof SIGNING_ALGORITHM;
45
+ timestamp: number;
46
+ };
47
+ type UnsignedEnvelope<T = FramePayload> = {
48
+ payload: T;
49
+ keyId: string;
50
+ algorithm: typeof SIGNING_ALGORITHM;
51
+ timestamp: number;
52
+ };
53
+ type RunnerPeer = {
54
+ subscribe(filter: (payload: FramePayload) => boolean, callback: (payload: FramePayload) => void): () => void;
55
+ send(payload: FramePayload): void;
56
+ close(): void;
57
+ };
58
+ type ConnectedPeer = {
59
+ readonly peerId: string;
60
+ subscribe(filter: (payload: FramePayload) => boolean, callback: (payload: FramePayload) => void): () => void;
61
+ send(payload: FramePayload): void;
62
+ close(): void;
63
+ };
64
+ type OrchestratorPeer = {
65
+ readonly address: {
66
+ host: string;
67
+ port: number;
68
+ };
69
+ onPeerConnect(callback: (peer: ConnectedPeer) => void): () => void;
70
+ onPeerDisconnect(callback: (peer: ConnectedPeer) => void): () => void;
71
+ send(peerId: string, payload: FramePayload): void;
72
+ close(): void;
73
+ };
74
+ type PeerIdentity = {
75
+ keyId: string;
76
+ publicKey: import("node:crypto").KeyObject;
77
+ privateKey: import("node:crypto").KeyObject;
78
+ };
79
+ type PeerOptions = {
80
+ identity: PeerIdentity;
81
+ trustedPublicKeys: ReadonlyMap<string, import("node:crypto").KeyObject>;
82
+ };
83
+ type CreateRunnerPeerOptions = PeerOptions & {
84
+ url: string;
85
+ };
86
+ type CreateOrchestratorPeerOptions = PeerOptions & {
87
+ port?: number;
88
+ host?: string;
89
+ };
90
+ //#endregion
91
+ //#region src/connection.d.ts
92
+ type ProtocolConnectionOptions = {
93
+ identity: PeerIdentity;
94
+ trustedPublicKeys: ReadonlyMap<string, KeyObject>;
95
+ onClose?: () => void;
96
+ };
97
+ type ProtocolConnection = {
98
+ subscribe(filter: (payload: FramePayload) => boolean, callback: (payload: FramePayload) => void): () => void;
99
+ send(payload: FramePayload): void;
100
+ close(): void;
101
+ };
102
+ declare function createProtocolConnection(socket: WebSocket$1, options: ProtocolConnectionOptions): ProtocolConnection;
103
+ declare function toConnectedPeer(peerId: string, connection: ProtocolConnection): ConnectedPeer;
104
+ //#endregion
105
+ //#region src/frames.d.ts
106
+ declare function encodeEnvelope(envelope: SignedEnvelope): string;
107
+ declare function decodeEnvelope(raw: string): SignedEnvelope | null;
108
+ declare function isFramePayload(value: unknown): value is FramePayload;
109
+ //#endregion
110
+ //#region src/keys.d.ts
111
+ type LoadKeyPairOptions = {
112
+ privateKeyPem: string;
113
+ publicKeyPem: string;
114
+ keyId: string;
115
+ };
116
+ declare function fingerprintPublicKey(publicKey: KeyObject): string;
117
+ declare function generateKeyPair(keyId?: string): PeerIdentity;
118
+ declare function loadKeyPair(options: LoadKeyPairOptions): PeerIdentity;
119
+ declare function loadTrustedPublicKey(options: {
120
+ keyId: string;
121
+ publicKeyPem: string;
122
+ }): ReadonlyMap<string, KeyObject>;
123
+ declare function exportPublicKeyPem(publicKey: KeyObject): string;
124
+ declare function exportPrivateKeyPem(privateKey: KeyObject): string;
125
+ //#endregion
126
+ //#region src/orchestrator.d.ts
127
+ declare function createOrchestratorPeer(options: CreateOrchestratorPeerOptions): Promise<OrchestratorPeer>;
128
+ //#endregion
129
+ //#region src/runner.d.ts
130
+ declare function createRunnerPeer(options: CreateRunnerPeerOptions): Promise<RunnerPeer>;
131
+ //#endregion
132
+ //#region src/sign.d.ts
133
+ declare function buildSigningMaterial(envelope: UnsignedEnvelope): string;
134
+ declare function signPayload(payload: FramePayload, identity: PeerIdentity, timestamp?: number): SignedEnvelope;
135
+ declare function verifyEnvelope(envelope: SignedEnvelope, trustedPublicKeys: ReadonlyMap<string, KeyObject>): boolean;
136
+ declare function signRawMaterial(material: string, identity: PeerIdentity): string;
137
+ //#endregion
138
+ export { type ConnectedPeer, type CreateOrchestratorPeerOptions, type CreateRunnerPeerOptions, type FramePayload, type Heartbeat, type LoadKeyPairOptions, type OrchestratorPeer, type PeerIdentity, type PeerOptions, type ProtocolConnection, type ProtocolConnectionOptions, type RpcRequest, type RpcResponse, type RpcStreamEvent, type RunnerPeer, SIGNING_ALGORITHM, type SignedEnvelope, type UnsignedEnvelope, buildSigningMaterial, canonicalize, createOrchestratorPeer, createProtocolConnection, createRunnerPeer, decodeEnvelope, encodeEnvelope, exportPrivateKeyPem, exportPublicKeyPem, fingerprintPublicKey, generateKeyPair, isFramePayload, loadKeyPair, loadTrustedPublicKey, signPayload, signRawMaterial, toConnectedPeer, verifyEnvelope };
package/dist/index.mjs ADDED
@@ -0,0 +1,299 @@
1
+ import { createHash, createPrivateKey, createPublicKey, generateKeyPairSync, randomUUID, sign, verify } from "node:crypto";
2
+ import WebSocket, { WebSocketServer } from "ws";
3
+ //#region src/canonicalize.ts
4
+ function canonicalize(value) {
5
+ return JSON.stringify(sortKeys(value));
6
+ }
7
+ function sortKeys(value) {
8
+ if (Array.isArray(value)) return value.map(sortKeys);
9
+ if (value !== null && typeof value === "object") {
10
+ const record = value;
11
+ const sorted = {};
12
+ for (const key of Object.keys(record).sort()) sorted[key] = sortKeys(record[key]);
13
+ return sorted;
14
+ }
15
+ return value;
16
+ }
17
+ //#endregion
18
+ //#region src/frames.ts
19
+ function encodeEnvelope(envelope) {
20
+ return JSON.stringify(envelope);
21
+ }
22
+ function decodeEnvelope(raw) {
23
+ try {
24
+ const parsed = JSON.parse(raw);
25
+ if (!isSignedEnvelope(parsed)) return null;
26
+ return parsed;
27
+ } catch {
28
+ return null;
29
+ }
30
+ }
31
+ function isFramePayload(value) {
32
+ if (value === null || typeof value !== "object" || !("kind" in value)) return false;
33
+ const record = value;
34
+ switch (record.kind) {
35
+ case "rpc.request": return typeof record.id === "string" && typeof record.method === "string" && "params" in record;
36
+ case "rpc.response": return typeof record.id === "string";
37
+ case "rpc.stream": return typeof record.id === "string" && typeof record.seq === "number" && (record.event === "data" || record.event === "end" || record.event === "error");
38
+ case "heartbeat": return typeof record.runnerId === "string";
39
+ default: return false;
40
+ }
41
+ }
42
+ function isSignedEnvelope(value) {
43
+ if (value === null || typeof value !== "object") return false;
44
+ const envelope = value;
45
+ return typeof envelope.signature === "string" && typeof envelope.keyId === "string" && envelope.algorithm === "ed25519" && typeof envelope.timestamp === "number" && envelope.payload !== void 0 && isFramePayload(envelope.payload);
46
+ }
47
+ //#endregion
48
+ //#region src/types.ts
49
+ const SIGNING_ALGORITHM = "ed25519";
50
+ //#endregion
51
+ //#region src/sign.ts
52
+ function buildSigningMaterial(envelope) {
53
+ return canonicalize({
54
+ algorithm: envelope.algorithm,
55
+ keyId: envelope.keyId,
56
+ payload: envelope.payload,
57
+ timestamp: envelope.timestamp
58
+ });
59
+ }
60
+ function signPayload(payload, identity, timestamp = Date.now()) {
61
+ const unsigned = {
62
+ algorithm: SIGNING_ALGORITHM,
63
+ keyId: identity.keyId,
64
+ payload,
65
+ timestamp
66
+ };
67
+ const material = buildSigningMaterial(unsigned);
68
+ const signature = sign(null, Buffer.from(material, "utf8"), identity.privateKey);
69
+ return {
70
+ ...unsigned,
71
+ signature: signature.toString("base64")
72
+ };
73
+ }
74
+ function verifyEnvelope(envelope, trustedPublicKeys) {
75
+ if (envelope.algorithm !== "ed25519") return false;
76
+ const publicKey = trustedPublicKeys.get(envelope.keyId);
77
+ if (publicKey === void 0) return false;
78
+ const material = buildSigningMaterial({
79
+ algorithm: envelope.algorithm,
80
+ keyId: envelope.keyId,
81
+ payload: envelope.payload,
82
+ timestamp: envelope.timestamp
83
+ });
84
+ let signature;
85
+ try {
86
+ signature = Buffer.from(envelope.signature, "base64");
87
+ } catch {
88
+ return false;
89
+ }
90
+ return verify(null, Buffer.from(material, "utf8"), publicKey, signature);
91
+ }
92
+ function signRawMaterial(material, identity) {
93
+ return sign(null, Buffer.from(material, "utf8"), identity.privateKey).toString("base64");
94
+ }
95
+ //#endregion
96
+ //#region src/connection.ts
97
+ function createProtocolConnection(socket, options) {
98
+ const subscribers = /* @__PURE__ */ new Set();
99
+ let closed = false;
100
+ const handleMessage = (data) => {
101
+ if (closed) return;
102
+ const envelope = decodeEnvelope(Buffer.isBuffer(data) ? data.toString("utf8") : Buffer.from(data).toString("utf8"));
103
+ if (envelope === null) return;
104
+ if (!verifyEnvelope(envelope, options.trustedPublicKeys)) return;
105
+ if (!isFramePayload(envelope.payload)) return;
106
+ const payload = envelope.payload;
107
+ for (const subscriber of subscribers) if (subscriber.filter(payload)) subscriber.callback(payload);
108
+ };
109
+ const handleClose = () => {
110
+ if (closed) return;
111
+ closed = true;
112
+ socket.removeListener("message", handleMessage);
113
+ socket.removeListener("close", handleClose);
114
+ subscribers.clear();
115
+ options.onClose?.();
116
+ };
117
+ socket.on("message", handleMessage);
118
+ socket.on("close", handleClose);
119
+ socket.on("error", () => {
120
+ handleClose();
121
+ });
122
+ return {
123
+ subscribe(filter, callback) {
124
+ const subscriber = {
125
+ filter,
126
+ callback
127
+ };
128
+ subscribers.add(subscriber);
129
+ return () => {
130
+ subscribers.delete(subscriber);
131
+ };
132
+ },
133
+ send(payload) {
134
+ if (closed || socket.readyState !== socket.OPEN) return;
135
+ const envelope = signPayload(payload, options.identity);
136
+ socket.send(encodeEnvelope(envelope));
137
+ },
138
+ close() {
139
+ if (closed) return;
140
+ closed = true;
141
+ socket.close();
142
+ handleClose();
143
+ }
144
+ };
145
+ }
146
+ function toConnectedPeer(peerId, connection) {
147
+ return {
148
+ peerId,
149
+ subscribe: connection.subscribe.bind(connection),
150
+ send: connection.send.bind(connection),
151
+ close: connection.close.bind(connection)
152
+ };
153
+ }
154
+ //#endregion
155
+ //#region src/keys.ts
156
+ function fingerprintPublicKey(publicKey) {
157
+ const der = publicKey.export({
158
+ format: "der",
159
+ type: "spki"
160
+ });
161
+ return createHash("sha256").update(der).digest("base64url").slice(0, 16);
162
+ }
163
+ function generateKeyPair(keyId) {
164
+ const { publicKey, privateKey } = generateKeyPairSync("ed25519");
165
+ return {
166
+ keyId: keyId ?? fingerprintPublicKey(publicKey),
167
+ publicKey,
168
+ privateKey
169
+ };
170
+ }
171
+ function loadKeyPair(options) {
172
+ return {
173
+ keyId: options.keyId,
174
+ publicKey: createPublicKey(options.publicKeyPem),
175
+ privateKey: createPrivateKey(options.privateKeyPem)
176
+ };
177
+ }
178
+ function loadTrustedPublicKey(options) {
179
+ return new Map([[options.keyId, createPublicKey(options.publicKeyPem)]]);
180
+ }
181
+ function exportPublicKeyPem(publicKey) {
182
+ return publicKey.export({
183
+ format: "pem",
184
+ type: "spki"
185
+ }).toString();
186
+ }
187
+ function exportPrivateKeyPem(privateKey) {
188
+ return privateKey.export({
189
+ format: "pem",
190
+ type: "pkcs8"
191
+ }).toString();
192
+ }
193
+ //#endregion
194
+ //#region src/orchestrator.ts
195
+ function createOrchestratorPeer(options) {
196
+ return new Promise((resolve, reject) => {
197
+ const connectCallbacks = /* @__PURE__ */ new Set();
198
+ const disconnectCallbacks = /* @__PURE__ */ new Set();
199
+ const peers = /* @__PURE__ */ new Map();
200
+ let closed = false;
201
+ const wss = new WebSocketServer({
202
+ host: options.host ?? "127.0.0.1",
203
+ port: options.port ?? 0
204
+ });
205
+ wss.on("error", (error) => {
206
+ if (!closed) reject(error);
207
+ });
208
+ wss.on("listening", () => {
209
+ const address = wss.address();
210
+ if (address === null || typeof address === "string") {
211
+ reject(/* @__PURE__ */ new Error("WebSocket server address is unavailable"));
212
+ return;
213
+ }
214
+ resolve({
215
+ address: {
216
+ host: address.address === "::" || address.address === "0.0.0.0" ? "127.0.0.1" : address.address,
217
+ port: address.port
218
+ },
219
+ onPeerConnect(callback) {
220
+ connectCallbacks.add(callback);
221
+ for (const entry of peers.values()) callback(entry.peer);
222
+ return () => {
223
+ connectCallbacks.delete(callback);
224
+ };
225
+ },
226
+ onPeerDisconnect(callback) {
227
+ disconnectCallbacks.add(callback);
228
+ return () => {
229
+ disconnectCallbacks.delete(callback);
230
+ };
231
+ },
232
+ send(peerId, payload) {
233
+ const entry = peers.get(peerId);
234
+ if (entry === void 0) throw new Error(`Unknown peer id: ${peerId}`);
235
+ entry.connection.send(payload);
236
+ },
237
+ close() {
238
+ if (closed) return;
239
+ closed = true;
240
+ for (const entry of peers.values()) entry.connection.close();
241
+ peers.clear();
242
+ wss.close();
243
+ }
244
+ });
245
+ });
246
+ wss.on("connection", (socket) => {
247
+ if (closed) {
248
+ socket.close();
249
+ return;
250
+ }
251
+ const peerId = randomUUID();
252
+ const connection = createProtocolConnection(socket, {
253
+ identity: options.identity,
254
+ trustedPublicKeys: options.trustedPublicKeys,
255
+ onClose: () => {
256
+ const entry = peers.get(peerId);
257
+ if (entry === void 0) return;
258
+ peers.delete(peerId);
259
+ for (const callback of disconnectCallbacks) callback(entry.peer);
260
+ }
261
+ });
262
+ const peer = toConnectedPeer(peerId, connection);
263
+ peers.set(peerId, {
264
+ peer,
265
+ connection
266
+ });
267
+ for (const callback of connectCallbacks) callback(peer);
268
+ });
269
+ });
270
+ }
271
+ //#endregion
272
+ //#region src/runner.ts
273
+ function createRunnerPeer(options) {
274
+ return new Promise((resolve, reject) => {
275
+ const socket = new WebSocket(options.url);
276
+ const fail = (error) => {
277
+ socket.removeAllListeners();
278
+ reject(error);
279
+ };
280
+ socket.once("error", fail);
281
+ socket.once("open", () => {
282
+ socket.removeListener("error", fail);
283
+ socket.on("error", () => {
284
+ connection.close();
285
+ });
286
+ const connection = createProtocolConnection(socket, {
287
+ identity: options.identity,
288
+ trustedPublicKeys: options.trustedPublicKeys
289
+ });
290
+ resolve({
291
+ subscribe: connection.subscribe.bind(connection),
292
+ send: connection.send.bind(connection),
293
+ close: connection.close.bind(connection)
294
+ });
295
+ });
296
+ });
297
+ }
298
+ //#endregion
299
+ export { SIGNING_ALGORITHM, buildSigningMaterial, canonicalize, createOrchestratorPeer, createProtocolConnection, createRunnerPeer, decodeEnvelope, encodeEnvelope, exportPrivateKeyPem, exportPublicKeyPem, fingerprintPublicKey, generateKeyPair, isFramePayload, loadKeyPair, loadTrustedPublicKey, signPayload, signRawMaterial, toConnectedPeer, verifyEnvelope };
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@bifrost-ai/protocol",
3
+ "version": "0.1.1-build.1783029473245",
4
+ "description": "Signed WebSocket RPC protocol for runner-orchestrator communication",
5
+ "license": "MIT",
6
+ "files": [
7
+ "dist"
8
+ ],
9
+ "type": "module",
10
+ "exports": {
11
+ ".": "./dist/index.mjs",
12
+ "./package.json": "./package.json"
13
+ },
14
+ "publishConfig": {
15
+ "access": "public"
16
+ },
17
+ "dependencies": {
18
+ "ws": "^8.18.3"
19
+ },
20
+ "devDependencies": {
21
+ "@types/node": "^25.6.2",
22
+ "@types/ws": "^8.18.1",
23
+ "@typescript/native-preview": "latest",
24
+ "bumpp": "^11.1.0",
25
+ "typescript": "^6.0.3",
26
+ "vite-plus": "^0.2.0",
27
+ "vitest-gwt": "^4.1.0"
28
+ },
29
+ "scripts": {
30
+ "build": "vp pack",
31
+ "dev": "vp pack --watch",
32
+ "test": "vp test",
33
+ "check": "vp check"
34
+ }
35
+ }