@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.js CHANGED
@@ -343,6 +343,81 @@ var init_identity = __esm({
343
343
  }
344
344
  });
345
345
 
346
+ // src/onramp.ts
347
+ var onramp_exports = {};
348
+ __export(onramp_exports, {
349
+ _doRegister: () => _doRegister,
350
+ _exchangeToken: () => _exchangeToken,
351
+ registerAndExchange: () => registerAndExchange,
352
+ registerOnly: () => registerOnly
353
+ });
354
+ async function _doRegister(opts) {
355
+ const base = opts.gateway.replace(/\/$/, "");
356
+ const body = { agent_name: opts.agentName };
357
+ if (opts.agentType) body["agent_type"] = opts.agentType;
358
+ if (opts.intendedUse) body["intended_use"] = opts.intendedUse;
359
+ if (opts.accessCode) body["access_code"] = opts.accessCode;
360
+ const initResp = await fetch(`${base}/onramp`, {
361
+ method: "POST",
362
+ headers: { "Content-Type": "application/json" },
363
+ body: JSON.stringify(body)
364
+ });
365
+ if (!initResp.ok) {
366
+ const text = await initResp.text();
367
+ throw new Error(`onramp init rejected (HTTP ${initResp.status}): ${text}`);
368
+ }
369
+ const initData = await initResp.json();
370
+ const sessionToken = initData.session_token;
371
+ const completeResp = await fetch(`${base}/onramp/complete`, {
372
+ method: "POST",
373
+ headers: { Authorization: `Bearer ${sessionToken}` }
374
+ });
375
+ if (!completeResp.ok) {
376
+ const text = await completeResp.text();
377
+ throw new Error(`onramp complete rejected (HTTP ${completeResp.status}): ${text}`);
378
+ }
379
+ const data = await completeResp.json();
380
+ return {
381
+ clientId: data["client_id"],
382
+ clientSecret: data["client_secret"],
383
+ tokenUrl: data["token_url"],
384
+ scopes: data["scopes"] ?? [],
385
+ expiresIn: data["expires_in"] ?? 0,
386
+ rpcUrl: data["rpc_url"],
387
+ mcpUrl: data["mcp_url"]
388
+ };
389
+ }
390
+ async function _exchangeToken(creds) {
391
+ const body = new URLSearchParams({
392
+ grant_type: "client_credentials",
393
+ client_id: creds.clientId,
394
+ client_secret: creds.clientSecret
395
+ });
396
+ const resp = await fetch(creds.tokenUrl, {
397
+ method: "POST",
398
+ body
399
+ });
400
+ if (!resp.ok) {
401
+ const text = await resp.text();
402
+ throw new Error(`token exchange failed (HTTP ${resp.status}): ${text}`);
403
+ }
404
+ const data = await resp.json();
405
+ return data.access_token;
406
+ }
407
+ async function registerOnly(opts) {
408
+ return _doRegister(opts);
409
+ }
410
+ async function registerAndExchange(opts) {
411
+ const creds = await _doRegister(opts);
412
+ const token = await _exchangeToken(creds);
413
+ return [creds, token];
414
+ }
415
+ var init_onramp = __esm({
416
+ "src/onramp.ts"() {
417
+ "use strict";
418
+ }
419
+ });
420
+
346
421
  // src/index.ts
347
422
  var index_exports = {};
