@datagrout/conduit 0.3.0 → 0.4.0

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
@@ -5,7 +5,7 @@ Production-ready MCP client with mTLS identity, OAuth 2.1, semantic discovery, a
5
5
  ## Installation
6
6
 
7
7
  ```bash
8
- npm install @datagrout/conduit
8
+ npm install @datagrout/conduit@0.4.0
9
9
  ```
10
10
 
11
11
  ## Quick Start
@@ -158,8 +158,58 @@ const client = new Client({ url });
158
158
 
159
159
  // JSONRPC — lightweight, stateless, same tools and auth
160
160
  const client = new Client({ url, transport: 'jsonrpc' });
161
+
162
+ // WebSocket — bidirectional push
163
+ const client = new Client({
164
+ url: 'wss://gateway.datagrout.ai/servers/{uuid}/ws',
165
+ transport: 'websocket',
166
+ });
161
167
  ```
162
168
 
169
+ ### WebSocket transport
170
+
171
+ The WebSocket transport uses the `datagrout-jsonrpc.v1` subprotocol over a single persistent `wss://` connection. Concurrent requests are multiplexed; responses correlated by JSON-RPC `id` via `Map<string, { resolve, reject }>`.
172
+
173
+ ```typescript
174
+ import { Client } from '@datagrout/conduit';
175
+
176
+ const client = new Client({
177
+ url: 'wss://gateway.datagrout.ai/servers/{uuid}/ws',
178
+ auth: { bearer: 'your-token' },
179
+ transport: 'websocket',
180
+ });
181
+ await client.connect();
182
+
183
+ // Subscribe to server-pushed events
184
+ const sub = await client.subscribe('agents.my-agent-id.events');
185
+
186
+ for await (const event of sub) {
187
+ console.log(event.event, event.data);
188
+ }
189
+
190
+ await client.unsubscribe(sub.id);
191
+ await client.disconnect();
192
+ ```
193
+
194
+ You can also call `await sub.recv()` for one-event-at-a-time consumption:
195
+
196
+ ```typescript
197
+ const event = await sub.recv();
198
+ console.log(event.event, event.data);
199
+ ```
200
+
201
+ Supported topics:
202
+
203
+ | Topic | Fires when |
204
+ |-------|-----------|
205
+ | `agents.<agent_id>.events` | Agent lifecycle events (plan started, IC completed, grounding failed, …) |
206
+ | `tools.<tool_name>.results` | A specific tool call completes |
207
+ | `tasks.<task_id>.*` | Long-running background task transitions |
208
+ | `flows.<flow_id>.*` | `flow.into` progress and completion |
209
+ | `governor.<server_uuid>` | Governor percept events (file change, schedule, webhook) |
210
+
211
+ **Reconnection**: after a disconnect, calls raise `NotInitializedError`. Re-call `connect()` and re-subscribe — subscriptions do not survive reconnects in v0.4.
212
+
163
213
  ## API Reference
164
214
 
165
215
  ### Client Options
