@datagrout/conduit 0.3.0 → 0.5.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/dist/index.mjs CHANGED
@@ -1,13 +1,19 @@
1
1
  import {
2
2
  OAuthTokenProvider,
3
- __esm,
4
- __export,
5
- __require,
6
- __toCommonJS,
7
3
  deriveTokenEndpoint,
8
4
  init_oauth,
9
5
  oauth_exports
10
- } from "./chunk-RED5DKGI.mjs";
6
+ } from "./chunk-26DYCD4G.mjs";
7
+ import {
8
+ registerAndExchange,
9
+ registerOnly
10
+ } from "./chunk-SWH5Y2U7.mjs";
11
+ import {
12
+ __esm,
13
+ __export,
14
+ __require,
15
+ __toCommonJS
16
+ } from "./chunk-CIESM3BP.mjs";
11
17
 
12
18
  // src/identity.ts
13
19
  var identity_exports = {};
@@ -589,6 +595,295 @@ var JSONRPCTransport = class extends Transport {
589
595
  }
590
596
  };
591
597
 
598
+ // src/transports/ws.ts
599
+ var SUBPROTOCOL = "datagrout-jsonrpc.v1";
600
+ var SUBSCRIPTION_BUFFER = 256;
601
+ var Subscription = class {
602
+ id;
603
+ topic;
604
+ _queue = [];
605
+ _waiters = [];
606
+ _rejecters = [];
607
+ _closed = false;
608
+ constructor(id, topic) {
609
+ this.id = id;
610
+ this.topic = topic;
611
+ }
612
+ /**
613
+ * Wait for the next event from this subscription.
614
+ *
615
+ * @throws When the subscription has been closed.
616
+ */
617
+ recv() {
618
+ if (this._queue.length > 0) {
619
+ return Promise.resolve(this._queue.shift());
620
+ }
621
+ if (this._closed) {
622
+ return Promise.reject(new Error("Subscription closed"));
623
+ }
624
+ return new Promise((resolve, reject) => {
625
+ this._waiters.push(resolve);
626
+ this._rejecters.push(reject);
627
+ });
628
+ }
629
+ async *[Symbol.asyncIterator]() {
630
+ while (this._queue.length > 0 || !this._closed) {
631
+ try {
632
+ yield await this.recv();
633
+ } catch {
634
+ return;
635
+ }
636
+ }
637
+ }
638
+ // ── Internal ───────────────────────────────────────────────────────────────
639
+ _enqueue(event) {
640
+ if (this._waiters.length > 0) {
641
+ const resolve = this._waiters.shift();
642
+ this._rejecters.shift();
643
+ resolve(event);
644
+ } else if (this._queue.length < SUBSCRIPTION_BUFFER) {
645
+ this._queue.push(event);
646
+ }
647
+ }
648
+ _close() {
649
+ this._closed = true;
650
+ const err = new Error("Subscription closed");
651
+ for (const reject of this._rejecters) {
652
+ reject(err);
653
+ }
654
+ this._waiters.length = 0;
655
+ this._rejecters.length = 0;
656
+ }
657
+ };
658
+ var WsTransport = class extends Transport {
659
+ _url;
660
+ _auth;
661
+ _ws = null;
662
+ _nextId = 0;
663
+ _pending = /* @__PURE__ */ new Map();
664
+ _pendingSubscribe = /* @__PURE__ */ new Map();
665
+ _subscriptions = /* @__PURE__ */ new Map();
666
+ constructor(url, auth, _timeout, _identity) {
667
+ super();
668
+ const scheme = new URL(url).protocol.replace(":", "");
669
+ if (scheme !== "ws" && scheme !== "wss") {
670
+ throw new Error(`WS transport requires a ws:// or wss:// URL, got ${scheme}://`);
671
+ }
672
+ this._url = url;
673
+ this._auth = auth;
674
+ }
675
+ // ── Lifecycle ─────────────────────────────────────────────────────────────
676
+ async connect() {
677
+ if (this._ws !== null) return;
678
+ const WsImpl = await resolveWebSocketImpl();
679
+ const headers = buildUpgradeHeaders(this._auth);
680
+ const ws = new WsImpl(this._url, [SUBPROTOCOL], {
681
+ headers
682
+ });
683
+ await new Promise((resolve, reject) => {
684
+ ws.onopen = () => resolve();
685
+ ws.onerror = (ev) => reject(new Error(`WS connect failed: ${ev.message ?? "unknown"}`));
686
+ });
687
+ ws.onmessage = (ev) => this._handleMessage(ev.data);
688
+ ws.onerror = (_ev) => this._failAll("WS connection error");
689
+ ws.onclose = () => {
690
+ this._failAll("WS connection closed");
691
+ this._ws = null;
692
+ };
693
+ this._ws = ws;
694
+ }
695
+ async disconnect() {
696
+ const ws = this._ws;
697
+ this._ws = null;
698
+ this._failAll("WS connection closed");
699
+ if (ws !== null) {
700
+ try {
701
+ ws.close();
702
+ } catch {
703
+ }
704
+ }
705
+ }
706
+ // ── Subscriptions ─────────────────────────────────────────────────────────
707
+ /**
708
+ * Open a server-side push subscription for `topic`.
709
+ *
710
+ * @param topic - Dotted namespace topic, e.g.
711
+ * `"agents.my-agent-id.events"` or `"tasks.task-123.*"`.
712
+ * @returns A {@link Subscription} handle whose async-for loop delivers events.
713
+ */
714
+ async subscribe(topic) {
715
+ this._requireConnected();
716
+ const id = this._mintId();
717
+ return new Promise((resolve, reject) => {
718
+ this._pendingSubscribe.set(id, { topic, resolve, reject });
719
+ this._send({ jsonrpc: "2.0", id, method: "subscribe", params: { topic } });
720
+ });
721
+ }
722
+ /**
723
+ * Cancel a server-side subscription.
724
+ *
725
+ * The local {@link Subscription} queue is closed immediately.
726
+ *
727
+ * @param subscriptionId - The `id` field from the {@link Subscription}
728
+ * returned by {@link subscribe}.
729
+ */
730
+ async unsubscribe(subscriptionId) {
731
+ this._requireConnected();
732
+ const sub = this._subscriptions.get(subscriptionId);
733
+ if (sub !== void 0) {
734
+ this._subscriptions.delete(subscriptionId);
735
+ sub._close();
736
+ }
737
+ const id = this._mintId();
738
+ const ackPromise = new Promise((resolve, reject) => {
739
+ this._pending.set(id, { resolve, reject });
740
+ });
741
+ this._send({
742
+ jsonrpc: "2.0",
743
+ id,
744
+ method: "unsubscribe",
745
+ params: { subscription: subscriptionId }
746
+ });
747
+ await Promise.race([
748
+ ackPromise,
749
+ new Promise((resolve) => setTimeout(resolve, 5e3))
750
+ ]);
751
+ this._pending.delete(id);
752
+ }
753
+ // ── Transport base implementation ─────────────────────────────────────────
754
+ async listTools(options) {
755
+ return await this._request("tools/list", options);
756
+ }
757
+ async callTool(name, args, _options) {
758
+ return this._request("tools/call", { name, arguments: args });
759
+ }
760
+ async listResources(_options) {
761
+ return await this._request("resources/list");
762
+ }
763
+ async readResource(uri, _options) {
764
+ return this._request("resources/read", { uri });
765
+ }
766
+ async listPrompts(_options) {
767
+ return await this._request("prompts/list");
768
+ }
769
+ async getPrompt(name, args, _options) {
770
+ return this._request("prompts/get", { name, arguments: args });
771
+ }
772
+ // ── Internal ──────────────────────────────────────────────────────────────
773
+ _mintId() {
774
+ return `ws-${++this._nextId}`;
775
+ }
776
+ _requireConnected() {
777
+ if (this._ws === null) {
778
+ throw new Error("WS transport not connected. Call connect() first.");
779
+ }
780
+ }
781
+ _send(payload) {
782
+ this._ws.send(JSON.stringify(payload));
783
+ }
784
+ async _request(method, params) {
785
+ this._requireConnected();
786
+ const id = this._mintId();
787
+ return new Promise((resolve, reject) => {
788
+ this._pending.set(id, { resolve, reject });
789
+ this._send({ jsonrpc: "2.0", id, method, ...params !== void 0 ? { params } : {} });
790
+ });
791
+ }
792
+ _handleMessage(data) {
793
+ let msg;
794
+ try {
795
+ msg = JSON.parse(data);
796
+ } catch {
797
+ return;
798
+ }
799
+ if (!("id" in msg)) {
800
+ if (msg["method"] === "notification") {
801
+ this._routeNotification(msg["params"]);
802
+ }
803
+ return;
804
+ }
805
+ const msgId = String(msg["id"]);
806
+ const pendingSub = this._pendingSubscribe.get(msgId);
807
+ if (pendingSub !== void 0) {
808
+ this._pendingSubscribe.delete(msgId);
809
+ const err = msg["error"];
810
+ if (err !== void 0) {
811
+ pendingSub.reject(new Error(String(err["message"] ?? "Subscribe failed")));
812
+ return;
813
+ }
814
+ const result = msg["result"] ?? {};
815
+ const subId = String(result["subscription"] ?? msgId);
816
+ const sub = new Subscription(subId, pendingSub.topic);
817
+ this._subscriptions.set(subId, sub);
818
+ pendingSub.resolve(sub);
819
+ return;
820
+ }
821
+ const pending = this._pending.get(msgId);
822
+ if (pending !== void 0) {
823
+ this._pending.delete(msgId);
824
+ const err = msg["error"];
825
+ if (err !== void 0) {
826
+ pending.reject(new Error(String(err["message"] ?? "RPC error")));
827
+ } else {
828
+ pending.resolve(msg["result"]);
829
+ }
830
+ }
831
+ }
832
+ _routeNotification(params) {
833
+ if (params === void 0) return;
834
+ const subId = params["subscription"];
835
+ if (typeof subId !== "string") return;
836
+ const sub = this._subscriptions.get(subId);
837
+ if (sub === void 0) return;
838
+ sub._enqueue({
839
+ subscription: subId,
840
+ event: String(params["event"] ?? ""),
841
+ data: params["data"]
842
+ });
843
+ }
844
+ _failAll(reason) {
845
+ const err = new Error(reason);
846
+ for (const { reject } of this._pending.values()) {
847
+ reject(err);
848
+ }
849
+ this._pending.clear();
850
+ for (const { reject } of this._pendingSubscribe.values()) {
851
+ reject(err);
852
+ }
853
+ this._pendingSubscribe.clear();
854
+ for (const sub of this._subscriptions.values()) {
855
+ sub._close();
856
+ }
857
+ this._subscriptions.clear();
858
+ }
859
+ };
860
+ function buildUpgradeHeaders(auth) {
861
+ const headers = {};
862
+ if (auth === void 0) return headers;
863
+ if ("bearer" in auth && auth.bearer !== void 0) {
864
+ headers["Authorization"] = `Bearer ${auth.bearer}`;
865
+ } else if ("apiKey" in auth && auth.apiKey !== void 0) {
866
+ headers["X-API-Key"] = auth.apiKey;
867
+ } else if ("basic" in auth && auth.basic !== void 0) {
868
+ const encoded = Buffer.from(`${auth.basic.username}:${auth.basic.password}`).toString("base64");
869
+ headers["Authorization"] = `Basic ${encoded}`;
870
+ }
871
+ return headers;
872
+ }
873
+ async function resolveWebSocketImpl() {
874
+ if (typeof globalThis.WebSocket !== "undefined") {
875
+ return globalThis.WebSocket;
876
+ }
877
+ try {
878
+ const { default: WS } = await import("ws");
879
+ return WS;
880
+ } catch {
881
+ throw new Error(
882
+ "No WebSocket implementation found. Install the 'ws' package: npm install ws"
883
+ );
884
+ }
885
+ }
886
+
592
887
  // src/client.ts