348
423
  __export(index_exports, {
@@ -360,6 +435,8 @@ __export(index_exports, {
360
435
  OAuthTokenProvider: () => OAuthTokenProvider,
361
436
  RateLimitError: () => RateLimitError,
362
437
  ServerError: () => ServerError,
438
+ WS_SUBPROTOCOL: () => SUBPROTOCOL,
439
+ WsTransport: () => WsTransport,
363
440
  deriveTokenEndpoint: () => deriveTokenEndpoint,
364
441
  extractMeta: () => extractMeta,
365
442
  fetchDgCaCert: () => fetchDgCaCert,
@@ -367,7 +444,9 @@ __export(index_exports, {
367
444
  generateKeypair: () => generateKeypair,
368
445
  isDgUrl: () => isDgUrl,
369
446
  refreshCaCert: () => refreshCaCert,
447
+ registerAndExchange: () => registerAndExchange,
370
448
  registerIdentity: () => registerIdentity,
449
+ registerOnly: () => registerOnly,
371
450
  rotateIdentity: () => rotateIdentity,
372
451
  saveIdentity: () => saveIdentity,
373
452
  version: () => version
@@ -727,6 +806,295 @@ var JSONRPCTransport = class extends Transport {
727
806
  }
728
807
  };
729
808
 
809
+ // src/transports/ws.ts
810
+ var SUBPROTOCOL = "datagrout-jsonrpc.v1";
811
+ var SUBSCRIPTION_BUFFER = 256;
812
+ var Subscription = class {
813
+ id;
814
+ topic;
815
+ _queue = [];
816
+ _waiters = [];
817
+ _rejecters = [];
818
+ _closed = false;
819
+ constructor(id, topic) {
820
+ this.id = id;
821
+ this.topic = topic;
822
+ }
823
+ /**
824
+ * Wait for the next event from this subscription.
825
+ *
826
+ * @throws When the subscription has been closed.
827
+ */
828
+ recv() {
829
+ if (this._queue.length > 0) {
830
+ return Promise.resolve(this._queue.shift());
831
+ }
832
+ if (this._closed) {
833
+ return Promise.reject(new Error("Subscription closed"));
834
+ }
835
+ return new Promise((resolve, reject) => {
836
+ this._waiters.push(resolve);
837
+ this._rejecters.push(reject);
838
+ });
839
+ }
840
+ async *[Symbol.asyncIterator]() {
841
+ while (this._queue.length > 0 || !this._closed) {
842
+ try {
843
+ yield await this.recv();
844
+ } catch {
845
+ return;
846
+ }
847
+ }
848
+ }
849
+ // ── Internal ───────────────────────────────────────────────────────────────
850
+ _enqueue(event) {
851
+ if (this._waiters.length > 0) {
852
+ const resolve = this._waiters.shift();
853
+ this._rejecters.shift();
854
+ resolve(event);
855
+ } else if (this._queue.length < SUBSCRIPTION_BUFFER) {
856
+ this._queue.push(event);
857
+ }
858
+ }
859
+ _close() {
860
+ this._closed = true;
861
+ const err = new Error("Subscription closed");
862
+ for (const reject of this._rejecters) {
863
+ reject(err);
864
+ }
865
+ this._waiters.length = 0;
866
+ this._rejecters.length = 0;
867
+ }
868
+ };
869
+ var WsTransport = class extends Transport {
870
+ _url;
871
+ _auth;
872
+ _ws = null;
873
+ _nextId = 0;
874
+ _pending = /* @__PURE__ */ new Map();
875
+ _pendingSubscribe = /* @__PURE__ */ new Map();
876
+ _subscriptions = /* @__PURE__ */ new Map();
877
+ constructor(url, auth, _timeout, _identity) {
878
+ super();
879
+ const scheme = new URL(url).protocol.replace(":", "");
880
+ if (scheme !== "ws" && scheme !== "wss") {
881
+ throw new Error(`WS transport requires a ws:// or wss:// URL, got ${scheme}://`);
882
+ }
883
+ this._url = url;
884
+ this._auth = auth;
885
+ }
886
+ // ── Lifecycle ─────────────────────────────────────────────────────────────
887
+ async connect() {
888
+ if (this._ws !== null) return;
889
+ const WsImpl = await resolveWebSocketImpl();
890
+ const headers = buildUpgradeHeaders(this._auth);
891
+ const ws = new WsImpl(this._url, [SUBPROTOCOL], {
892
+ headers
893
+ });
894
+ await new Promise((resolve, reject) => {
895
+ ws.onopen = () => resolve();
896
+ ws.onerror = (ev) => reject(new Error(`WS connect failed: ${ev.message ?? "unknown"}`));
897
+ });
898
+ ws.onmessage = (ev) => this._handleMessage(ev.data);
899
+ ws.onerror = (_ev) => this._failAll("WS connection error");
900
+ ws.onclose = () => {
901
+ this._failAll("WS connection closed");
902
+ this._ws = null;
903
+ };
904
+ this._ws = ws;
905
+ }
906
+ async disconnect() {
907
+ const ws = this._ws;
908
+ this._ws = null;
909
+ this._failAll("WS connection closed");
910
+ if (ws !== null) {
911
+ try {
912
+ ws.close();
913
+ } catch {
914
+ }
915
+ }
916
+ }
917
+ // ── Subscriptions ─────────────────────────────────────────────────────────
918
+ /**
919
+ * Open a server-side push subscription for `topic`.
920
+ *
921
+ * @param topic - Dotted namespace topic, e.g.
922
+ * `"agents.my-agent-id.events"` or `"tasks.task-123.*"`.
923
+ * @returns A {@link Subscription} handle whose async-for loop delivers events.
924
+ */
925
+ async subscribe(topic) {
926
+ this._requireConnected();
927
+ const id = this._mintId();
928
+ return new Promise((resolve, reject) => {
929
+ this._pendingSubscribe.set(id, { topic, resolve, reject });
930
+ this._send({ jsonrpc: "2.0", id, method: "subscribe", params: { topic } });
931
+ });
932
+ }
933
+ /**
934
+ * Cancel a server-side subscription.
935
+ *
936
+ * The local {@link Subscription} queue is closed immediately.
937
+ *
938
+ * @param subscriptionId - The `id` field from the {@link Subscription}
939
+ * returned by {@link subscribe}.
940
+ */
941
+ async unsubscribe(subscriptionId) {
942
+ this._requireConnected();
943
+ const sub = this._subscriptions.get(subscriptionId);
944
+ if (sub !== void 0) {
945
+ this._subscriptions.delete(subscriptionId);
946
+ sub._close();
947
+ }
948
+ const id = this._mintId();
949
+ const ackPromise = new Promise((resolve, reject) => {
950
+ this._pending.set(id, { resolve, reject });
951
+ });
952
+ this._send({
953
+ jsonrpc: "2.0",
954
+ id,
955
+ method: "unsubscribe",
956
+ params: { subscription: subscriptionId }
957
+ });
958
+ await Promise.race([
959
+ ackPromise,
960
+ new Promise((resolve) => setTimeout(resolve, 5e3))
961
+ ]);
962
+ this._pending.delete(id);
963
+ }
964
+ // ── Transport base implementation ─────────────────────────────────────────
965
+ async listTools(options) {
966
+ return await this._request("tools/list", options);
967
+ }
968
+ async callTool(name, args, _options) {
969
+ return this._request("tools/call", { name, arguments: args });
970
+ }
971
+ async listResources(_options) {
972
+ return await this._request("resources/list");
973
+ }
974
+ async readResource(uri, _options) {
975
+ return this._request("resources/read", { uri });
976
+ }
977
+ async listPrompts(_options) {
978
+ return await this._request("prompts/list");
979
+ }
980
+ async getPrompt(name, args, _options) {
981
+ return this._request("prompts/get", { name, arguments: args });
982
+ }
983
+ // ── Internal ──────────────────────────────────────────────────────────────
984
+ _mintId() {
985
+ return `ws-${++this._nextId}`;
986
+ }
987
+ _requireConnected() {
988
+ if (this._ws === null) {
989
+ throw new Error("WS transport not connected. Call connect() first.");
990
+ }
991
+ }
992
+ _send(payload) {
993
+ this._ws.send(JSON.stringify(payload));
994
+ }
995
+ async _request(method, params) {
996
+ this._requireConnected();
997
+ const id = this._mintId();
998
+ return new Promise((resolve, reject) => {
999
+ this._pending.set(id, { resolve, reject });
1000
+ this._send({ jsonrpc: "2.0", id, method, ...params !== void 0 ? { params } : {} });
1001
+ });
1002
+ }
1003
+ _handleMessage(data) {
1004
+ let msg;
1005
+ try {
1006
+ msg = JSON.parse(data);
1007
+ } catch {
1008
+ return;
1009
+ }
1010
+ if (!("id" in msg)) {
1011
+ if (msg["method"] === "notification") {
1012
+ this._routeNotification(msg["params"]);
1013
+ }
1014
+ return;
1015
+ }
1016
+ const msgId = String(msg["id"]);
1017
+ const pendingSub = this._pendingSubscribe.get(msgId);
1018
+ if (pendingSub !== void 0) {
1019
+ this._pendingSubscribe.delete(msgId);
1020
+ const err = msg["error"];
1021
+ if (err !== void 0) {
1022
+ pendingSub.reject(new Error(String(err["message"] ?? "Subscribe failed")));
1023
+ return;
1024
+ }
1025
+ const result = msg["result"] ?? {};
1026
+ const subId = String(result["subscription"] ?? msgId);
1027
+ const sub = new Subscription(subId, pendingSub.topic);
1028
+ this._subscriptions.set(subId, sub);
1029
+ pendingSub.resolve(sub);
1030
+ return;
1031
+ }
1032
+ const pending = this._pending.get(msgId);
1033
+ if (pending !== void 0) {
1034
+ this._pending.delete(msgId);
1035
+ const err = msg["error"];
1036
+ if (err !== void 0) {
1037
+ pending.reject(new Error(String(err["message"] ?? "RPC error")));
1038
+ } else {
1039
+ pending.resolve(msg["result"]);
1040
+ }
1041
+ }
1042
+ }
1043
+ _routeNotification(params) {
1044
+ if (params === void 0) return;
1045
+ const subId = params["subscription"];
1046
+ if (typeof subId !== "string") return;
1047
+ const sub = this._subscriptions.get(subId);
1048
+ if (sub === void 0) return;
1049
+ sub._enqueue({
1050
+ subscription: subId,
1051
+ event: String(params["event"] ?? ""),
1052
+ data: params["data"]
1053
+ });
1054
+ }
1055
+ _failAll(reason) {
1056
+ const err = new Error(reason);
1057
+ for (const { reject } of this._pending.values()) {
1058
+ reject(err);
1059
+ }
1060
+ this._pending.clear();
1061
+ for (const { reject } of this._pendingSubscribe.values()) {
1062
+ reject(err);
1063
+ }
1064
+ this._pendingSubscribe.clear();
1065
+ for (const sub of this._subscriptions.values()) {
1066
+ sub._close();
1067
+ }
1068
+ this._subscriptions.clear();
1069
+ }
1070
+ };
1071
+ function buildUpgradeHeaders(auth) {
1072
+ const headers = {};
1073
+ if (auth === void 0) return headers;
1074
+ if ("bearer" in auth && auth.bearer !== void 0) {
1075
+ headers["Authorization"] = `Bearer ${auth.bearer}`;
1076
+ } else if ("apiKey" in auth && auth.apiKey !== void 0) {
1077
+ headers["X-API-Key"] = auth.apiKey;
1078
+ } else if ("basic" in auth && auth.basic !== void 0) {
1079
+ const encoded = Buffer.from(`${auth.basic.username}:${auth.basic.password}`).toString("base64");
1080
+ headers["Authorization"] = `Basic ${encoded}`;
1081
+ }
1082
+ return headers;
1083
+ }
1084
+ async function resolveWebSocketImpl() {
1085
+ if (typeof globalThis.WebSocket !== "undefined") {
1086
+ return globalThis.WebSocket;
1087
+ }
1088
+ try {
1089
+ const { default: WS } = await import("ws");
1090
+ return WS;
1091
+ } catch {
1092
+ throw new Error(
1093
+ "No WebSocket implementation found. Install the 'ws' package: npm install ws"
1094
+ );
1095
+ }
1096
+ }
1097
+
730
1098
  // src/client.ts
731
1099
  init_identity();
732
1100
 
@@ -1247,13 +1615,15 @@ var Client2 = class _Client {
1247
1615
  this.isDg = isDgUrl(this.url);
1248
1616
  this.useIntelligentInterface = options.useIntelligentInterface ?? this.isDg;
1249
1617
  this.maxRetries = options.maxRetries ?? 3;
1250
- let identity = options.identity ?? (options.identityAuto ? ConduitIdentity.tryDiscover(options.identityDir) ?? void 0 : void 0);
1251
- if (identity === void 0 && this.isDg && !options.disableMtls) {
1252
- identity = ConduitIdentity.tryDiscover(options.identityDir) ?? void 0;
1253
- }
1618
+ const identity = options.identity ?? (options.identityAuto ? ConduitIdentity.tryDiscover(options.identityDir) ?? void 0 : void 0);
1254
1619
  const transportType = options.transport || "mcp";
1255
1620
  if (transportType === "mcp") {
1256
1621
  this.transport = new MCPTransport(this.url, this.auth, identity);
1622
+ } else if (transportType === "websocket") {
1623
+ let wsUrl = this.url;
1624
+ if (wsUrl.startsWith("https://")) wsUrl = "wss://" + wsUrl.slice(8);
1625
+ else if (wsUrl.startsWith("http://")) wsUrl = "ws://" + wsUrl.slice(7);
1626
+ this.transport = new WsTransport(wsUrl, this.auth, options.timeout, identity);
1257
1627
  } else {
1258
1628
  const rpcUrl = this.url.endsWith("/mcp") ? this.url.slice(0, -4) + "/rpc" : this.url;
1259
1629
  this.transport = new JSONRPCTransport(rpcUrl, this.auth, options.timeout, identity);
@@ -1330,6 +1700,60 @@ var Client2 = class _Client {
1330
1700
  substrateEndpoint: options.substrateEndpoint
1331
1701
  });
1332
1702
  }
1703
+ /**
1704
+ * Register autonomously with DG and bootstrap an mTLS identity.
1705
+ *
1706
+ * The all-in-one flow: onramp (no prior credentials required) →
1707
+ * OAuth token exchange → mTLS identity registration and persistence.
1708
+ *
1709
+ * On subsequent runs the saved mTLS identity is auto-discovered and
1710
+ * no credentials are needed.
1711
+ *
1712
+ * @param options.opts - Onramp registration options.
1713
+ * @param options.url - MCP server URL. Required if the onramp
1714
+ * response does not include `mcpUrl`.
1715
+ * @param options.identityDir - Custom identity storage directory.
1716
+ *
1717
+ * @example
1718
+ * ```ts
1719
+ * import { Client } from './client';
1720
+ * import type { OnrampOptions } from './onramp';
1721
+ *
1722
+ * const client = await Client.bootstrapOnramp({
1723
+ * opts: {
1724
+ * gateway: 'https://app.datagrout.ai',
1725
+ * agentName: 'my-research-agent',
1726
+ * agentType: 'claude-sonnet-4-6',
1727
+ * },
1728
+ * });
1729
+ * await client.connect();
1730
+ * ```
1731
+ */
1732
+ static async bootstrapOnramp(options) {
1733
+ const { _doRegister: _doRegister2, _exchangeToken: _exchangeToken2 } = await Promise.resolve().then(() => (init_onramp(), onramp_exports));
1734
+ const dir = options.identityDir || DEFAULT_IDENTITY_DIR;
1735
+ const existing = ConduitIdentity.tryDiscover(dir);
1736
+ if (existing && !existing.needsRotation(7)) {
1737
+ if (!options.url) {
1738
+ throw new Error("'url' must be provided when an existing identity is reused");
1739
+ }
1740
+ return new _Client({ url: options.url, identity: existing, identityDir: dir });
1741
+ }
1742
+ const creds = await _doRegister2(options.opts);
1743
+ const token = await _exchangeToken2(creds);
1744
+ const url = creds.mcpUrl ?? options.url;
1745
+ if (!url) {
1746
+ throw new Error(
1747
+ "'url' must be provided when mcpUrl is absent from the onramp response"
1748
+ );
1749
+ }
1750
+ return _Client.bootstrapIdentity({
1751
+ url,
1752
+ authToken: token,
1753
+ name: options.opts.agentName,
1754
+ identityDir: options.identityDir
1755
+ });
1756
+ }
1333
1757
  // ===== Lifecycle =====
1334
1758
  /**
1335
1759
  * Establish the underlying transport connection.
@@ -1488,6 +1912,50 @@ var Client2 = class _Client {
1488
1912
  this.ensureInitialized();
1489
1913
  return this.sendWithRetry(() => this.transport.getPrompt(name, args, options));
1490
1914
  }
1915
+ // ===== WebSocket push subscriptions =====
1916
+ /**
1917
+ * Subscribe to a server-push topic (WebSocket transport only).
1918
+ *
1919
+ * Requires `transport: 'websocket'` when constructing the client.
1920
+ *
1921
+ * @param topic - Dotted namespace topic, e.g.
1922
+ * `"agents.my-agent-id.events"` or `"tasks.task-123.*"`.
1923
+ * @returns A {@link Subscription} handle. Consume events with
1924
+ * {@link Subscription.recv} or an `for await` loop.
1925
+ *
1926
+ * @example
1927
+ * ```ts
1928
+ * const sub = await client.subscribe('agents.my-agent-id.events');
1929
+ * for await (const event of sub) {
1930
+ * console.log(event.event, event.data);
1931
+ * }
1932
+ * await client.unsubscribe(sub.id);
1933
+ * ```
1934
+ */
1935
+ async subscribe(topic) {
1936
+ this.ensureInitialized();
1937
+ if (!(this.transport instanceof WsTransport)) {
1938
+ throw new Error(
1939
+ "subscribe() requires transport: 'websocket'. Reinitialise the client with transport: 'websocket'."
1940
+ );
1941
+ }
1942
+ return this.transport.subscribe(topic);
1943
+ }
1944
+ /**
1945
+ * Cancel a server-side push subscription.
1946
+ *
1947
+ * @param subscriptionId - The `id` from the {@link Subscription} returned
1948
+ * by {@link subscribe}.
1949
+ */
1950
+ async unsubscribe(subscriptionId) {
1951
+ this.ensureInitialized();
1952
+ if (!(this.transport instanceof WsTransport)) {
1953
+ throw new Error(
1954
+ "unsubscribe() requires transport: 'websocket'. Reinitialise the client with transport: 'websocket'."
1955
+ );
1956
+ }
1957
+ return this.transport.unsubscribe(subscriptionId);
1958
+ }
1491
1959
  // ===== DG-awareness helpers =====
1492
1960
  warnIfNotDg(method) {
1493
1961
  if (!this.isDg && !this.dgWarned) {
@@ -1778,7 +2246,8 @@ function buildToolMeta(raw) {
1778
2246
  }
1779
2247
 
1780
2248
  // src/index.ts
1781
- var version = "0.1.0";
2249
+ init_onramp();
2250
+ var version = "0.5.0";
1782
2251
  // Annotate the CommonJS export names for ESM import in node:
1783
2252
  0 && (module.exports = {
1784
2253
  AuthError,
@@ -1795,6 +2264,8 @@ var version = "0.1.0";
1795
2264
  OAuthTokenProvider,
1796
2265
  RateLimitError,
1797
2266
  ServerError,
2267
+ WS_SUBPROTOCOL,
2268
+ WsTransport,
1798
2269
  deriveTokenEndpoint,
1799
2270
  extractMeta,
1800
2271
  fetchDgCaCert,
@@ -1802,7 +2273,9 @@ var version = "0.1.0";
1802
2273
  generateKeypair,
1803
2274
  isDgUrl,
1804
2275
  refreshCaCert,
2276
+ registerAndExchange,
1805
2277
  registerIdentity,
2278
+ registerOnly,
1806
2279
  rotateIdentity,
1807
2280
  saveIdentity,
1808
2281
  version