@@ -168,7 +218,7 @@ const client = new Client({ url, transport: 'jsonrpc' });
168
218
  new Client(options: {
169
219
  url: string;
170
220
  auth?: { bearer?: string; apiKey?: string; clientCredentials?: {...} };
171
- transport?: 'mcp' | 'jsonrpc';
221
+ transport?: 'mcp' | 'jsonrpc' | 'websocket';
172
222
  useIntelligentInterface?: boolean;
173
223
  identity?: ConduitIdentity;
174
224
  identityAuto?: boolean;
package/dist/index.d.mts CHANGED
@@ -256,6 +256,7 @@ interface GuideState {
256
256
  }
257
257
  interface AuthConfig {
258
258
  bearer?: string;
259
+ apiKey?: string;
259
260
  basic?: {
260
261
  username: string;
261
262
  password: string;
@@ -321,7 +322,7 @@ interface ClientOptions {
321
322
  * @default false
322
323
  */
323
324
  disableMtls?: boolean;
324
- transport?: 'mcp' | 'jsonrpc';
325
+ transport?: 'mcp' | 'jsonrpc' | 'websocket';
325
326
  timeout?: number;
326
327
  /**
327
328
  * Maximum number of automatic retries on "server not initialized" errors.
@@ -461,6 +462,136 @@ interface MCPPrompt {
461
462
  }>;
462
463
  }
463
464
 
465
+ /**
466
+ * Base transport interface
467
+ */
468
+
469
+ declare abstract class Transport {
470
+ abstract connect(): Promise<void>;
471
+ abstract disconnect(): Promise<void>;
472
+ abstract listTools(options?: any): Promise<MCPTool[]>;
473
+ abstract callTool(name: string, args: Record<string, any>, options?: any): Promise<any>;
474
+ abstract listResources(options?: any): Promise<MCPResource[]>;
475
+ abstract readResource(uri: string, options?: any): Promise<any>;
476
+ abstract listPrompts(options?: any): Promise<MCPPrompt[]>;
477
+ abstract getPrompt(name: string, args?: Record<string, any>, options?: any): Promise<any>;
478
+ }
479
+
480
+ /**
481
+ * WebSocket transport for `datagrout-jsonrpc.v1`.
482
+ *
483
+ * Recommended for any client that needs bidirectional push (server-initiated
484
+ * notifications) without a polling loop. A single `wss://` connection is
485
+ * multiplexed for all in-flight requests; concurrent calls are correlated by
486
+ * JSON-RPC `id` with no head-of-line blocking.
487
+ *
488
+ * @example
489
+ * ```ts
490
+ * const client = new Client({
491
+ * url: 'wss://gateway.datagrout.ai/servers/<uuid>/ws',
492
+ * transport: 'websocket',
493
+ * auth: { bearer: 'your-token' },
494
+ * });
495
+ * await client.connect();
496
+ *
497
+ * // Push subscription
498
+ * const sub = await client.subscribe('agents.my-agent-id.events');
499
+ * for await (const event of sub) {
500
+ * console.log(event.event, event.data);
501
+ * }
502
+ * await client.unsubscribe(sub.id);
503
+ * ```
504
+ *
505
+ * @module
506
+ */
507
+
508
+ declare const SUBPROTOCOL = "datagrout-jsonrpc.v1";
509
+ /** A single server-pushed notification. */
510
+ interface SubscriptionEvent {
511
+ /** The subscription id this event belongs to. */
512
+ subscription: string;
513
+ /** Server-named event slug (e.g. `"agent.thought"`). */
514
+ event: string;
515
+ /** Free-form payload from the server. */
516
+ data: unknown;
517
+ }
518
+ /**
519
+ * Handle for an active server-push subscription.
520
+ *
521
+ * Consume events with {@link Subscription.recv} or an async-for loop:
522
+ * ```ts
523
+ * for await (const event of sub) {
524
+ * handle(event);
525
+ * }
526
+ * ```
527
+ */
528
+ declare class Subscription {
529
+ readonly id: string;
530
+ readonly topic: string;
531
+ private _queue;
532
+ private _waiters;
533
+ private _rejecters;
534
+ private _closed;
535
+ constructor(id: string, topic: string);
536
+ /**
537
+ * Wait for the next event from this subscription.
538
+ *
539
+ * @throws When the subscription has been closed.
540
+ */
541
+ recv(): Promise<SubscriptionEvent>;
542
+ [Symbol.asyncIterator](): AsyncGenerator<SubscriptionEvent>;
543
+ _enqueue(event: SubscriptionEvent): void;
544
+ _close(): void;
545
+ }
546
+ /**
547
+ * JSON-RPC 2.0 over WebSocket transport (`datagrout-jsonrpc.v1`).
548
+ *
549
+ * Multiplexes any number of in-flight requests on one socket and routes
550
+ * server-pushed notifications back to {@link Subscription} queues.
551
+ */
552
+ declare class WsTransport extends Transport {
553
+ private readonly _url;
554
+ private readonly _auth?;
555
+ private _ws;
556
+ private _nextId;
557
+ private readonly _pending;
558
+ private readonly _pendingSubscribe;
559
+ private readonly _subscriptions;
560
+ constructor(url: string, auth?: AuthConfig, _timeout?: number, _identity?: ConduitIdentity);
561
+ connect(): Promise<void>;
562
+ disconnect(): Promise<void>;
563
+ /**
564
+ * Open a server-side push subscription for `topic`.
565
+ *
566
+ * @param topic - Dotted namespace topic, e.g.
567
+ * `"agents.my-agent-id.events"` or `"tasks.task-123.*"`.
568
+ * @returns A {@link Subscription} handle whose async-for loop delivers events.
569
+ */
570
+ subscribe(topic: string): Promise<Subscription>;
571
+ /**
572
+ * Cancel a server-side subscription.
573
+ *
574
+ * The local {@link Subscription} queue is closed immediately.
575
+ *
576
+ * @param subscriptionId - The `id` field from the {@link Subscription}
577
+ * returned by {@link subscribe}.
578
+ */
579
+ unsubscribe(subscriptionId: string): Promise<void>;
580
+ listTools(options?: any): Promise<MCPTool[]>;
581
+ callTool(name: string, args: Record<string, any>, _options?: any): Promise<any>;
582
+ listResources(_options?: any): Promise<MCPResource[]>;
583
+ readResource(uri: string, _options?: any): Promise<any>;
584
+ listPrompts(_options?: any): Promise<MCPPrompt[]>;
585
+ getPrompt(name: string, args?: Record<string, any>, _options?: any): Promise<any>;
586
+ private _mintId;
587
+ private _requireConnected;
588
+ private _send;
589
+ private _request;
590
+ private _handleMessage;
591
+ private _routeNotification;
592
+ private _failAll;
593
+ }
594
+
464
595
  /**
465
596
  * Prism namespace — data transformation, charting, rendering, and export.
466
597
  */
@@ -842,6 +973,33 @@ declare class Client {
842
973
  * @param args - Template argument values.
843
974
  */
844
975
  getPrompt(name: string, args?: Record<string, any>, options?: any): Promise<any>;
976
+ /**
977
+ * Subscribe to a server-push topic (WebSocket transport only).
978
+ *
979
+ * Requires `transport: 'websocket'` when constructing the client.
980
+ *
981
+ * @param topic - Dotted namespace topic, e.g.
982
+ * `"agents.my-agent-id.events"` or `"tasks.task-123.*"`.
983
+ * @returns A {@link Subscription} handle. Consume events with
984
+ * {@link Subscription.recv} or an `for await` loop.
985
+ *
986
+ * @example
987
+ * ```ts
988
+ * const sub = await client.subscribe('agents.my-agent-id.events');
989
+ * for await (const event of sub) {
990
+ * console.log(event.event, event.data);
991
+ * }
992
+ * await client.unsubscribe(sub.id);
993
+ * ```
994
+ */
995
+ subscribe(topic: string): Promise<Subscription>;
996
+ /**
997
+ * Cancel a server-side push subscription.
998
+ *
999
+ * @param subscriptionId - The `id` from the {@link Subscription} returned
1000
+ * by {@link subscribe}.
1001
+ */
1002
+ unsubscribe(subscriptionId: string): Promise<void>;
845
1003
  private warnIfNotDg;
846
1004
  /**
847
1005
  * Semantically discover tools relevant to a goal or query.
@@ -1223,6 +1381,6 @@ declare class InvalidConfigError extends ConduitError {
1223
1381
  * DataGrout Conduit SDK for TypeScript/JavaScript
1224
1382
  */
1225
1383
 
1226
- declare const version = "0.1.0";
1384
+ declare const version = "0.4.0";
1227
1385
 
1228
- export { type AuthConfig, AuthError, type Byok, type ChartOptions, Client, type ClientOptions, ConduitError, ConduitIdentity, type ConstrainOptions, type CreditEstimate, DEFAULT_IDENTITY_DIR, DG_CA_URL, DG_SUBSTRATE_ENDPOINT, type DiscoverOptions, type DiscoverResult, type FlowOptions, type ForgetOptions, type GuideOptions, type GuideRequestOptions, type GuideState, GuidedSession, InvalidConfigError, type Keypair, type MCPPrompt, type MCPResource, type MCPTool, type MtlsConfig, NetworkError, NotInitializedError, OAuthTokenProvider, type PerformOptions, type PerformResult, type PlanOptions, type PrismFocusOptions, type QueryCellOptions, type RateLimit, RateLimitError, type RateLimitStatus, type Receipt, type ReflectOptions, type RefractOptions, type RegisteredIdentity, type RegistrationOptions, type RememberOptions, type RenewalOptions, type RotationOptions, type SavedPaths, ServerError, type ToolInfo, type ToolMeta, deriveTokenEndpoint, extractMeta, fetchDgCaCert, fetchWithIdentity, generateKeypair, isDgUrl, refreshCaCert, registerIdentity, rotateIdentity, saveIdentity, version };
1386
+ export { type AuthConfig, AuthError, type Byok, type ChartOptions, Client, type ClientOptions, ConduitError, ConduitIdentity, type ConstrainOptions, type CreditEstimate, DEFAULT_IDENTITY_DIR, DG_CA_URL, DG_SUBSTRATE_ENDPOINT, type DiscoverOptions, type DiscoverResult, type FlowOptions, type ForgetOptions, type GuideOptions, type GuideRequestOptions, type GuideState, GuidedSession, InvalidConfigError, type Keypair, type MCPPrompt, type MCPResource, type MCPTool, type MtlsConfig, NetworkError, NotInitializedError, OAuthTokenProvider, type PerformOptions, type PerformResult, type PlanOptions, type PrismFocusOptions, type QueryCellOptions, type RateLimit, RateLimitError, type RateLimitStatus, type Receipt, type ReflectOptions, type RefractOptions, type RegisteredIdentity, type RegistrationOptions, type RememberOptions, type RenewalOptions, type RotationOptions, type SavedPaths, ServerError, Subscription, type SubscriptionEvent, type ToolInfo, type ToolMeta, SUBPROTOCOL as WS_SUBPROTOCOL, WsTransport, deriveTokenEndpoint, extractMeta, fetchDgCaCert, fetchWithIdentity, generateKeypair, isDgUrl, refreshCaCert, registerIdentity, rotateIdentity, saveIdentity, version };
package/dist/index.d.ts CHANGED
@@ -256,6 +256,7 @@ interface GuideState {
256
256
  }
257
257
  interface AuthConfig {
258
258
  bearer?: string;
259
+ apiKey?: string;
259
260
  basic?: {
260
261
  username: string;
261
262
  password: string;
@@ -321,7 +322,7 @@ interface ClientOptions {
321
322
  * @default false
322
323
  */
323
324
  disableMtls?: boolean;
324
- transport?: 'mcp' | 'jsonrpc';
325
+ transport?: 'mcp' | 'jsonrpc' | 'websocket';
325
326
  timeout?: number;
326
327
  /**
327
328
  * Maximum number of automatic retries on "server not initialized" errors.
@@ -461,6 +462,136 @@ interface MCPPrompt {
461
462
  }>;
462
463
  }
463
464
 
465
+ /**
466
+ * Base transport interface
467
+ */
468
+
469
+ declare abstract class Transport {
470
+ abstract connect(): Promise<void>;
471
+ abstract disconnect(): Promise<void>;
472
+ abstract listTools(options?: any): Promise<MCPTool[]>;
473
+ abstract callTool(name: string, args: Record<string, any>, options?: any): Promise<any>;
474
+ abstract listResources(options?: any): Promise<MCPResource[]>;
475
+ abstract readResource(uri: string, options?: any): Promise<any>;
476
+ abstract listPrompts(options?: any): Promise<MCPPrompt[]>;
477
+ abstract getPrompt(name: string, args?: Record<string, any>, options?: any): Promise<any>;
478
+ }
479
+
480
+ /**
481
+ * WebSocket transport for `datagrout-jsonrpc.v1`.
482
+ *
483
+ * Recommended for any client that needs bidirectional push (server-initiated
484
+ * notifications) without a polling loop. A single `wss://` connection is
485
+ * multiplexed for all in-flight requests; concurrent calls are correlated by
486
+ * JSON-RPC `id` with no head-of-line blocking.
487
+ *
488
+ * @example
489
+ * ```ts
490
+ * const client = new Client({
491
+ * url: 'wss://gateway.datagrout.ai/servers/<uuid>/ws',
492
+ * transport: 'websocket',
493
+ * auth: { bearer: 'your-token' },
494
+ * });
495
+ * await client.connect();
496
+ *
497
+ * // Push subscription
498
+ * const sub = await client.subscribe('agents.my-agent-id.events');
499
+ * for await (const event of sub) {
500
+ * console.log(event.event, event.data);
501
+ * }
502
+ * await client.unsubscribe(sub.id);
503
+ * ```
504
+ *
505
+ * @module
506
+ */
507
+
508
+ declare const SUBPROTOCOL = "datagrout-jsonrpc.v1";
509
+ /** A single server-pushed notification. */
510
+ interface SubscriptionEvent {
511
+ /** The subscription id this event belongs to. */
512
+ subscription: string;
513
+ /** Server-named event slug (e.g. `"agent.thought"`). */
514
+ event: string;
515
+ /** Free-form payload from the server. */
516
+ data: unknown;
517
+ }
518
+ /**
519
+ * Handle for an active server-push subscription.
520
+ *
521
+ * Consume events with {@link Subscription.recv} or an async-for loop:
522
+ * ```ts
523
+ * for await (const event of sub) {
524
+ * handle(event);
525
+ * }
526
+ * ```
527
+ */
528
+ declare class Subscription {
529
+ readonly id: string;
530
+ readonly topic: string;
531
+ private _queue;
532
+ private _waiters;
533
+ private _rejecters;
534
+ private _closed;
535
+ constructor(id: string, topic: string);
536
+ /**
537
+ * Wait for the next event from this subscription.
538
+ *
539
+ * @throws When the subscription has been closed.
540
+ */
541
+ recv(): Promise<SubscriptionEvent>;
542
+ [Symbol.asyncIterator](): AsyncGenerator<SubscriptionEvent>;
543
+ _enqueue(event: SubscriptionEvent): void;
544
+ _close(): void;
545
+ }
546
+ /**
547
+ * JSON-RPC 2.0 over WebSocket transport (`datagrout-jsonrpc.v1`).
548
+ *
549
+ * Multiplexes any number of in-flight requests on one socket and routes
550
+ * server-pushed notifications back to {@link Subscription} queues.
551
+ */
552
+ declare class WsTransport extends Transport {
553
+ private readonly _url;
554
+ private readonly _auth?;
555
+ private _ws;
556
+ private _nextId;
557
+ private readonly _pending;
558
+ private readonly _pendingSubscribe;
559
+ private readonly _subscriptions;
560
+ constructor(url: string, auth?: AuthConfig, _timeout?: number, _identity?: ConduitIdentity);
561
+ connect(): Promise<void>;
562
+ disconnect(): Promise<void>;
563
+ /**
564
+ * Open a server-side push subscription for `topic`.
565
+ *
566
+ * @param topic - Dotted namespace topic, e.g.
567
+ * `"agents.my-agent-id.events"` or `"tasks.task-123.*"`.
568
+ * @returns A {@link Subscription} handle whose async-for loop delivers events.
569
+ */
570
+ subscribe(topic: string): Promise<Subscription>;
571
+ /**
572
+ * Cancel a server-side subscription.
573
+ *
574
+ * The local {@link Subscription} queue is closed immediately.
575
+ *
576
+ * @param subscriptionId - The `id` field from the {@link Subscription}
577
+ * returned by {@link subscribe}.
578
+ */
579
+ unsubscribe(subscriptionId: string): Promise<void>;
580
+ listTools(options?: any): Promise<MCPTool[]>;
581
+ callTool(name: string, args: Record<string, any>, _options?: any): Promise<any>;
582
+ listResources(_options?: any): Promise<MCPResource[]>;
583
+ readResource(uri: string, _options?: any): Promise<any>;
584
+ listPrompts(_options?: any): Promise<MCPPrompt[]>;
585
+ getPrompt(name: string, args?: Record<string, any>, _options?: any): Promise<any>;
586
+ private _mintId;
587
+ private _requireConnected;
588
+ private _send;
589
+ private _request;
590
+ private _handleMessage;
591
+ private _routeNotification;
592
+ private _failAll;
593
+ }
594
+
464
595
  /**
465
596
  * Prism namespace — data transformation, charting, rendering, and export.
466
597
  */
@@ -842,6 +973,33 @@ declare class Client {
842
973
  * @param args - Template argument values.
843
974
  */
844
975
  getPrompt(name: string, args?: Record<string, any>, options?: any): Promise<any>;
976
+ /**
977
+ * Subscribe to a server-push topic (WebSocket transport only).
978
+ *
979
+ * Requires `transport: 'websocket'` when constructing the client.
980
+ *
981
+ * @param topic - Dotted namespace topic, e.g.
982
+ * `"agents.my-agent-id.events"` or `"tasks.task-123.*"`.
983
+ * @returns A {@link Subscription} handle. Consume events with
984
+ * {@link Subscription.recv} or an `for await` loop.
985
+ *
986
+ * @example
987
+ * ```ts
988
+ * const sub = await client.subscribe('agents.my-agent-id.events');
989
+ * for await (const event of sub) {
990
+ * console.log(event.event, event.data);
991
+ * }
992
+ * await client.unsubscribe(sub.id);
993
+ * ```
994
+ */
995
+ subscribe(topic: string): Promise<Subscription>;
996
+ /**
997
+ * Cancel a server-side push subscription.
998
+ *
999
+ * @param subscriptionId - The `id` from the {@link Subscription} returned
1000
+ * by {@link subscribe}.
1001
+ */
1002
+ unsubscribe(subscriptionId: string): Promise<void>;
845
1003
  private warnIfNotDg;
846
1004
  /**
847
1005
  * Semantically discover tools relevant to a goal or query.
@@ -1223,6 +1381,6 @@ declare class InvalidConfigError extends ConduitError {
1223
1381
  * DataGrout Conduit SDK for TypeScript/JavaScript
1224
1382
  */
1225
1383
 
1226
- declare const version = "0.1.0";
1384
+ declare const version = "0.4.0";
1227
1385
 
1228
- export { type AuthConfig, AuthError, type Byok, type ChartOptions, Client, type ClientOptions, ConduitError, ConduitIdentity, type ConstrainOptions, type CreditEstimate, DEFAULT_IDENTITY_DIR, DG_CA_URL, DG_SUBSTRATE_ENDPOINT, type DiscoverOptions, type DiscoverResult, type FlowOptions, type ForgetOptions, type GuideOptions, type GuideRequestOptions, type GuideState, GuidedSession, InvalidConfigError, type Keypair, type MCPPrompt, type MCPResource, type MCPTool, type MtlsConfig, NetworkError, NotInitializedError, OAuthTokenProvider, type PerformOptions, type PerformResult, type PlanOptions, type PrismFocusOptions, type QueryCellOptions, type RateLimit, RateLimitError, type RateLimitStatus, type Receipt, type ReflectOptions, type RefractOptions, type RegisteredIdentity, type RegistrationOptions, type RememberOptions, type RenewalOptions, type RotationOptions, type SavedPaths, ServerError, type ToolInfo, type ToolMeta, deriveTokenEndpoint, extractMeta, fetchDgCaCert, fetchWithIdentity, generateKeypair, isDgUrl, refreshCaCert, registerIdentity, rotateIdentity, saveIdentity, version };
1386
+ export { type AuthConfig, AuthError, type Byok, type ChartOptions, Client, type ClientOptions, ConduitError, ConduitIdentity, type ConstrainOptions, type CreditEstimate, DEFAULT_IDENTITY_DIR, DG_CA_URL, DG_SUBSTRATE_ENDPOINT, type DiscoverOptions, type DiscoverResult, type FlowOptions, type ForgetOptions, type GuideOptions, type GuideRequestOptions, type GuideState, GuidedSession, InvalidConfigError, type Keypair, type MCPPrompt, type MCPResource, type MCPTool, type MtlsConfig, NetworkError, NotInitializedError, OAuthTokenProvider, type PerformOptions, type PerformResult, type PlanOptions, type PrismFocusOptions, type QueryCellOptions, type RateLimit, RateLimitError, type RateLimitStatus, type Receipt, type ReflectOptions, type RefractOptions, type RegisteredIdentity, type RegistrationOptions, type RememberOptions, type RenewalOptions, type RotationOptions, type SavedPaths, ServerError, Subscription, type SubscriptionEvent, type ToolInfo, type ToolMeta, SUBPROTOCOL as WS_SUBPROTOCOL, WsTransport, deriveTokenEndpoint, extractMeta, fetchDgCaCert, fetchWithIdentity, generateKeypair, isDgUrl, refreshCaCert, registerIdentity, rotateIdentity, saveIdentity, version };
package/dist/index.js CHANGED
@@ -360,6 +360,8 @@ __export(index_exports, {
360
360
  OAuthTokenProvider: () => OAuthTokenProvider,
361
361
  RateLimitError: () => RateLimitError,
362
362
  ServerError: () => ServerError,
363
+ WS_SUBPROTOCOL: () => SUBPROTOCOL,
364
+ WsTransport: () => WsTransport,
363
365
  deriveTokenEndpoint: () => deriveTokenEndpoint,
364
366
  extractMeta: () => extractMeta,
365
367
  fetchDgCaCert: () => fetchDgCaCert,
@@ -727,6 +729,295 @@ var JSONRPCTransport = class extends Transport {
727
729
  }
728
730
  };
729
731
 
732
+ // src/transports/ws.ts
733
+ var SUBPROTOCOL = "datagrout-jsonrpc.v1";
734
+ var SUBSCRIPTION_BUFFER = 256;
735
+ var Subscription = class {
736
+ id;
737
+ topic;
738
+ _queue = [];
739
+ _waiters = [];
740
+ _rejecters = [];
741
+ _closed = false;
742
+ constructor(id, topic) {
743
+ this.id = id;
744
+ this.topic = topic;
745
+ }
746
+ /**
747
+ * Wait for the next event from this subscription.
748
+ *
749
+ * @throws When the subscription has been closed.
750
+ */
751
+ recv() {
752
+ if (this._queue.length > 0) {
753
+ return Promise.resolve(this._queue.shift());
754
+ }
755
+ if (this._closed) {
756
+ return Promise.reject(new Error("Subscription closed"));
757
+ }
758
+ return new Promise((resolve, reject) => {
759
+ this._waiters.push(resolve);
760
+ this._rejecters.push(reject);
761
+ });
762
+ }
763
+ async *[Symbol.asyncIterator]() {
764
+ while (this._queue.length > 0 || !this._closed) {
765
+ try {
766
+ yield await this.recv();
767
+ } catch {
768
+ return;
769
+ }
770
+ }
771
+ }
772
+ // ── Internal ───────────────────────────────────────────────────────────────
773
+ _enqueue(event) {
774
+ if (this._waiters.length > 0) {
775
+ const resolve = this._waiters.shift();
776
+ this._rejecters.shift();
777
+ resolve(event);
778
+ } else if (this._queue.length < SUBSCRIPTION_BUFFER) {
779
+ this._queue.push(event);
780
+ }
781
+ }
782
+ _close() {
783
+ this._closed = true;
784
+ const err = new Error("Subscription closed");
785
+ for (const reject of this._rejecters) {
786
+ reject(err);
787
+ }
788
+ this._waiters.length = 0;
789
+ this._rejecters.length = 0;
790
+ }
791
+ };
792
+ var WsTransport = class extends Transport {
793
+ _url;
794
+ _auth;
795
+ _ws = null;
796
+ _nextId = 0;
797
+ _pending = /* @__PURE__ */ new Map();
798
+ _pendingSubscribe = /* @__PURE__ */ new Map();
799
+ _subscriptions = /* @__PURE__ */ new Map();
800
+ constructor(url, auth, _timeout, _identity) {
801
+ super();
802
+ const scheme = new URL(url).protocol.replace(":", "");
803
+ if (scheme !== "ws" && scheme !== "wss") {
804
+ throw new Error(`WS transport requires a ws:// or wss:// URL, got ${scheme}://`);
805
+ }
806
+ this._url = url;
807
+ this._auth = auth;
808
+ }
809
+ // ── Lifecycle ─────────────────────────────────────────────────────────────
810
+ async connect() {
811
+ if (this._ws !== null) return;
812
+ const WsImpl = await resolveWebSocketImpl();
813
+ const headers = buildUpgradeHeaders(this._auth);
814
+ const ws = new WsImpl(this._url, [SUBPROTOCOL], {
815
+ headers
816
+ });
817
+ await new Promise((resolve, reject) => {
818
+ ws.onopen = () => resolve();
819
+ ws.onerror = (ev) => reject(new Error(`WS connect failed: ${ev.message ?? "unknown"}`));
820
+ });
821
+ ws.onmessage = (ev) => this._handleMessage(ev.data);
822
+ ws.onerror = (_ev) => this._failAll("WS connection error");
823
+ ws.onclose = () => {
824
+ this._failAll("WS connection closed");
825
+ this._ws = null;
826
+ };
827
+ this._ws = ws;
828
+ }
829
+ async disconnect() {
830
+ const ws = this._ws;
831
+ this._ws = null;
832
+ this._failAll("WS connection closed");
833
+ if (ws !== null) {
834
+ try {
835
+ ws.close();
836
+ } catch {
837
+ }
838
+ }
839
+ }
840
+ // ── Subscriptions ─────────────────────────────────────────────────────────
841
+ /**
842
+ * Open a server-side push subscription for `topic`.
843
+ *
844
+ * @param topic - Dotted namespace topic, e.g.
845
+ * `"agents.my-agent-id.events"` or `"tasks.task-123.*"`.
846
+ * @returns A {@link Subscription} handle whose async-for loop delivers events.
847
+ */
848
+ async subscribe(topic) {
849
+ this._requireConnected();
850
+ const id = this._mintId();
851
+ return new Promise((resolve, reject) => {
852
+ this._pendingSubscribe.set(id, { topic, resolve, reject });
853
+ this._send({ jsonrpc: "2.0", id, method: "subscribe", params: { topic } });
854
+ });
855
+ }
856
+ /**
857
+ * Cancel a server-side subscription.
858
+ *
859
+ * The local {@link Subscription} queue is closed immediately.
860
+ *
861
+ * @param subscriptionId - The `id` field from the {@link Subscription}
862
+ * returned by {@link subscribe}.
863
+ */
864
+ async unsubscribe(subscriptionId) {
865
+ this._requireConnected();
866
+ const sub = this._subscriptions.get(subscriptionId);
867
+ if (sub !== void 0) {
868
+ this._subscriptions.delete(subscriptionId);
869
+ sub._close();
870
+ }
871
+ const id = this._mintId();
872
+ const ackPromise = new Promise((resolve, reject) => {
873
+ this._pending.set(id, { resolve, reject });
874
+ });
875
+ this._send({
876
+ jsonrpc: "2.0",
877
+ id,
878
+ method: "unsubscribe",
879
+ params: { subscription: subscriptionId }
880
+ });
881
+ await Promise.race([
882
+ ackPromise,
883
+ new Promise((resolve) => setTimeout(resolve, 5e3))
884
+ ]);
885
+ this._pending.delete(id);
886
+ }
887
+ // ── Transport base implementation ─────────────────────────────────────────
888
+ async listTools(options) {
889
+ return await this._request("tools/list", options);
890
+ }
891
+ async callTool(name, args, _options) {
892
+ return this._request("tools/call", { name, arguments: args });
893
+ }
894
+ async listResources(_options) {
895
+ return await this._request("resources/list");
896
+ }
897
+ async readResource(uri, _options) {
898
+ return this._request("resources/read", { uri });
899
+ }
900
+ async listPrompts(_options) {
901
+ return await this._request("prompts/list");
902
+ }
903
+ async getPrompt(name, args, _options) {
904
+ return this._request("prompts/get", { name, arguments: args });
905
+ }
906
+ // ── Internal ──────────────────────────────────────────────────────────────
907
+ _mintId() {
908
+ return `ws-${++this._nextId}`;
909
+ }
910
+ _requireConnected() {
911
+ if (this._ws === null) {
912
+ throw new Error("WS transport not connected. Call connect() first.");
913
+ }
914
+ }
915
+ _send(payload) {
916
+ this._ws.send(JSON.stringify(payload));
917
+ }
918
+ async _request(method, params) {
919
+ this._requireConnected();
920
+ const id = this._mintId();
921
+ return new Promise((resolve, reject) => {
922
+ this._pending.set(id, { resolve, reject });
923
+ this._send({ jsonrpc: "2.0", id, method, ...params !== void 0 ? { params } : {} });
924
+ });
925
+ }
926
+ _handleMessage(data) {
927
+ let msg;
928
+ try {
929
+ msg = JSON.parse(data);
930
+ } catch {
931
+ return;
932
+ }
933
+ if (!("id" in msg)) {
934
+ if (msg["method"] === "notification") {
935
+ this._routeNotification(msg["params"]);
936
+ }
937
+ return;
938
+ }
939
+ const msgId = String(msg["id"]);
940
+ const pendingSub = this._pendingSubscribe.get(msgId);
941
+ if (pendingSub !== void 0) {
942
+ this._pendingSubscribe.delete(msgId);
943
+ const err = msg["error"];
944
+ if (err !== void 0) {
945
+ pendingSub.reject(new Error(String(err["message"] ?? "Subscribe failed")));
946
+ return;
947
+ }
948
+ const result = msg["result"] ?? {};
949
+ const subId = String(result["subscription"] ?? msgId);
950
+ const sub = new Subscription(subId, pendingSub.topic);
951
+ this._subscriptions.set(subId, sub);
952
+ pendingSub.resolve(sub);
953
+ return;
954
+ }
955
+ const pending = this._pending.get(msgId);
956
+ if (pending !== void 0) {
957
+ this._pending.delete(msgId);
958
+ const err = msg["error"];
959
+ if (err !== void 0) {
960
+ pending.reject(new Error(String(err["message"] ?? "RPC error")));
961
+ } else {
962
+ pending.resolve(msg["result"]);
963
+ }
964
+ }
965
+ }
966
+ _routeNotification(params) {
967
+ if (params === void 0) return;
968
+ const subId = params["subscription"];
969
+ if (typeof subId !== "string") return;
970
+ const sub = this._subscriptions.get(subId);
971
+ if (sub === void 0) return;
972
+ sub._enqueue({
973
+ subscription: subId,
974
+ event: String(params["event"] ?? ""),
975
+ data: params["data"]
976
+ });
977
+ }
978
+ _failAll(reason) {
979
+ const err = new Error(reason);
980
+ for (const { reject } of this._pending.values()) {
981
+ reject(err);
982
+ }
983
+ this._pending.clear();
984
+ for (const { reject } of this._pendingSubscribe.values()) {
985
+ reject(err);
986
+ }
987
+ this._pendingSubscribe.clear();
988
+ for (const sub of this._subscriptions.values()) {
989
+ sub._close();
990
+ }
991
+ this._subscriptions.clear();
992
+ }
993
+ };
994
+ function buildUpgradeHeaders(auth) {
995
+ const headers = {};
996
+ if (auth === void 0) return headers;
997
+ if ("bearer" in auth && auth.bearer !== void 0) {
998
+ headers["Authorization"] = `Bearer ${auth.bearer}`;
999
+ } else if ("apiKey" in auth && auth.apiKey !== void 0) {
1000
+ headers["X-API-Key"] = auth.apiKey;
1001
+ } else if ("basic" in auth && auth.basic !== void 0) {
1002
+ const encoded = Buffer.from(`${auth.basic.username}:${auth.basic.password}`).toString("base64");
1003
+ headers["Authorization"] = `Basic ${encoded}`;
1004
+ }
1005
+ return headers;
1006
+ }
1007
+ async function resolveWebSocketImpl() {
1008
+ if (typeof globalThis.WebSocket !== "undefined") {
1009
+ return globalThis.WebSocket;
1010
+ }
1011
+ try {
1012
+ const { default: WS } = await import("ws");
1013
+ return WS;
1014
+ } catch {
1015
+ throw new Error(
1016
+ "No WebSocket implementation found. Install the 'ws' package: npm install ws"
1017
+ );
1018
+ }
1019
+ }
1020
+
730
1021
  // src/client.ts
731
1022
  init_identity();
732
1023
 
@@ -1254,6 +1545,11 @@ var Client2 = class _Client {
1254
1545
  const transportType = options.transport || "mcp";
1255
1546
  if (transportType === "mcp") {
1256
1547
  this.transport = new MCPTransport(this.url, this.auth, identity);
1548
+ } else if (transportType === "websocket") {
1549
+ let wsUrl = this.url;
1550
+ if (wsUrl.startsWith("https://")) wsUrl = "wss://" + wsUrl.slice(8);
1551
+ else if (wsUrl.startsWith("http://")) wsUrl = "ws://" + wsUrl.slice(7);
1552
+ this.transport = new WsTransport(wsUrl, this.auth, options.timeout, identity);
1257
1553
  } else {
1258
1554
  const rpcUrl = this.url.endsWith("/mcp") ? this.url.slice(0, -4) + "/rpc" : this.url;
1259
1555
  this.transport = new JSONRPCTransport(rpcUrl, this.auth, options.timeout, identity);
@@ -1488,6 +1784,50 @@ var Client2 = class _Client {
1488
1784
  this.ensureInitialized();
1489
1785
  return this.sendWithRetry(() => this.transport.getPrompt(name, args, options));
1490
1786
  }
1787
+ // ===== WebSocket push subscriptions =====
1788
+ /**
1789
+ * Subscribe to a server-push topic (WebSocket transport only).
1790
+ *
1791
+ * Requires `transport: 'websocket'` when constructing the client.
1792
+ *
1793
+ * @param topic - Dotted namespace topic, e.g.
1794
+ * `"agents.my-agent-id.events"` or `"tasks.task-123.*"`.
1795
+ * @returns A {@link Subscription} handle. Consume events with
1796
+ * {@link Subscription.recv} or an `for await` loop.
1797
+ *
1798
+ * @example
1799
+ * ```ts
1800
+ * const sub = await client.subscribe('agents.my-agent-id.events');
1801
+ * for await (const event of sub) {
1802
+ * console.log(event.event, event.data);
1803
+ * }
1804
+ * await client.unsubscribe(sub.id);
1805
+ * ```
1806
+ */
1807
+ async subscribe(topic) {
1808
+ this.ensureInitialized();
1809
+ if (!(this.transport instanceof WsTransport)) {
1810
+ throw new Error(
1811
+ "subscribe() requires transport: 'websocket'. Reinitialise the client with transport: 'websocket'."
1812
+ );
1813
+ }
1814
+ return this.transport.subscribe(topic);
1815
+ }
1816
+ /**
1817
+ * Cancel a server-side push subscription.
1818
+ *
1819
+ * @param subscriptionId - The `id` from the {@link Subscription} returned
1820
+ * by {@link subscribe}.
1821
+ */
1822
+ async unsubscribe(subscriptionId) {
1823
+ this.ensureInitialized();
1824
+ if (!(this.transport instanceof WsTransport)) {
1825
+ throw new Error(
1826
+ "unsubscribe() requires transport: 'websocket'. Reinitialise the client with transport: 'websocket'."
1827
+ );
1828
+ }
1829
+ return this.transport.unsubscribe(subscriptionId);
1830
+ }
1491
1831
  // ===== DG-awareness helpers =====
1492
1832
  warnIfNotDg(method) {
1493
1833
  if (!this.isDg && !this.dgWarned) {
@@ -1778,7 +2118,7 @@ function buildToolMeta(raw) {
1778
2118
  }
1779
2119
 
1780
2120
  // src/index.ts
1781
- var version = "0.1.0";
2121
+ var version = "0.4.0";
1782
2122
  // Annotate the CommonJS export names for ESM import in node:
1783
2123
  0 && (module.exports = {
1784
2124
  AuthError,
@@ -1795,6 +2135,8 @@ var version = "0.1.0";
1795
2135
  OAuthTokenProvider,
1796
2136
  RateLimitError,
1797
2137
  ServerError,
2138
+ WS_SUBPROTOCOL,
2139
+ WsTransport,
1798
2140
  deriveTokenEndpoint,
1799
2141
  extractMeta,
1800
2142
  fetchDgCaCert,
package/dist/index.mjs CHANGED
@@ -589,6 +589,295 @@ var JSONRPCTransport = class extends Transport {
589
589
  }
590
590
  };
591
591
 
592
+ // src/transports/ws.ts
593
+ var SUBPROTOCOL = "datagrout-jsonrpc.v1";
594
+ var SUBSCRIPTION_BUFFER = 256;
595
+ var Subscription = class {
596
+ id;
597
+ topic;
598
+ _queue = [];
599
+ _waiters = [];
600
+ _rejecters = [];
601
+ _closed = false;
602
+ constructor(id, topic) {
603
+ this.id = id;
604
+ this.topic = topic;
605
+ }
606
+ /**
607
+ * Wait for the next event from this subscription.
608
+ *
609
+ * @throws When the subscription has been closed.
610
+ */
611
+ recv() {
612
+ if (this._queue.length > 0) {
613
+ return Promise.resolve(this._queue.shift());
614
+ }
615
+ if (this._closed) {
616
+ return Promise.reject(new Error("Subscription closed"));
617
+ }
618
+ return new Promise((resolve, reject) => {
619
+ this._waiters.push(resolve);
620
+ this._rejecters.push(reject);
621
+ });
622
+ }
623
+ async *[Symbol.asyncIterator]() {
624
+ while (this._queue.length > 0 || !this._closed) {
625
+ try {
626
+ yield await this.recv();
627
+ } catch {
628
+ return;
629
+ }
630
+ }
631
+ }
632
+ // ── Internal ───────────────────────────────────────────────────────────────
633
+ _enqueue(event) {
634
+ if (this._waiters.length > 0) {
635
+ const resolve = this._waiters.shift();
636
+ this._rejecters.shift();
637
+ resolve(event);
638
+ } else if (this._queue.length < SUBSCRIPTION_BUFFER) {
639
+ this._queue.push(event);
640
+ }
641
+ }
642
+ _close() {
643
+ this._closed = true;
644
+ const err = new Error("Subscription closed");
645
+ for (const reject of this._rejecters) {
646
+ reject(err);
647
+ }
648
+ this._waiters.length = 0;
649
+ this._rejecters.length = 0;
650
+ }
651
+ };
652
+ var WsTransport = class extends Transport {
653
+ _url;
654
+ _auth;
655
+ _ws = null;
656
+ _nextId = 0;
657
+ _pending = /* @__PURE__ */ new Map();
658
+ _pendingSubscribe = /* @__PURE__ */ new Map();
659
+ _subscriptions = /* @__PURE__ */ new Map();
660
+ constructor(url, auth, _timeout, _identity) {
661
+ super();
662
+ const scheme = new URL(url).protocol.replace(":", "");
663
+ if (scheme !== "ws" && scheme !== "wss") {
664
+ throw new Error(`WS transport requires a ws:// or wss:// URL, got ${scheme}://`);
665
+ }
666
+ this._url = url;
667
+ this._auth = auth;
668
+ }
669
+ // ── Lifecycle ─────────────────────────────────────────────────────────────
670
+ async connect() {
671
+ if (this._ws !== null) return;
672
+ const WsImpl = await resolveWebSocketImpl();
673
+ const headers = buildUpgradeHeaders(this._auth);
674
+ const ws = new WsImpl(this._url, [SUBPROTOCOL], {
675
+ headers
676
+ });
677
+ await new Promise((resolve, reject) => {
678
+ ws.onopen = () => resolve();
679
+ ws.onerror = (ev) => reject(new Error(`WS connect failed: ${ev.message ?? "unknown"}`));
680
+ });
681
+ ws.onmessage = (ev) => this._handleMessage(ev.data);
682
+ ws.onerror = (_ev) => this._failAll("WS connection error");
683
+ ws.onclose = () => {
684
+ this._failAll("WS connection closed");
685
+ this._ws = null;
686
+ };
687
+ this._ws = ws;
688
+ }
689
+ async disconnect() {
690
+ const ws = this._ws;
691
+ this._ws = null;
692
+ this._failAll("WS connection closed");
693
+ if (ws !== null) {
694
+ try {
695
+ ws.close();
696
+ } catch {
697
+ }
698
+ }
699
+ }
700
+ // ── Subscriptions ─────────────────────────────────────────────────────────
701
+ /**
702
+ * Open a server-side push subscription for `topic`.
703
+ *
704
+ * @param topic - Dotted namespace topic, e.g.
705
+ * `"agents.my-agent-id.events"` or `"tasks.task-123.*"`.
706
+ * @returns A {@link Subscription} handle whose async-for loop delivers events.
707
+ */
708
+ async subscribe(topic) {
709
+ this._requireConnected();
710
+ const id = this._mintId();
711
+ return new Promise((resolve, reject) => {
712
+ this._pendingSubscribe.set(id, { topic, resolve, reject });
713
+ this._send({ jsonrpc: "2.0", id, method: "subscribe", params: { topic } });
714
+ });
715
+ }
716
+ /**
717
+ * Cancel a server-side subscription.
718
+ *
719
+ * The local {@link Subscription} queue is closed immediately.
720
+ *
721
+ * @param subscriptionId - The `id` field from the {@link Subscription}
722
+ * returned by {@link subscribe}.
723
+ */
724
+ async unsubscribe(subscriptionId) {
725
+ this._requireConnected();
726
+ const sub = this._subscriptions.get(subscriptionId);
727
+ if (sub !== void 0) {
728
+ this._subscriptions.delete(subscriptionId);
729
+ sub._close();
730
+ }
731
+ const id = this._mintId();
732
+ const ackPromise = new Promise((resolve, reject) => {
733
+ this._pending.set(id, { resolve, reject });
734
+ });
735
+ this._send({
736
+ jsonrpc: "2.0",
737
+ id,
738
+ method: "unsubscribe",
739
+ params: { subscription: subscriptionId }
740
+ });
741
+ await Promise.race([
742
+ ackPromise,
743
+ new Promise((resolve) => setTimeout(resolve, 5e3))
744
+ ]);
745
+ this._pending.delete(id);
746
+ }
747
+ // ── Transport base implementation ─────────────────────────────────────────
748
+ async listTools(options) {
749
+ return await this._request("tools/list", options);
750
+ }
751
+ async callTool(name, args, _options) {
752
+ return this._request("tools/call", { name, arguments: args });
753
+ }
754
+ async listResources(_options) {
755
+ return await this._request("resources/list");
756
+ }
757
+ async readResource(uri, _options) {
758
+ return this._request("resources/read", { uri });
759
+ }
760
+ async listPrompts(_options) {
761
+ return await this._request("prompts/list");
762
+ }
763
+ async getPrompt(name, args, _options) {
764
+ return this._request("prompts/get", { name, arguments: args });
765
+ }
766
+ // ── Internal ──────────────────────────────────────────────────────────────
767
+ _mintId() {
768
+ return `ws-${++this._nextId}`;
769
+ }
770
+ _requireConnected() {
771
+ if (this._ws === null) {
772
+ throw new Error("WS transport not connected. Call connect() first.");
773
+ }
774
+ }
775
+ _send(payload) {
776
+ this._ws.send(JSON.stringify(payload));
777
+ }
778
+ async _request(method, params) {
779
+ this._requireConnected();
780
+ const id = this._mintId();
781
+ return new Promise((resolve, reject) => {
782
+ this._pending.set(id, { resolve, reject });
783
+ this._send({ jsonrpc: "2.0", id, method, ...params !== void 0 ? { params } : {} });
784
+ });
785
+ }
786
+ _handleMessage(data) {
787
+ let msg;
788
+ try {
789
+ msg = JSON.parse(data);
790
+ } catch {
791
+ return;
792
+ }
793
+ if (!("id" in msg)) {
794
+ if (msg["method"] === "notification") {
795
+ this._routeNotification(msg["params"]);
796
+ }
797
+ return;
798
+ }
799
+ const msgId = String(msg["id"]);
800
+ const pendingSub = this._pendingSubscribe.get(msgId);
801
+ if (pendingSub !== void 0) {
802
+ this._pendingSubscribe.delete(msgId);
803
+ const err = msg["error"];
804
+ if (err !== void 0) {
805
+ pendingSub.reject(new Error(String(err["message"] ?? "Subscribe failed")));
806
+ return;
807
+ }
808
+ const result = msg["result"] ?? {};
809
+ const subId = String(result["subscription"] ?? msgId);
810
+ const sub = new Subscription(subId, pendingSub.topic);
811
+ this._subscriptions.set(subId, sub);
812
+ pendingSub.resolve(sub);
813
+ return;
814
+ }
815
+ const pending = this._pending.get(msgId);
816
+ if (pending !== void 0) {
817
+ this._pending.delete(msgId);
818
+ const err = msg["error"];
819
+ if (err !== void 0) {
820
+ pending.reject(new Error(String(err["message"] ?? "RPC error")));
821
+ } else {
822
+ pending.resolve(msg["result"]);
823
+ }
824
+ }
825
+ }
826
+ _routeNotification(params) {
827
+ if (params === void 0) return;
828
+ const subId = params["subscription"];
829
+ if (typeof subId !== "string") return;
830
+ const sub = this._subscriptions.get(subId);
831
+ if (sub === void 0) return;
832
+ sub._enqueue({
833
+ subscription: subId,
834
+ event: String(params["event"] ?? ""),
835
+ data: params["data"]
836
+ });
837
+ }
838
+ _failAll(reason) {
839
+ const err = new Error(reason);
840
+ for (const { reject } of this._pending.values()) {
841
+ reject(err);
842
+ }
843
+ this._pending.clear();
844
+ for (const { reject } of this._pendingSubscribe.values()) {
845
+ reject(err);
846
+ }
847
+ this._pendingSubscribe.clear();
848
+ for (const sub of this._subscriptions.values()) {
849
+ sub._close();
850
+ }
851
+ this._subscriptions.clear();
852
+ }
853
+ };
854
+ function buildUpgradeHeaders(auth) {
855
+ const headers = {};
856
+ if (auth === void 0) return headers;
857
+ if ("bearer" in auth && auth.bearer !== void 0) {
858
+ headers["Authorization"] = `Bearer ${auth.bearer}`;
859
+ } else if ("apiKey" in auth && auth.apiKey !== void 0) {
860
+ headers["X-API-Key"] = auth.apiKey;
861
+ } else if ("basic" in auth && auth.basic !== void 0) {
862
+ const encoded = Buffer.from(`${auth.basic.username}:${auth.basic.password}`).toString("base64");
863
+ headers["Authorization"] = `Basic ${encoded}`;
864
+ }
865
+ return headers;
866
+ }
867
+ async function resolveWebSocketImpl() {
868
+ if (typeof globalThis.WebSocket !== "undefined") {
869
+ return globalThis.WebSocket;
870
+ }
871
+ try {
872
+ const { default: WS } = await import("ws");
873
+ return WS;
874
+ } catch {
875
+ throw new Error(
876
+ "No WebSocket implementation found. Install the 'ws' package: npm install ws"
877
+ );
878
+ }
879
+ }
880
+
592
881
  // src/client.ts
593
882
  init_identity();
594
883
 
@@ -1116,6 +1405,11 @@ var Client2 = class _Client {
1116
1405
  const transportType = options.transport || "mcp";
1117
1406
  if (transportType === "mcp") {
1118
1407
  this.transport = new MCPTransport(this.url, this.auth, identity);
1408
+ } else if (transportType === "websocket") {
1409
+ let wsUrl = this.url;
1410
+ if (wsUrl.startsWith("https://")) wsUrl = "wss://" + wsUrl.slice(8);
1411
+ else if (wsUrl.startsWith("http://")) wsUrl = "ws://" + wsUrl.slice(7);
1412
+ this.transport = new WsTransport(wsUrl, this.auth, options.timeout, identity);
1119
1413
  } else {
1120
1414
  const rpcUrl = this.url.endsWith("/mcp") ? this.url.slice(0, -4) + "/rpc" : this.url;
1121
1415
  this.transport = new JSONRPCTransport(rpcUrl, this.auth, options.timeout, identity);
@@ -1350,6 +1644,50 @@ var Client2 = class _Client {
1350
1644
  this.ensureInitialized();
1351
1645
  return this.sendWithRetry(() => this.transport.getPrompt(name, args, options));
1352
1646
  }
1647
+ // ===== WebSocket push subscriptions =====
1648
+ /**
1649
+ * Subscribe to a server-push topic (WebSocket transport only).
1650
+ *
1651
+ * Requires `transport: 'websocket'` when constructing the client.
1652
+ *
1653
+ * @param topic - Dotted namespace topic, e.g.
1654
+ * `"agents.my-agent-id.events"` or `"tasks.task-123.*"`.
1655
+ * @returns A {@link Subscription} handle. Consume events with
1656
+ * {@link Subscription.recv} or an `for await` loop.
1657
+ *
1658
+ * @example
1659
+ * ```ts
1660
+ * const sub = await client.subscribe('agents.my-agent-id.events');
1661
+ * for await (const event of sub) {
1662
+ * console.log(event.event, event.data);
1663
+ * }
1664
+ * await client.unsubscribe(sub.id);
1665
+ * ```
1666
+ */
1667
+ async subscribe(topic) {
1668
+ this.ensureInitialized();
1669
+ if (!(this.transport instanceof WsTransport)) {
1670
+ throw new Error(
1671
+ "subscribe() requires transport: 'websocket'. Reinitialise the client with transport: 'websocket'."
1672
+ );
1673
+ }
1674
+ return this.transport.subscribe(topic);
1675
+ }
1676
+ /**
1677
+ * Cancel a server-side push subscription.
1678
+ *
1679
+ * @param subscriptionId - The `id` from the {@link Subscription} returned
1680
+ * by {@link subscribe}.
1681
+ */
1682
+ async unsubscribe(subscriptionId) {
1683
+ this.ensureInitialized();
1684
+ if (!(this.transport instanceof WsTransport)) {
1685
+ throw new Error(
1686
+ "unsubscribe() requires transport: 'websocket'. Reinitialise the client with transport: 'websocket'."
1687
+ );
1688
+ }
1689
+ return this.transport.unsubscribe(subscriptionId);
1690
+ }
1353
1691
  // ===== DG-awareness helpers =====
1354
1692
  warnIfNotDg(method) {
1355
1693
  if (!this.isDg && !this.dgWarned) {
@@ -1640,7 +1978,7 @@ function buildToolMeta(raw) {
1640
1978
  }
1641
1979
 
1642
1980
  // src/index.ts
1643
- var version = "0.1.0";
1981
+ var version = "0.4.0";
1644
1982
  export {
1645
1983
  AuthError,
1646
1984
  Client2 as Client,
@@ -1656,6 +1994,8 @@ export {
1656
1994
  OAuthTokenProvider,
1657
1995
  RateLimitError,
1658
1996
  ServerError,
1997
+ SUBPROTOCOL as WS_SUBPROTOCOL,
1998
+ WsTransport,
1659
1999
  deriveTokenEndpoint,
1660
2000
  extractMeta,
1661
2001
  fetchDgCaCert,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@datagrout/conduit",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Production-ready MCP client with mTLS, OAuth 2.1, and semantic discovery",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -45,10 +45,12 @@
45
45
  "url": "https://github.com/DataGrout/conduit-sdk/issues"
46
46
  },
47
47
  "dependencies": {
48
- "@modelcontextprotocol/sdk": "^1.0.0"
48
+ "@modelcontextprotocol/sdk": "^1.0.0",
49
+ "ws": "^8.18.0"
49
50
  },
50
51
  "devDependencies": {
51
52
  "@types/node": "^20.11.0",
53
+ "@types/ws": "^8.5.10",
52
54
  "@typescript-eslint/eslint-plugin": "^6.19.0",
53
55
  "@typescript-eslint/parser": "^6.19.0",
54
56
  "eslint": "^8.56.0",