593
888
  init_identity();
594
889
 
@@ -1109,13 +1404,15 @@ var Client2 = class _Client {
1109
1404
  this.isDg = isDgUrl(this.url);
1110
1405
  this.useIntelligentInterface = options.useIntelligentInterface ?? this.isDg;
1111
1406
  this.maxRetries = options.maxRetries ?? 3;
1112
- let identity = options.identity ?? (options.identityAuto ? ConduitIdentity.tryDiscover(options.identityDir) ?? void 0 : void 0);
1113
- if (identity === void 0 && this.isDg && !options.disableMtls) {
1114
- identity = ConduitIdentity.tryDiscover(options.identityDir) ?? void 0;
1115
- }
1407
+ const identity = options.identity ?? (options.identityAuto ? ConduitIdentity.tryDiscover(options.identityDir) ?? void 0 : void 0);
1116
1408
  const transportType = options.transport || "mcp";
1117
1409
  if (transportType === "mcp") {
1118
1410
  this.transport = new MCPTransport(this.url, this.auth, identity);
1411
+ } else if (transportType === "websocket") {
1412
+ let wsUrl = this.url;
1413
+ if (wsUrl.startsWith("https://")) wsUrl = "wss://" + wsUrl.slice(8);
1414
+ else if (wsUrl.startsWith("http://")) wsUrl = "ws://" + wsUrl.slice(7);
1415
+ this.transport = new WsTransport(wsUrl, this.auth, options.timeout, identity);
1119
1416
  } else {
1120
1417
  const rpcUrl = this.url.endsWith("/mcp") ? this.url.slice(0, -4) + "/rpc" : this.url;
1121
1418
  this.transport = new JSONRPCTransport(rpcUrl, this.auth, options.timeout, identity);
@@ -1176,7 +1473,7 @@ var Client2 = class _Client {
1176
1473
  * @param options.substrateEndpoint - Override the DG Substrate endpoint.
1177
1474
  */
1178
1475
  static async bootstrapIdentityOAuth(options) {
1179
- const { OAuthTokenProvider: OAuthTokenProvider2, deriveTokenEndpoint: deriveTokenEndpoint2 } = await import("./oauth-OMPWCI2X.mjs");
1476
+ const { OAuthTokenProvider: OAuthTokenProvider2, deriveTokenEndpoint: deriveTokenEndpoint2 } = await import("./oauth-NSDC2G7W.mjs");
1180
1477
  const tokenEndpoint = deriveTokenEndpoint2(options.url);
1181
1478
  const provider = new OAuthTokenProvider2({
1182
1479
  clientId: options.clientId,
@@ -1192,6 +1489,60 @@ var Client2 = class _Client {
1192
1489
  substrateEndpoint: options.substrateEndpoint
1193
1490
  });
1194
1491
  }
1492
+ /**
1493
+ * Register autonomously with DG and bootstrap an mTLS identity.
1494
+ *
1495
+ * The all-in-one flow: onramp (no prior credentials required) →
1496
+ * OAuth token exchange → mTLS identity registration and persistence.
1497
+ *
1498
+ * On subsequent runs the saved mTLS identity is auto-discovered and
1499
+ * no credentials are needed.
1500
+ *
1501
+ * @param options.opts - Onramp registration options.
1502
+ * @param options.url - MCP server URL. Required if the onramp
1503
+ * response does not include `mcpUrl`.
1504
+ * @param options.identityDir - Custom identity storage directory.
1505
+ *
1506
+ * @example
1507
+ * ```ts
1508
+ * import { Client } from './client';
1509
+ * import type { OnrampOptions } from './onramp';
1510
+ *
1511
+ * const client = await Client.bootstrapOnramp({
1512
+ * opts: {
1513
+ * gateway: 'https://app.datagrout.ai',
1514
+ * agentName: 'my-research-agent',
1515
+ * agentType: 'claude-sonnet-4-6',
1516
+ * },
1517
+ * });
1518
+ * await client.connect();
1519
+ * ```
1520
+ */
1521
+ static async bootstrapOnramp(options) {
1522
+ const { _doRegister, _exchangeToken } = await import("./onramp-743RJTNI.mjs");
1523
+ const dir = options.identityDir || DEFAULT_IDENTITY_DIR;
1524
+ const existing = ConduitIdentity.tryDiscover(dir);
1525
+ if (existing && !existing.needsRotation(7)) {
1526
+ if (!options.url) {
1527
+ throw new Error("'url' must be provided when an existing identity is reused");
1528
+ }
1529
+ return new _Client({ url: options.url, identity: existing, identityDir: dir });
1530
+ }
1531
+ const creds = await _doRegister(options.opts);
1532
+ const token = await _exchangeToken(creds);
1533
+ const url = creds.mcpUrl ?? options.url;
1534
+ if (!url) {
1535
+ throw new Error(
1536
+ "'url' must be provided when mcpUrl is absent from the onramp response"
1537
+ );
1538
+ }
1539
+ return _Client.bootstrapIdentity({
1540
+ url,
1541
+ authToken: token,
1542
+ name: options.opts.agentName,
1543
+ identityDir: options.identityDir
1544
+ });
1545
+ }
1195
1546
  // ===== Lifecycle =====
1196
1547
  /**
1197
1548
  * Establish the underlying transport connection.
@@ -1350,6 +1701,50 @@ var Client2 = class _Client {
1350
1701
  this.ensureInitialized();
1351
1702
  return this.sendWithRetry(() => this.transport.getPrompt(name, args, options));
1352
1703
  }
1704
+ // ===== WebSocket push subscriptions =====
1705
+ /**
1706
+ * Subscribe to a server-push topic (WebSocket transport only).
1707
+ *
1708
+ * Requires `transport: 'websocket'` when constructing the client.
1709
+ *
1710
+ * @param topic - Dotted namespace topic, e.g.
1711
+ * `"agents.my-agent-id.events"` or `"tasks.task-123.*"`.
1712
+ * @returns A {@link Subscription} handle. Consume events with
1713
+ * {@link Subscription.recv} or an `for await` loop.
1714
+ *
1715
+ * @example
1716
+ * ```ts
1717
+ * const sub = await client.subscribe('agents.my-agent-id.events');
1718
+ * for await (const event of sub) {
1719
+ * console.log(event.event, event.data);
1720
+ * }
1721
+ * await client.unsubscribe(sub.id);
1722
+ * ```
1723
+ */
1724
+ async subscribe(topic) {
1725
+ this.ensureInitialized();
1726
+ if (!(this.transport instanceof WsTransport)) {
1727
+ throw new Error(
1728
+ "subscribe() requires transport: 'websocket'. Reinitialise the client with transport: 'websocket'."
1729
+ );
1730
+ }
1731
+ return this.transport.subscribe(topic);
1732
+ }
1733
+ /**
1734
+ * Cancel a server-side push subscription.
1735
+ *
1736
+ * @param subscriptionId - The `id` from the {@link Subscription} returned
1737
+ * by {@link subscribe}.
1738
+ */
1739
+ async unsubscribe(subscriptionId) {
1740
+ this.ensureInitialized();
1741
+ if (!(this.transport instanceof WsTransport)) {
1742
+ throw new Error(
1743
+ "unsubscribe() requires transport: 'websocket'. Reinitialise the client with transport: 'websocket'."
1744
+ );
1745
+ }
1746
+ return this.transport.unsubscribe(subscriptionId);
1747
+ }
1353
1748
  // ===== DG-awareness helpers =====
1354
1749
  warnIfNotDg(method) {
1355
1750
  if (!this.isDg && !this.dgWarned) {
@@ -1640,7 +2035,7 @@ function buildToolMeta(raw) {
1640
2035
  }
1641
2036
 
1642
2037
  // src/index.ts
1643
- var version = "0.1.0";
2038
+ var version = "0.5.0";
1644
2039
  export {
1645
2040
  AuthError,
1646
2041
  Client2 as Client,
@@ -1656,6 +2051,8 @@ export {
1656
2051
  OAuthTokenProvider,
1657
2052
  RateLimitError,
1658
2053
  ServerError,
2054
+ SUBPROTOCOL as WS_SUBPROTOCOL,
2055
+ WsTransport,
1659
2056
  deriveTokenEndpoint,
1660
2057
  extractMeta,
1661
2058
  fetchDgCaCert,
@@ -1663,7 +2060,9 @@ export {
1663
2060
  generateKeypair,
1664
2061
  isDgUrl,
1665
2062
  refreshCaCert,
2063
+ registerAndExchange,
1666
2064
  registerIdentity,
2065
+ registerOnly,
1667
2066
  rotateIdentity,
1668
2067
  saveIdentity,
1669
2068
  version
@@ -2,7 +2,8 @@ import {
2
2
  OAuthTokenProvider,
3
3
  deriveTokenEndpoint,
4
4
  init_oauth
5
- } from "./chunk-RED5DKGI.mjs";
5
+ } from "./chunk-26DYCD4G.mjs";
6
+ import "./chunk-CIESM3BP.mjs";
6
7
  init_oauth();
7
8
  export {
8
9
  OAuthTokenProvider,
@@ -0,0 +1,13 @@
1
+ import {
2
+ _doRegister,
3
+ _exchangeToken,
4
+ registerAndExchange,
5
+ registerOnly
6
+ } from "./chunk-SWH5Y2U7.mjs";
7
+ import "./chunk-CIESM3BP.mjs";
8
+ export {
9
+ _doRegister,
10
+ _exchangeToken,
11
+ registerAndExchange,
12
+ registerOnly
13
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@datagrout/conduit",
3
- "version": "0.3.0",
3
+ "version": "0.5.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",