@datagrout/conduit 0.7.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -435,34 +435,56 @@ var init_onramp = __esm({
435
435
  // src/index.ts
436
436
  var index_exports = {};
437
437
  __export(index_exports, {
438
+ AuthCodeError: () => AuthCodeError,
439
+ AuthCodeFlow: () => AuthCodeFlow,
440
+ AuthCodeProvider: () => AuthCodeProvider,
438
441
  AuthError: () => AuthError,
439
442
  Client: () => Client2,
440
443
  ConduitError: () => ConduitError,
441
444
  ConduitIdentity: () => ConduitIdentity,
442
445
  DEFAULT_IDENTITY_DIR: () => DEFAULT_IDENTITY_DIR,
446
+ DEFAULT_SCOPE: () => DEFAULT_SCOPE,
447
+ DELEGATION_GRANT_TYPE: () => GRANT_TYPE,
448
+ DELEGATION_SERVER_ERROR_CODES: () => SERVER_ERROR_CODES,
443
449
  DG_CA_URL: () => DG_CA_URL,
444
450
  DG_SUBSTRATE_ENDPOINT: () => DG_SUBSTRATE_ENDPOINT,
451
+ DelegatedProvider: () => DelegatedProvider,
452
+ DelegationError: () => DelegationError,
453
+ DelegationRequest: () => DelegationRequest,
445
454
  GuidedSession: () => GuidedSession,
446
455
  InvalidConfigError: () => InvalidConfigError,
456
+ LoopbackListener: () => LoopbackListener,
447
457
  NetworkError: () => NetworkError,
448
458
  NotInitializedError: () => NotInitializedError,
449
459
  OAuthTokenProvider: () => OAuthTokenProvider,
450
460
  RateLimitError: () => RateLimitError,
451
461
  ServerError: () => ServerError,
462
+ TOKEN_TYPES: () => TOKEN_TYPES,
463
+ TokenSource: () => TokenSource,
452
464
  WS_SUBPROTOCOL: () => SUBPROTOCOL,
453
465
  WsTransport: () => WsTransport,
466
+ authCodeProviderFrom: () => authCodeProviderFrom,
467
+ challengeS256: () => challengeS256,
454
468
  deriveTokenEndpoint: () => deriveTokenEndpoint,
455
469
  extractMeta: () => extractMeta,
456
470
  fetchDgCaCert: () => fetchDgCaCert,
457
471
  fetchWithIdentity: () => fetchWithIdentity,
458
472
  generateKeypair: () => generateKeypair,
473
+ generateVerifier: () => generateVerifier,
474
+ isDelegatedTokenExpired: () => isDelegatedTokenExpired,
475
+ isDgNativeTool: () => isDgNativeTool,
459
476
  isDgUrl: () => isDgUrl,
477
+ isGrantExpired: () => isGrantExpired,
478
+ isGrantRefreshable: () => isGrantRefreshable,
460
479
  refreshCaCert: () => refreshCaCert,
480
+ refreshGrant: () => refreshGrant,
461
481
  registerAndExchange: () => registerAndExchange,
462
482
  registerIdentity: () => registerIdentity,
463
483
  registerOnly: () => registerOnly,
464
484
  rotateIdentity: () => rotateIdentity,
465
485
  saveIdentity: () => saveIdentity,
486
+ supportsS256: () => supportsS256,
487
+ tokenTypeName: () => tokenTypeName,
466
488
  version: () => version
467
489
  });
468
490
  module.exports = __toCommonJS(index_exports);
@@ -470,12 +492,504 @@ module.exports = __toCommonJS(index_exports);
470
492
  // src/client.ts
471
493
  var path2 = __toESM(require("path"));
472
494
 
495
+ // src/version.ts
496
+ var version = "0.8.1";
497
+
473
498
  // src/transports/base.ts
474
499
  var Transport = class {
475
500
  };
476
501
 
477
502
  // src/transports/mcp.ts
478
503
  init_oauth();
504
+
505
+ // src/authcode.ts
506
+ var import_node_crypto = require("crypto");
507
+
508
+ // src/errors.ts
509
+ var ConduitError = class extends Error {
510
+ constructor(message) {
511
+ super(message);
512
+ this.name = this.constructor.name;
513
+ Object.setPrototypeOf(this, new.target.prototype);
514
+ }
515
+ };
516
+ var NotInitializedError = class extends ConduitError {
517
+ constructor() {
518
+ super("Client not initialized. Call connect() first.");
519
+ }
520
+ };
521
+ var RateLimitError = class extends ConduitError {
522
+ status;
523
+ retryAfter;
524
+ constructor(status, retryAfter) {
525
+ const limitStr = status.limit === "unlimited" ? "unlimited" : `${status.limit.perHour}/hour`;
526
+ super(`Rate limit exceeded (${status.used} / ${limitStr} calls this hour)`);
527
+ this.status = status;
528
+ this.retryAfter = retryAfter;
529
+ }
530
+ };
531
+ var AuthError = class extends ConduitError {
532
+ constructor(message = "Authentication failed") {
533
+ super(message);
534
+ }
535
+ };
536
+ var NetworkError = class extends ConduitError {
537
+ constructor(message) {
538
+ super(message);
539
+ }
540
+ };
541
+ var ServerError = class extends ConduitError {
542
+ code;
543
+ serverMessage;
544
+ constructor(code, serverMessage) {
545
+ super(`Server error ${code}: ${serverMessage}`);
546
+ this.code = code;
547
+ this.serverMessage = serverMessage;
548
+ }
549
+ };
550
+ var InvalidConfigError = class extends ConduitError {
551
+ constructor(message) {
552
+ super(message);
553
+ }
554
+ };
555
+
556
+ // src/authcode.ts
557
+ var DEFAULT_SCOPE = "mcp tools";
558
+ var REFRESH_SKEW_SECS = 60;
559
+ var AuthCodeError = class extends ConduitError {
560
+ kind;
561
+ /** HTTP status, for `registration_rejected` and `token_exchange`. */
562
+ status;
563
+ /** Response body, for `registration_rejected` and `token_exchange`. */
564
+ body;
565
+ constructor(kind, message, extra) {
566
+ super(message);
567
+ this.kind = kind;
568
+ this.status = extra?.status;
569
+ this.body = extra?.body;
570
+ }
571
+ };
572
+ function supportsS256(metadata) {
573
+ const methods = metadata.code_challenge_methods_supported;
574
+ if (!methods || methods.length === 0) return true;
575
+ return methods.some((m) => m.toUpperCase() === "S256");
576
+ }
577
+ function isGrantExpired(grant) {
578
+ if (grant.expires_at === void 0) return false;
579
+ return nowSecs() + REFRESH_SKEW_SECS >= grant.expires_at;
580
+ }
581
+ function isGrantRefreshable(grant) {
582
+ return grant.refresh_token !== void 0 && grant.refresh_token !== "";
583
+ }
584
+ async function refreshGrant(grant, fetchImpl = globalThis.fetch) {
585
+ if (!isGrantRefreshable(grant)) {
586
+ throw new AuthCodeError(
587
+ "not_refreshable",
588
+ "grant has expired and carries no refresh_token \u2014 re-authorize"
589
+ );
590
+ }
591
+ const form = {
592
+ grant_type: "refresh_token",
593
+ refresh_token: grant.refresh_token,
594
+ client_id: grant.client_id
595
+ };
596
+ if (grant.resource) form.resource = grant.resource;
597
+ const token = await postForm(fetchImpl, grant.token_endpoint, form);
598
+ return {
599
+ access_token: token.access_token,
600
+ // A server that does not rotate returns no new refresh token; keep the
601
+ // existing one rather than silently making the grant unrefreshable.
602
+ refresh_token: token.refresh_token ?? grant.refresh_token,
603
+ expires_at: token.expires_in === void 0 ? void 0 : nowSecs() + token.expires_in,
604
+ client_id: grant.client_id,
605
+ token_endpoint: grant.token_endpoint,
606
+ scope: token.scope ?? grant.scope,
607
+ resource: grant.resource
608
+ };
609
+ }
610
+ var AuthCodeFlow = class _AuthCodeFlow {
611
+ fetchImpl;
612
+ metadataDoc;
613
+ /** The protected resource this grant will be bound to (RFC 8707). */
614
+ resource;
615
+ clientIdValue;
616
+ redirectUriValue;
617
+ scope = DEFAULT_SCOPE;
618
+ constructor(fetchImpl, metadata, resource) {
619
+ this.fetchImpl = fetchImpl;
620
+ this.metadataDoc = metadata;
621
+ this.resource = resource;
622
+ }
623
+ /**
624
+ * Discover the authorization server protecting `resourceUrl`.
625
+ *
626
+ * `resourceUrl` is the MCP endpoint being connected to — for DataGrout,
627
+ * `https://gateway.datagrout.ai/connect` or a `.../servers/{uuid}/mcp` URL.
628
+ *
629
+ * Tries RFC 9728 protected-resource metadata first, then RFC 8414
630
+ * authorization-server metadata on whatever that names. Falls back to the
631
+ * resource's own origin, which is where DataGrout serves it.
632
+ */
633
+ static async discover(resourceUrl, fetchImpl = globalThis.fetch) {
634
+ const resource = resourceUrl.replace(/\/+$/, "");
635
+ const prm = await fetchResourceMetadata(fetchImpl, resource);
636
+ let issuer;
637
+ if (prm?.authorization_servers && prm.authorization_servers.length > 0) {
638
+ issuer = prm.authorization_servers[0];
639
+ } else {
640
+ issuer = originOf(resource);
641
+ }
642
+ if (!issuer) {
643
+ throw new AuthCodeError("discovery", `not a URL: ${resource}`);
644
+ }
645
+ const metadata = await fetchAsMetadata(fetchImpl, issuer);
646
+ if (!supportsS256(metadata)) {
647
+ throw new AuthCodeError(
648
+ "pkce_unsupported",
649
+ "authorization server does not support PKCE S256; refusing to downgrade"
650
+ );
651
+ }
652
+ return new _AuthCodeFlow(fetchImpl, metadata, resource);
653
+ }
654
+ /** Use a client id registered out of band, skipping dynamic registration. */
655
+ withClientId(clientId, redirectUri) {
656
+ this.clientIdValue = clientId;
657
+ this.redirectUriValue = redirectUri;
658
+ return this;
659
+ }
660
+ /**
661
+ * Reuse a client registered on a previous run.
662
+ *
663
+ * Prefer this over {@link withClientId}: it carries the redirect URI with the
664
+ * id, which is not optional bookkeeping — an authorization server matches the
665
+ * redirect URI **exactly** against what was registered, so a client id reused
666
+ * with a different URI is rejected.
667
+ */
668
+ withRegisteredClient(client) {
669
+ return this.withClientId(client.client_id, client.redirect_uri);
670
+ }
671
+ /** Request scopes other than {@link DEFAULT_SCOPE}. */
672
+ withScope(scope) {
673
+ this.scope = scope;
674
+ return this;
675
+ }
676
+ /** The discovered metadata. */
677
+ get metadata() {
678
+ return this.metadataDoc;
679
+ }
680
+ /** The client id, once registered or supplied. */
681
+ get clientId() {
682
+ return this.clientIdValue;
683
+ }
684
+ /** The redirect URI this flow is bound to. */
685
+ get redirectUri() {
686
+ return this.redirectUriValue;
687
+ }
688
+ /**
689
+ * Register this application via RFC 7591 dynamic client registration.
690
+ *
691
+ * Registers a **public client** — `token_endpoint_auth_method: "none"`, no
692
+ * secret issued. A desktop or CLI application cannot keep a secret, and PKCE
693
+ * is what stands in for one.
694
+ *
695
+ * Returns the id **and** the redirect URI it is bound to. Persist the pair
696
+ * and restore it with {@link withRegisteredClient} — re-registering on every
697
+ * launch creates a new client record each time, and reusing an id against a
698
+ * different redirect URI is rejected.
699
+ */
700
+ async register(clientName, redirectUri) {
701
+ const endpoint = this.metadataDoc.registration_endpoint;
702
+ if (!endpoint) {
703
+ throw new AuthCodeError(
704
+ "no_registration_endpoint",
705
+ "authorization server has no registration endpoint \u2014 register a client manually and use withClientId()"
706
+ );
707
+ }
708
+ let response;
709
+ try {
710
+ response = await this.fetchImpl(endpoint, {
711
+ method: "POST",
712
+ headers: { "Content-Type": "application/json" },
713
+ body: JSON.stringify({
714
+ client_name: clientName,
715
+ redirect_uris: [redirectUri],
716
+ grant_types: ["authorization_code", "refresh_token"],
717
+ response_types: ["code"],
718
+ token_endpoint_auth_method: "none",
719
+ application_type: "native"
720
+ })
721
+ });
722
+ } catch (err) {
723
+ throw new AuthCodeError("http", `HTTP error: ${err}`);
724
+ }
725
+ if (!response.ok) {
726
+ const body = await response.text().catch(() => "");
727
+ throw new AuthCodeError(
728
+ "registration_rejected",
729
+ `client registration rejected (HTTP ${response.status}): ${body}`,
730
+ { status: response.status, body }
731
+ );
732
+ }
733
+ let clientId;
734
+ try {
735
+ const parsed = await response.json();
736
+ clientId = parsed.client_id;
737
+ } catch (err) {
738
+ throw new AuthCodeError("http", `bad registration response: ${err}`);
739
+ }
740
+ this.clientIdValue = clientId;
741
+ this.redirectUriValue = redirectUri;
742
+ return { client_id: clientId, redirect_uri: redirectUri };
743
+ }
744
+ /**
745
+ * Build the consent URL, plus the {@link PendingAuthorization} needed to
746
+ * redeem the resulting code.
747
+ *
748
+ * The caller opens the URL however suits it — a browser, a printed
749
+ * instruction, a QR code. This SDK does not launch browsers.
750
+ */
751
+ authorizeUrl() {
752
+ const clientId = this.clientIdValue;
753
+ const redirectUri = this.redirectUriValue;
754
+ if (!clientId || !redirectUri) {
755
+ throw new AuthCodeError(
756
+ "no_client_id",
757
+ "no client_id \u2014 call register() or withClientId() first"
758
+ );
759
+ }
760
+ const codeVerifier = generateVerifier();
761
+ const state = generateState();
762
+ const query = [
763
+ ["response_type", "code"],
764
+ ["client_id", clientId],
765
+ ["redirect_uri", redirectUri],
766
+ ["scope", this.scope],
767
+ ["state", state],
768
+ ["code_challenge", challengeS256(codeVerifier)],
769
+ ["code_challenge_method", "S256"],
770
+ // RFC 8707: bind the token to this resource so it cannot be replayed
771
+ // against a different one.
772
+ ["resource", this.resource]
773
+ ].map(([k, v]) => `${k}=${urlencode(v)}`).join("&");
774
+ const separator = this.metadataDoc.authorization_endpoint.includes("?") ? "&" : "?";
775
+ const url = `${this.metadataDoc.authorization_endpoint}${separator}${query}`;
776
+ return { url, pending: { codeVerifier, state, redirectUri } };
777
+ }
778
+ /**
779
+ * Redeem an authorization code for a {@link Grant}.
780
+ *
781
+ * `returnedState` is the `state` parameter from the redirect. It is checked
782
+ * against the pending request before anything is sent: a mismatch means the
783
+ * response belongs to a different authorization request, and the exchange is
784
+ * refused rather than attempted.
785
+ */
786
+ async exchange(pending, code, returnedState) {
787
+ if (!constantTimeEqual(pending.state, returnedState)) {
788
+ throw new AuthCodeError(
789
+ "state_mismatch",
790
+ "state mismatch \u2014 the authorization response does not match this request"
791
+ );
792
+ }
793
+ const clientId = this.clientIdValue;
794
+ if (!clientId) {
795
+ throw new AuthCodeError(
796
+ "no_client_id",
797
+ "no client_id \u2014 call register() or withClientId() first"
798
+ );
799
+ }
800
+ const token = await postForm(
801
+ this.fetchImpl,
802
+ this.metadataDoc.token_endpoint,
803
+ {
804
+ grant_type: "authorization_code",
805
+ code,
806
+ redirect_uri: pending.redirectUri,
807
+ client_id: clientId,
808
+ code_verifier: pending.codeVerifier,
809
+ resource: this.resource
810
+ }
811
+ );
812
+ return {
813
+ access_token: token.access_token,
814
+ refresh_token: token.refresh_token,
815
+ expires_at: token.expires_in === void 0 ? void 0 : nowSecs() + token.expires_in,
816
+ client_id: clientId,
817
+ token_endpoint: this.metadataDoc.token_endpoint,
818
+ scope: token.scope,
819
+ resource: this.resource
820
+ };
821
+ }
822
+ };
823
+ var AuthCodeProvider = class {
824
+ grantValue;
825
+ dirty = false;
826
+ refreshPromise = null;
827
+ fetchImpl;
828
+ constructor(grant, fetchImpl = globalThis.fetch) {
829
+ this.grantValue = grant;
830
+ this.fetchImpl = fetchImpl;
831
+ }
832
+ /** The current access token, refreshing first if it is at or near expiry. */
833
+ async getToken() {
834
+ if (!isGrantExpired(this.grantValue)) {
835
+ return this.grantValue.access_token;
836
+ }
837
+ if (!this.refreshPromise) {
838
+ this.refreshPromise = refreshGrant(this.grantValue, this.fetchImpl).then((refreshed2) => {
839
+ this.grantValue = refreshed2;
840
+ this.dirty = true;
841
+ return refreshed2;
842
+ }).finally(() => {
843
+ this.refreshPromise = null;
844
+ });
845
+ }
846
+ const refreshed = await this.refreshPromise;
847
+ return refreshed.access_token;
848
+ }
849
+ /** A snapshot of the current grant, for persisting. */
850
+ grant() {
851
+ return { ...this.grantValue };
852
+ }
853
+ /** Whether the grant changed since the last {@link takeIfDirty}. */
854
+ isDirty() {
855
+ return this.dirty;
856
+ }
857
+ /**
858
+ * Return the grant if it has changed since the last call, clearing the flag.
859
+ *
860
+ * The intended use is a persistence loop: call periodically and write
861
+ * whatever comes back, so a rotated refresh token is never lost.
862
+ */
863
+ takeIfDirty() {
864
+ if (!this.dirty) return null;
865
+ this.dirty = false;
866
+ return this.grant();
867
+ }
868
+ /** Force the next {@link getToken} to refresh. Call on a 401. */
869
+ invalidate() {
870
+ this.grantValue = { ...this.grantValue, expires_at: 0 };
871
+ }
872
+ };
873
+ function authCodeProviderFrom(value, fetchImpl = globalThis.fetch) {
874
+ if (value === void 0) return void 0;
875
+ if (value instanceof AuthCodeProvider) return value;
876
+ return new AuthCodeProvider(value, fetchImpl);
877
+ }
878
+ function generateVerifier() {
879
+ return (0, import_node_crypto.randomBytes)(32).toString("base64url");
880
+ }
881
+ function challengeS256(verifier) {
882
+ return (0, import_node_crypto.createHash)("sha256").update(verifier, "utf8").digest("base64url");
883
+ }
884
+ function generateState() {
885
+ return (0, import_node_crypto.randomBytes)(16).toString("base64url");
886
+ }
887
+ function nowSecs() {
888
+ return Math.floor(Date.now() / 1e3);
889
+ }
890
+ function constantTimeEqual(a, b) {
891
+ const ab = Buffer.from(a, "utf8");
892
+ const bb = Buffer.from(b, "utf8");
893
+ if (ab.length !== bb.length) return false;
894
+ if (ab.length === 0) return true;
895
+ return (0, import_node_crypto.timingSafeEqual)(ab, bb);
896
+ }
897
+ function urlencode(value) {
898
+ let out = "";
899
+ for (const byte of Buffer.from(value, "utf8")) {
900
+ const ch = String.fromCharCode(byte);
901
+ if (/[A-Za-z0-9\-._~]/.test(ch)) {
902
+ out += ch;
903
+ } else {
904
+ out += `%${byte.toString(16).toUpperCase().padStart(2, "0")}`;
905
+ }
906
+ }
907
+ return out;
908
+ }
909
+ function originOf(url) {
910
+ try {
911
+ const parsed = new URL(url);
912
+ if (!parsed.hostname) return void 0;
913
+ return parsed.port ? `${parsed.protocol}//${parsed.hostname}:${parsed.port}` : `${parsed.protocol}//${parsed.hostname}`;
914
+ } catch {
915
+ return void 0;
916
+ }
917
+ }
918
+ async function fetchResourceMetadata(fetchImpl, resource) {
919
+ const origin = originOf(resource);
920
+ const candidates = [
921
+ `${resource}/.well-known/oauth-protected-resource`,
922
+ origin ? `${origin}/.well-known/oauth-protected-resource` : void 0
923
+ ].filter((u) => u !== void 0);
924
+ for (const url of candidates) {
925
+ try {
926
+ const resp = await fetchImpl(url);
927
+ if (resp.ok) {
928
+ return await resp.json();
929
+ }
930
+ } catch {
931
+ }
932
+ }
933
+ return void 0;
934
+ }
935
+ async function fetchAsMetadata(fetchImpl, issuer) {
936
+ const base = issuer.replace(/\/+$/, "");
937
+ const candidates = [
938
+ `${base}/.well-known/oauth-authorization-server`,
939
+ `${base}/.well-known/openid-configuration`
940
+ ];
941
+ let last = "";
942
+ for (const url of candidates) {
943
+ try {
944
+ const resp = await fetchImpl(url);
945
+ if (resp.ok) {
946
+ try {
947
+ return await resp.json();
948
+ } catch (err) {
949
+ throw new AuthCodeError(
950
+ "discovery",
951
+ `OAuth discovery failed: bad metadata at ${url}: ${err}`
952
+ );
953
+ }
954
+ }
955
+ last = `${url} \u2192 HTTP ${resp.status}`;
956
+ } catch (err) {
957
+ if (err instanceof AuthCodeError) throw err;
958
+ last = `${url} \u2192 ${err}`;
959
+ }
960
+ }
961
+ throw new AuthCodeError(
962
+ "discovery",
963
+ `OAuth discovery failed: no authorization server metadata found (last attempt: ${last})`
964
+ );
965
+ }
966
+ async function postForm(fetchImpl, endpoint, form) {
967
+ let response;
968
+ try {
969
+ response = await fetchImpl(endpoint, {
970
+ method: "POST",
971
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
972
+ body: new URLSearchParams(form).toString()
973
+ });
974
+ } catch (err) {
975
+ throw new AuthCodeError("http", `HTTP error: ${err}`);
976
+ }
977
+ if (!response.ok) {
978
+ const body = await response.text().catch(() => "");
979
+ throw new AuthCodeError(
980
+ "token_exchange",
981
+ `token exchange failed (HTTP ${response.status}): ${body}`,
982
+ { status: response.status, body }
983
+ );
984
+ }
985
+ try {
986
+ return await response.json();
987
+ } catch (err) {
988
+ throw new AuthCodeError("http", `bad token response: ${err}`);
989
+ }
990
+ }
991
+
992
+ // src/transports/mcp.ts
479
993
  var import_client = require("@modelcontextprotocol/sdk/client/index.js");
480
994
  var import_sse = require("@modelcontextprotocol/sdk/client/sse.js");
481
995
  var import_stdio = require("@modelcontextprotocol/sdk/client/stdio.js");
@@ -486,6 +1000,10 @@ var MCPTransport = class extends Transport {
486
1000
  client;
487
1001
  clientTransport;
488
1002
  oauthProvider;
1003
+ /** Present only when `auth.authorizationCode` is set. */
1004
+ authCodeProvider;
1005
+ /** Present only when `auth.delegation` is set (RFC 8693). */
1006
+ delegatedProvider;
489
1007
  constructor(url, auth, identity) {
490
1008
  super();
491
1009
  this.url = url;
@@ -501,12 +1019,20 @@ var MCPTransport = class extends Transport {
501
1019
  scope: cc.scope
502
1020
  });
503
1021
  }
1022
+ this.authCodeProvider = authCodeProviderFrom(auth?.authorizationCode);
1023
+ this.delegatedProvider = auth?.delegation;
504
1024
  }
505
1025
  async buildHeaders() {
506
1026
  const headers = {};
507
- if (this.oauthProvider) {
1027
+ if (this.delegatedProvider) {
1028
+ const token = await this.delegatedProvider.getToken();
1029
+ headers["Authorization"] = `Bearer ${token}`;
1030
+ } else if (this.oauthProvider) {
508
1031
  const token = await this.oauthProvider.getToken();
509
1032
  headers["Authorization"] = `Bearer ${token}`;
1033
+ } else if (this.authCodeProvider) {
1034
+ const token = await this.authCodeProvider.getToken();
1035
+ headers["Authorization"] = `Bearer ${token}`;
510
1036
  } else if (this.auth?.bearer) {
511
1037
  headers["Authorization"] = `Bearer ${this.auth.bearer}`;
512
1038
  } else if (this.auth?.basic) {
@@ -536,7 +1062,10 @@ var MCPTransport = class extends Transport {
536
1062
  if (this.identity) {
537
1063
  const id = this.identity;
538
1064
  transportOpts.fetcher = (url, init) => {
539
- const { fetchWithIdentity: fetchWithIdentity2 } = (init_identity(), __toCommonJS(identity_exports));
1065
+ const { fetchWithIdentity: fetchWithIdentity2 } = (
1066
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
1067
+ (init_identity(), __toCommonJS(identity_exports))
1068
+ );
540
1069
  return fetchWithIdentity2(
541
1070
  typeof url === "string" ? url : url instanceof URL ? url.toString() : url.url,
542
1071
  init ?? {},
@@ -552,7 +1081,7 @@ var MCPTransport = class extends Transport {
552
1081
  throw new Error(`Unsupported MCP URL scheme: ${this.url}`);
553
1082
  }
554
1083
  this.client = new import_client.Client(
555
- { name: "datagrout-conduit", version: "0.1.0" },
1084
+ { name: "datagrout-conduit", version },
556
1085
  { capabilities: {} }
557
1086
  );
558
1087
  await this.client.connect(this.clientTransport);
@@ -574,7 +1103,11 @@ var MCPTransport = class extends Transport {
574
1103
  annotations: tool.annotations
575
1104
  }));
576
1105
  }
577
- async callTool(name, args, options) {
1106
+ // `_options` on this and the four methods below: accepted for signature
1107
+ // parity with the other conduit SDKs, not yet consulted here. The leading
1108
+ // underscore is the codebase's marker for a deliberately unused parameter,
1109
+ // and what tsconfig's `noUnusedParameters` exempts.
1110
+ async callTool(name, args, _options) {
578
1111
  if (!this.client) {
579
1112
  throw new Error("Not connected. Call connect() first.");
580
1113
  }
@@ -596,7 +1129,7 @@ var MCPTransport = class extends Transport {
596
1129
  }
597
1130
  return result;
598
1131
  }
599
- async listResources(options) {
1132
+ async listResources(_options) {
600
1133
  if (!this.client) {
601
1134
  throw new Error("Not connected. Call connect() first.");
602
1135
  }
@@ -608,14 +1141,14 @@ var MCPTransport = class extends Transport {
608
1141
  mimeType: resource.mimeType
609
1142
  }));
610
1143
  }
611
- async readResource(uri, options) {
1144
+ async readResource(uri, _options) {
612
1145
  if (!this.client) {
613
1146
  throw new Error("Not connected. Call connect() first.");
614
1147
  }
615
1148
  const result = await this.client.readResource({ uri });
616
1149
  return result.contents;
617
1150
  }
618
- async listPrompts(options) {
1151
+ async listPrompts(_options) {
619
1152
  if (!this.client) {
620
1153
  throw new Error("Not connected. Call connect() first.");
621
1154
  }
@@ -626,7 +1159,7 @@ var MCPTransport = class extends Transport {
626
1159
  arguments: prompt.arguments
627
1160
  }));
628
1161
  }
629
- async getPrompt(name, args, options) {
1162
+ async getPrompt(name, args, _options) {
630
1163
  if (!this.client) {
631
1164
  throw new Error("Not connected. Call connect() first.");
632
1165
  }
@@ -638,56 +1171,6 @@ var MCPTransport = class extends Transport {
638
1171
  // src/transports/jsonrpc.ts
639
1172
  init_identity();
640
1173
  init_oauth();
641
-
642
- // src/errors.ts
643
- var ConduitError = class extends Error {
644
- constructor(message) {
645
- super(message);
646
- this.name = this.constructor.name;
647
- Object.setPrototypeOf(this, new.target.prototype);
648
- }
649
- };
650
- var NotInitializedError = class extends ConduitError {
651
- constructor() {
652
- super("Client not initialized. Call connect() first.");
653
- }
654
- };
655
- var RateLimitError = class extends ConduitError {
656
- status;
657
- retryAfter;
658
- constructor(status, retryAfter) {
659
- const limitStr = status.limit === "unlimited" ? "unlimited" : `${status.limit.perHour}/hour`;
660
- super(`Rate limit exceeded (${status.used} / ${limitStr} calls this hour)`);
661
- this.status = status;
662
- this.retryAfter = retryAfter;
663
- }
664
- };
665
- var AuthError = class extends ConduitError {
666
- constructor(message = "Authentication failed") {
667
- super(message);
668
- }
669
- };
670
- var NetworkError = class extends ConduitError {
671
- constructor(message) {
672
- super(message);
673
- }
674
- };
675
- var ServerError = class extends ConduitError {
676
- code;
677
- serverMessage;
678
- constructor(code, serverMessage) {
679
- super(`Server error ${code}: ${serverMessage}`);
680
- this.code = code;
681
- this.serverMessage = serverMessage;
682
- }
683
- };
684
- var InvalidConfigError = class extends ConduitError {
685
- constructor(message) {
686
- super(message);
687
- }
688
- };
689
-
690
- // src/transports/jsonrpc.ts
691
1174
  function unwrapContent(result) {
692
1175
  if (!result) return result;
693
1176
  if (result.structuredContent !== void 0) {
@@ -722,6 +1205,10 @@ var JSONRPCTransport = class extends Transport {
722
1205
  requestId = 0;
723
1206
  /** Resolved token provider, present only when `auth.clientCredentials` is set. */
724
1207
  oauthProvider;
1208
+ /** Present only when `auth.authorizationCode` is set. */
1209
+ authCodeProvider;
1210
+ /** Present only when `auth.delegation` is set (RFC 8693). */
1211
+ delegatedProvider;
725
1212
  constructor(url, auth, timeout = 3e4, identity) {
726
1213
  super();
727
1214
  this.url = url;
@@ -736,7 +1223,10 @@ var JSONRPCTransport = class extends Transport {
736
1223
  if (auth?.clientCredentials) {
737
1224
  const cc = auth.clientCredentials;
738
1225
  const tokenEndpoint = cc.tokenEndpoint ?? (() => {
739
- const { deriveTokenEndpoint: deriveTokenEndpoint2 } = (init_oauth(), __toCommonJS(oauth_exports));
1226
+ const { deriveTokenEndpoint: deriveTokenEndpoint2 } = (
1227
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
1228
+ (init_oauth(), __toCommonJS(oauth_exports))
1229
+ );
740
1230
  return deriveTokenEndpoint2(url);
741
1231
  })();
742
1232
  this.oauthProvider = new OAuthTokenProvider({
@@ -746,6 +1236,8 @@ var JSONRPCTransport = class extends Transport {
746
1236
  scope: cc.scope
747
1237
  });
748
1238
  }
1239
+ this.authCodeProvider = authCodeProviderFrom(auth?.authorizationCode);
1240
+ this.delegatedProvider = auth?.delegation;
749
1241
  }
750
1242
  async connect() {
751
1243
  }
@@ -758,9 +1250,15 @@ var JSONRPCTransport = class extends Transport {
758
1250
  const headers = {
759
1251
  "Content-Type": "application/json"
760
1252
  };
761
- if (this.oauthProvider) {
1253
+ if (this.delegatedProvider) {
1254
+ const token = await this.delegatedProvider.getToken();
1255
+ headers["Authorization"] = `Bearer ${token}`;
1256
+ } else if (this.oauthProvider) {
762
1257
  const token = await this.oauthProvider.getToken();
763
1258
  headers["Authorization"] = `Bearer ${token}`;
1259
+ } else if (this.authCodeProvider) {
1260
+ const token = await this.authCodeProvider.getToken();
1261
+ headers["Authorization"] = `Bearer ${token}`;
764
1262
  } else if (this.auth?.bearer) {
765
1263
  headers["Authorization"] = `Bearer ${this.auth.bearer}`;
766
1264
  } else if (this.auth?.basic) {
@@ -790,9 +1288,19 @@ var JSONRPCTransport = class extends Transport {
790
1288
  if (response.status === 429) {
791
1289
  throw parseRateLimitError(response);
792
1290
  }
793
- if (response.status === 401 && this.oauthProvider && !isRetry) {
794
- this.oauthProvider.invalidate();
795
- return this._callWithRetry(method, params, true);
1291
+ if (response.status === 401 && !isRetry) {
1292
+ if (this.delegatedProvider) {
1293
+ this.delegatedProvider.invalidate();
1294
+ return this._callWithRetry(method, params, true);
1295
+ }
1296
+ if (this.oauthProvider) {
1297
+ this.oauthProvider.invalidate();
1298
+ return this._callWithRetry(method, params, true);
1299
+ }
1300
+ if (this.authCodeProvider) {
1301
+ this.authCodeProvider.invalidate();
1302
+ return this._callWithRetry(method, params, true);
1303
+ }
796
1304
  }
797
1305
  if (!response.ok) {
798
1306
  throw new Error(`HTTP ${response.status}: ${response.statusText}`);
@@ -834,6 +1342,7 @@ var JSONRPCTransport = class extends Transport {
834
1342
  };
835
1343
 
836
1344
  // src/transports/ws.ts
1345
+ init_oauth();
837
1346
  var SUBPROTOCOL = "datagrout-jsonrpc.v1";
838
1347
  var SUBSCRIPTION_BUFFER = 256;
839
1348
  var PING_INTERVAL_MS = 25e3;
@@ -897,6 +1406,23 @@ var Subscription = class {
897
1406
  var WsTransport = class extends Transport {
898
1407
  _url;
899
1408
  _auth;
1409
+ /**
1410
+ * mTLS identity presented on the `wss://` handshake, if any. The HTTP
1411
+ * transports route through {@link fetchWithIdentity}; here the PEMs go to the
1412
+ * `ws` client as `cert` / `key` / `ca` options, which it forwards to
1413
+ * `tls.connect`. Mirrors `build_connector` in the Rust reference.
1414
+ */
1415
+ _identity;
1416
+ /**
1417
+ * Resolved OAuth providers, built once so a token survives reconnects.
1418
+ *
1419
+ * All are consulted in {@link _resolveBearer} before the upgrade request is
1420
+ * built — see the note there on why that has to happen up front.
1421
+ */
1422
+ _oauthProvider;
1423
+ _authCodeProvider;
1424
+ /** RFC 8693 delegation, when `auth.delegation` is set. */
1425
+ _delegatedProvider;
900
1426
  _ws = null;
901
1427
  _nextId = 0;
902
1428
  _pending = /* @__PURE__ */ new Map();
@@ -913,7 +1439,7 @@ var WsTransport = class extends Transport {
913
1439
  * defaults to {@link PING_INTERVAL_MS}.
914
1440
  */
915
1441
  _pingIntervalMs = PING_INTERVAL_MS;
916
- constructor(url, auth, _timeout, _identity) {
1442
+ constructor(url, auth, _timeout, identity) {
917
1443
  super();
918
1444
  const scheme = new URL(url).protocol.replace(":", "");
919
1445
  if (scheme !== "ws" && scheme !== "wss") {
@@ -923,14 +1449,46 @@ var WsTransport = class extends Transport {
923
1449
  }
924
1450
  this._url = url;
925
1451
  this._auth = auth;
1452
+ this._identity = identity;
1453
+ if (auth?.clientCredentials) {
1454
+ const cc = auth.clientCredentials;
1455
+ const tokenEndpoint = cc.tokenEndpoint ?? deriveTokenEndpoint(url.replace(/^ws/, "http"));
1456
+ this._oauthProvider = new OAuthTokenProvider({
1457
+ clientId: cc.clientId,
1458
+ clientSecret: cc.clientSecret,
1459
+ tokenEndpoint,
1460
+ scope: cc.scope
1461
+ });
1462
+ }
1463
+ this._authCodeProvider = authCodeProviderFrom(auth?.authorizationCode);
1464
+ this._delegatedProvider = auth?.delegation;
926
1465
  }
927
1466
  // ── Lifecycle ─────────────────────────────────────────────────────────────
1467
+ /**
1468
+ * The bearer to put on the upgrade request, if any.
1469
+ *
1470
+ * Resolved *before* the handshake is built. Fetching a token is async while
1471
+ * header construction is not, so a provider-backed token could never reach
1472
+ * the upgrade if it were resolved inside the header builder — which is
1473
+ * exactly the bug this replaced: an OAuth client authenticated over WS only
1474
+ * if it also happened to present an mTLS identity.
1475
+ */
1476
+ async _resolveBearer() {
1477
+ if (this._delegatedProvider) return this._delegatedProvider.getToken();
1478
+ if (this._oauthProvider) return this._oauthProvider.getToken();
1479
+ if (this._authCodeProvider) return this._authCodeProvider.getToken();
1480
+ return void 0;
1481
+ }
928
1482
  async connect() {
929
1483
  if (this._ws !== null) return;
930
1484
  const WsImpl = await resolveWebSocketImpl();
931
- const headers = buildUpgradeHeaders(this._auth);
1485
+ const headers = buildUpgradeHeaders(
1486
+ this._auth,
1487
+ await this._resolveBearer()
1488
+ );
932
1489
  const ws = new WsImpl(this._url, [SUBPROTOCOL], {
933
- headers
1490
+ headers,
1491
+ ...buildTlsOptions(this._url, this._identity)
934
1492
  });
935
1493
  await new Promise((resolve, reject) => {
936
1494
  ws.onopen = () => resolve();
@@ -1164,8 +1722,12 @@ var WsTransport = class extends Transport {
1164
1722
  this._subscriptions.clear();
1165
1723
  }
1166
1724
  };
1167
- function buildUpgradeHeaders(auth) {
1725
+ function buildUpgradeHeaders(auth, resolvedBearer) {
1168
1726
  const headers = {};
1727
+ if (resolvedBearer !== void 0) {
1728
+ headers["Authorization"] = `Bearer ${resolvedBearer}`;
1729
+ return headers;
1730
+ }
1169
1731
  if (auth === void 0) return headers;
1170
1732
  if ("bearer" in auth && auth.bearer !== void 0) {
1171
1733
  headers["Authorization"] = `Bearer ${auth.bearer}`;
@@ -1179,6 +1741,21 @@ function buildUpgradeHeaders(auth) {
1179
1741
  }
1180
1742
  return headers;
1181
1743
  }
1744
+ function buildTlsOptions(url, identity) {
1745
+ if (identity === void 0) return {};
1746
+ if (!url.startsWith("wss:")) return {};
1747
+ if (typeof process === "undefined" || !process.versions?.node) {
1748
+ console.warn(
1749
+ "[conduit] mTLS identity is set but this environment does not support client certificates on WebSocket. The connection will proceed without mTLS."
1750
+ );
1751
+ return {};
1752
+ }
1753
+ return {
1754
+ cert: identity.certPem,
1755
+ key: identity.keyPem,
1756
+ ...identity.caPem ? { ca: identity.caPem } : {}
1757
+ };
1758
+ }
1182
1759
  async function resolveWebSocketImpl() {
1183
1760
  if (typeof globalThis.WebSocket !== "undefined") {
1184
1761
  return globalThis.WebSocket;
@@ -1364,6 +1941,8 @@ var PrismNamespace = class {
1364
1941
  this.callDg = callDg;
1365
1942
  this.warn = warn;
1366
1943
  }
1944
+ callDg;
1945
+ warn;
1367
1946
  /** AI-driven data transformation (`data-grout/prism.refract`). */
1368
1947
  async refract(options) {
1369
1948
  this.warn("prism.refract");
@@ -1433,6 +2012,8 @@ var LogicNamespace = class {
1433
2012
  this.callDg = callDg;
1434
2013
  this.warn = warn;
1435
2014
  }
2015
+ callDg;
2016
+ warn;
1436
2017
  async remember(statementOrOptions, optionsArg) {
1437
2018
  let statement;
1438
2019
  let opts;
@@ -1541,6 +2122,8 @@ var WardenNamespace = class {
1541
2122
  this.callDg = callDg;
1542
2123
  this.warn = warn;
1543
2124
  }
2125
+ callDg;
2126
+ warn;
1544
2127
  /** Run a canary safety check (`data-grout/warden.canary`). */
1545
2128
  async canary(params) {
1546
2129
  this.warn("warden.canary");
@@ -1570,6 +2153,8 @@ var DeliverablesNamespace = class {
1570
2153
  this.callDg = callDg;
1571
2154
  this.warn = warn;
1572
2155
  }
2156
+ callDg;
2157
+ warn;
1573
2158
  /** Register a work product (`data-grout/deliverables.register`). */
1574
2159
  async register(params) {
1575
2160
  this.warn("deliverables.register");
@@ -1594,6 +2179,8 @@ var EphemeralsNamespace = class {
1594
2179
  this.callDg = callDg;
1595
2180
  this.warn = warn;
1596
2181
  }
2182
+ callDg;
2183
+ warn;
1597
2184
  /** List cached results (`data-grout/ephemerals.list`). */
1598
2185
  async list(params = {}) {
1599
2186
  this.warn("ephemerals.list");
@@ -1613,6 +2200,8 @@ var FlowNamespace = class {
1613
2200
  this.callDg = callDg;
1614
2201
  this.warn = warn;
1615
2202
  }
2203
+ callDg;
2204
+ warn;
1616
2205
  /** Execute a multi-step workflow plan (`data-grout/flow.into`). */
1617
2206
  async run(options) {
1618
2207
  this.warn("flow.into");
@@ -1730,6 +2319,9 @@ var GuidedSession = class {
1730
2319
  function isDgUrl(url) {
1731
2320
  return url.includes("datagrout.ai") || url.includes("datagrout.dev") || !!process.env["CONDUIT_IS_DG"];
1732
2321
  }
2322
+ function isDgNativeTool(name) {
2323
+ return !name.includes("@") || name.startsWith("data-grout@") || name.startsWith("data-grout/");
2324
+ }
1733
2325
  var Client2 = class _Client {
1734
2326
  url;
1735
2327
  auth;
@@ -1999,7 +2591,7 @@ var Client2 = class _Client {
1999
2591
  async listTools(options) {
2000
2592
  this.ensureInitialized();
2001
2593
  return this.sendWithRetry(async () => {
2002
- let allTools = [];
2594
+ const allTools = [];
2003
2595
  let cursor;
2004
2596
  do {
2005
2597
  const response = await this.transport.listTools({ ...options, cursor });
@@ -2008,7 +2600,7 @@ var Client2 = class _Client {
2008
2600
  cursor = Array.isArray(response) ? void 0 : response.nextCursor;
2009
2601
  } while (cursor);
2010
2602
  if (this.useIntelligentInterface) {
2011
- return allTools.filter((t) => !t.name.includes("@"));
2603
+ return allTools.filter((t) => isDgNativeTool(t.name));
2012
2604
  }
2013
2605
  return allTools;
2014
2606
  });
@@ -2337,6 +2929,637 @@ var Client2 = class _Client {
2337
2929
  init_identity();
2338
2930
  init_oauth();
2339
2931
 
2932
+ // src/delegation.ts
2933
+ var GRANT_TYPE = "urn:ietf:params:oauth:grant-type:token-exchange";
2934
+ var INSPECT_CUSTOM = /* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom");
2935
+ var REFRESH_SKEW_SECS2 = 60;
2936
+ var SERVER_ERROR_CODES = Object.freeze([
2937
+ /** Malformed request, or a required parameter missing. */
2938
+ "invalid_request",
2939
+ /** Client authentication failed. */
2940
+ "invalid_client",
2941
+ /** The subject or actor token is invalid, expired, or revoked. */
2942
+ "invalid_grant",
2943
+ /** This client may not use this grant — including a client that is not the actor. */
2944
+ "unauthorized_client",
2945
+ /** The requested `audience` or `resource` is not served here (RFC 8693 §2.2.2). */
2946
+ "invalid_target",
2947
+ /** A requested scope is unknown or exceeds what the subject token allows. */
2948
+ "invalid_scope",
2949
+ /** The server does not support the exchange. */
2950
+ "unsupported_grant_type"
2951
+ ]);
2952
+ var TOKEN_TYPES = Object.freeze({
2953
+ /** The default for both subject and actor, and what DataGrout issues. */
2954
+ access_token: "urn:ietf:params:oauth:token-type:access_token",
2955
+ /** A JWT presented as a JWT rather than as an opaque access token. */
2956
+ jwt: "urn:ietf:params:oauth:token-type:jwt",
2957
+ id_token: "urn:ietf:params:oauth:token-type:id_token",
2958
+ refresh_token: "urn:ietf:params:oauth:token-type:refresh_token",
2959
+ saml2: "urn:ietf:params:oauth:token-type:saml2"
2960
+ });
2961
+ function tokenTypeName(tokenType) {
2962
+ for (const [name, urn] of Object.entries(TOKEN_TYPES)) {
2963
+ if (urn === tokenType) return name;
2964
+ }
2965
+ return "other";
2966
+ }
2967
+ var DelegationError = class extends ConduitError {
2968
+ kind;
2969
+ /** HTTP status, for `server`. */
2970
+ status;
2971
+ /** RFC 6749 error code, for `server`; see {@link SERVER_ERROR_CODES}. */
2972
+ error;
2973
+ /** Human-readable description, when the server gave one. */
2974
+ errorDescription;
2975
+ constructor(kind, message, extra) {
2976
+ super(message);
2977
+ this.kind = kind;
2978
+ this.status = extra?.status;
2979
+ this.error = extra?.error;
2980
+ this.errorDescription = extra?.errorDescription;
2981
+ }
2982
+ };
2983
+ var DelegationRequest = class _DelegationRequest {
2984
+ endpoint;
2985
+ client;
2986
+ secret;
2987
+ auth = "body";
2988
+ subject;
2989
+ actor;
2990
+ audienceValue;
2991
+ resourceValue;
2992
+ scopeValue;
2993
+ requestedTokenTypeValue;
2994
+ impersonating = false;
2995
+ /**
2996
+ * Start a request against `tokenEndpoint`, authenticating as `clientId`.
2997
+ *
2998
+ * The client should be the actor — see the module docs.
2999
+ */
3000
+ constructor(tokenEndpoint, clientId) {
3001
+ this.endpoint = tokenEndpoint;
3002
+ this.client = clientId;
3003
+ }
3004
+ /** The client secret, for confidential clients. */
3005
+ clientSecret(secret) {
3006
+ this.secret = secret;
3007
+ return this;
3008
+ }
3009
+ /** Where the client secret travels. Defaults to `"body"`. */
3010
+ clientAuth(auth) {
3011
+ this.auth = auth;
3012
+ return this;
3013
+ }
3014
+ /**
3015
+ * The token being exchanged: the **user's**, whose identity the issued token
3016
+ * will carry as `sub`.
3017
+ */
3018
+ subjectToken(token, tokenType = TOKEN_TYPES.access_token) {
3019
+ this.subject = { token, tokenType };
3020
+ return this;
3021
+ }
3022
+ /** The **agent's** own token, which the issued token will name in `act`. */
3023
+ actorToken(token, tokenType = TOKEN_TYPES.access_token) {
3024
+ this.actor = { token, tokenType };
3025
+ return this;
3026
+ }
3027
+ /** Logical name of the service the token is for (RFC 8693 `audience`). */
3028
+ audience(audience) {
3029
+ this.audienceValue = audience;
3030
+ return this;
3031
+ }
3032
+ /**
3033
+ * URI of the resource the token is for (RFC 8707 `resource`). Always sent
3034
+ * when set, so the token cannot be replayed elsewhere.
3035
+ */
3036
+ resource(resource) {
3037
+ this.resourceValue = resource;
3038
+ return this;
3039
+ }
3040
+ /** Scopes to request, space-separated. */
3041
+ scope(scope) {
3042
+ this.scopeValue = scope;
3043
+ return this;
3044
+ }
3045
+ /** The kind of token wanted back. Servers default to an access token. */
3046
+ requestedTokenType(tokenType) {
3047
+ this.requestedTokenTypeValue = tokenType;
3048
+ return this;
3049
+ }
3050
+ /**
3051
+ * Opt out of delegation: send no `actor_token`, so the issued token has no
3052
+ * `act` claim and the agent is indistinguishable from the user.
3053
+ *
3054
+ * DataGrout does not issue these. This exists for other RFC 8693 servers, and
3055
+ * it is a builder call rather than a default precisely so that forgetting to
3056
+ * set an actor is an error instead of a silent downgrade.
3057
+ */
3058
+ impersonation() {
3059
+ this.impersonating = true;
3060
+ return this;
3061
+ }
3062
+ /** The token endpoint this request posts to. */
3063
+ get tokenEndpoint() {
3064
+ return this.endpoint;
3065
+ }
3066
+ /** The client id this request authenticates as. */
3067
+ get clientId() {
3068
+ return this.client;
3069
+ }
3070
+ /** Whether {@link impersonation} was called. */
3071
+ get isImpersonation() {
3072
+ return this.impersonating;
3073
+ }
3074
+ /** An independent copy, so a template can be filled in per exchange. */
3075
+ clone() {
3076
+ const copy = new _DelegationRequest(this.endpoint, this.client);
3077
+ copy.secret = this.secret;
3078
+ copy.auth = this.auth;
3079
+ copy.subject = this.subject && { ...this.subject };
3080
+ copy.actor = this.actor && { ...this.actor };
3081
+ copy.audienceValue = this.audienceValue;
3082
+ copy.resourceValue = this.resourceValue;
3083
+ copy.scopeValue = this.scopeValue;
3084
+ copy.requestedTokenTypeValue = this.requestedTokenTypeValue;
3085
+ copy.impersonating = this.impersonating;
3086
+ return copy;
3087
+ }
3088
+ /**
3089
+ * The form body this request will post, in wire order.
3090
+ *
3091
+ * Throws before any network activity when the request is incomplete:
3092
+ * `missing_subject`, or `missing_actor` unless {@link impersonation} was
3093
+ * called. Public so a caller — or another SDK's test suite — can check the
3094
+ * body against the contract fixture without a server.
3095
+ */
3096
+ formParams() {
3097
+ if (this.subject === void 0) {
3098
+ throw new DelegationError(
3099
+ "missing_subject",
3100
+ "no subject_token \u2014 call subjectToken() first"
3101
+ );
3102
+ }
3103
+ const form = [
3104
+ ["grant_type", GRANT_TYPE],
3105
+ ["subject_token", this.subject.token],
3106
+ ["subject_token_type", this.subject.tokenType]
3107
+ ];
3108
+ if (this.actor !== void 0) {
3109
+ form.push(["actor_token", this.actor.token]);
3110
+ form.push(["actor_token_type", this.actor.tokenType]);
3111
+ } else if (!this.impersonating) {
3112
+ throw new DelegationError(
3113
+ "missing_actor",
3114
+ "no actor_token \u2014 delegation requires one; call impersonation() to opt out explicitly"
3115
+ );
3116
+ }
3117
+ form.push(["client_id", this.client]);
3118
+ if (this.secret !== void 0 && this.auth === "body") {
3119
+ form.push(["client_secret", this.secret]);
3120
+ }
3121
+ for (const [key, value] of [
3122
+ ["audience", this.audienceValue],
3123
+ ["resource", this.resourceValue],
3124
+ ["scope", this.scopeValue],
3125
+ ["requested_token_type", this.requestedTokenTypeValue]
3126
+ ]) {
3127
+ if (value !== void 0) form.push([key, value]);
3128
+ }
3129
+ return form;
3130
+ }
3131
+ /** Perform the exchange. */
3132
+ async exchange(fetchImpl = globalThis.fetch) {
3133
+ const form = this.formParams();
3134
+ const headers = {
3135
+ "Content-Type": "application/x-www-form-urlencoded"
3136
+ };
3137
+ if (this.secret !== void 0 && this.auth === "basic") {
3138
+ const credentials = Buffer.from(
3139
+ `${this.client}:${this.secret}`,
3140
+ "utf8"
3141
+ ).toString("base64");
3142
+ headers["Authorization"] = `Basic ${credentials}`;
3143
+ }
3144
+ let response;
3145
+ try {
3146
+ response = await fetchImpl(this.endpoint, {
3147
+ method: "POST",
3148
+ headers,
3149
+ // `URLSearchParams` keeps the order it is given, which is the wire
3150
+ // order the contract fixture pins.
3151
+ body: new URLSearchParams(form).toString()
3152
+ });
3153
+ } catch (err) {
3154
+ throw new DelegationError("http", `HTTP error: ${err}`);
3155
+ }
3156
+ const body = await response.text().catch(() => "");
3157
+ if (!response.ok) {
3158
+ throw errorFromBody(response.status, body);
3159
+ }
3160
+ let parsed;
3161
+ try {
3162
+ parsed = JSON.parse(body);
3163
+ } catch (err) {
3164
+ throw new DelegationError(
3165
+ "invalid_response",
3166
+ `HTTP ${response.status}: ${err}`
3167
+ );
3168
+ }
3169
+ return tokenFromWire(parsed, response.status);
3170
+ }
3171
+ };
3172
+ function errorFromBody(status, body) {
3173
+ let parsed;
3174
+ try {
3175
+ parsed = JSON.parse(body);
3176
+ } catch {
3177
+ parsed = void 0;
3178
+ }
3179
+ const oauth = parsed;
3180
+ if (oauth && typeof oauth.error === "string") {
3181
+ const description = typeof oauth.error_description === "string" ? oauth.error_description : void 0;
3182
+ return new DelegationError(
3183
+ "server",
3184
+ `delegation exchange refused (HTTP ${status}): ${oauth.error}` + (description ? ` \u2014 ${description}` : ""),
3185
+ { status, error: oauth.error, errorDescription: description }
3186
+ );
3187
+ }
3188
+ return new DelegationError(
3189
+ "invalid_response",
3190
+ `HTTP ${status} with a non-OAuth body: ${body.slice(0, 200)}`
3191
+ );
3192
+ }
3193
+ function tokenFromWire(parsed, status) {
3194
+ const wire = parsed;
3195
+ const missing = ["access_token", "issued_token_type", "token_type"].filter(
3196
+ (field) => typeof wire?.[field] !== "string"
3197
+ );
3198
+ if (missing.length > 0) {
3199
+ throw new DelegationError(
3200
+ "invalid_response",
3201
+ `HTTP ${status}: delegation response is missing required field(s): ${missing.join(", ")}`
3202
+ );
3203
+ }
3204
+ const expiresIn = wire["expires_in"];
3205
+ const scope = wire["scope"];
3206
+ return {
3207
+ access_token: wire["access_token"],
3208
+ issued_token_type: wire["issued_token_type"],
3209
+ token_type: wire["token_type"],
3210
+ ...typeof expiresIn === "number" ? { expires_at: nowSecs2() + expiresIn } : {},
3211
+ ...typeof scope === "string" ? { scope } : {}
3212
+ };
3213
+ }
3214
+ function isDelegatedTokenExpired(token) {
3215
+ if (token.expires_at === void 0) return false;
3216
+ return nowSecs2() + REFRESH_SKEW_SECS2 >= token.expires_at;
3217
+ }
3218
+ var TokenSource = class _TokenSource {
3219
+ resolver;
3220
+ sourceKind;
3221
+ declaredType;
3222
+ constructor(kind, tokenType, resolver) {
3223
+ this.sourceKind = kind;
3224
+ this.declaredType = tokenType;
3225
+ this.resolver = resolver;
3226
+ }
3227
+ /** A fixed token, e.g. one handed to the agent for this run. */
3228
+ static staticToken(token, tokenType = TOKEN_TYPES.access_token) {
3229
+ return new _TokenSource("static", tokenType, async () => token);
3230
+ }
3231
+ /** The agent's own `client_credentials` provider — the usual **actor**. */
3232
+ static clientCredentials(provider, tokenType = TOKEN_TYPES.access_token) {
3233
+ return new _TokenSource(
3234
+ "client_credentials",
3235
+ tokenType,
3236
+ () => provider.getToken()
3237
+ );
3238
+ }
3239
+ /**
3240
+ * A user's authorization-code provider — the usual **subject** in an app that
3241
+ * signed the user in itself. Refreshes its grant as needed, so the exchange
3242
+ * always sees a live subject token.
3243
+ */
3244
+ static authorizationCode(provider, tokenType = TOKEN_TYPES.access_token) {
3245
+ return new _TokenSource(
3246
+ "authorization_code",
3247
+ tokenType,
3248
+ () => provider.getToken()
3249
+ );
3250
+ }
3251
+ /**
3252
+ * Any function that yields a token — a vault lookup, a header from an inbound
3253
+ * request, another SDK's provider. Called on every exchange.
3254
+ */
3255
+ static dynamic(fn, tokenType = TOKEN_TYPES.access_token) {
3256
+ return new _TokenSource("dynamic", tokenType, async () => fn());
3257
+ }
3258
+ /** Declare a different {@link TokenType} for this source. */
3259
+ withTokenType(tokenType) {
3260
+ return new _TokenSource(this.sourceKind, tokenType, this.resolver);
3261
+ }
3262
+ /** The declared token type. */
3263
+ get tokenType() {
3264
+ return this.declaredType;
3265
+ }
3266
+ /** Where the token comes from. */
3267
+ get kind() {
3268
+ return this.sourceKind;
3269
+ }
3270
+ /** Draw a token. */
3271
+ resolve() {
3272
+ return this.resolver();
3273
+ }
3274
+ /** Never print tokens — only where they come from. */
3275
+ toJSON() {
3276
+ return { kind: this.sourceKind, tokenType: this.declaredType };
3277
+ }
3278
+ [INSPECT_CUSTOM]() {
3279
+ return `TokenSource { kind: '${this.sourceKind}', tokenType: '${this.declaredType}' }`;
3280
+ }
3281
+ };
3282
+ var DelegatedProvider = class {
3283
+ template;
3284
+ subject;
3285
+ actor;
3286
+ fetchImpl;
3287
+ cached = null;
3288
+ /**
3289
+ * The in-flight exchange, so concurrent callers make one request rather than
3290
+ * a stampede. Mirrors `AuthCodeProvider`'s refresh de-duplication.
3291
+ */
3292
+ exchangePromise = null;
3293
+ /**
3294
+ * Wrap a request template with the sources of its two tokens.
3295
+ *
3296
+ * Any `subjectToken` or `actorToken` already on `request` is ignored; the
3297
+ * sources supply them. Omit `actor` only with a request that called
3298
+ * {@link DelegationRequest.impersonation} — otherwise every `getToken` fails
3299
+ * with `missing_actor`, which is the intended loud failure rather than a
3300
+ * silent downgrade.
3301
+ */
3302
+ constructor(request, subject, actor, fetchImpl = globalThis.fetch) {
3303
+ this.template = request;
3304
+ this.subject = subject;
3305
+ this.actor = actor;
3306
+ this.fetchImpl = fetchImpl;
3307
+ }
3308
+ /**
3309
+ * The current delegated bearer, exchanging first if there is none or it is at
3310
+ * or near expiry.
3311
+ */
3312
+ async getToken() {
3313
+ const live = this.liveToken();
3314
+ if (live !== void 0) return live;
3315
+ if (!this.exchangePromise) {
3316
+ this.exchangePromise = this.exchange().then((token2) => {
3317
+ this.cached = token2;
3318
+ return token2;
3319
+ }).finally(() => {
3320
+ this.exchangePromise = null;
3321
+ });
3322
+ }
3323
+ const token = await this.exchangePromise;
3324
+ return token.access_token;
3325
+ }
3326
+ /**
3327
+ * Force the next {@link getToken} to exchange again. Call on a 401.
3328
+ *
3329
+ * Only the delegated token is dropped. The subject and actor sources are left
3330
+ * alone: a provider-backed source tracks its own expiry, and a 401 from the
3331
+ * resource server says nothing about them.
3332
+ */
3333
+ invalidate() {
3334
+ this.cached = null;
3335
+ }
3336
+ /** A snapshot of the cached token, if any — for inspection or logging. */
3337
+ token() {
3338
+ return this.cached === null ? void 0 : { ...this.cached };
3339
+ }
3340
+ /** The request template, without tokens. */
3341
+ get request() {
3342
+ return this.template;
3343
+ }
3344
+ /** Never print tokens, and never the client secret the template carries. */
3345
+ toJSON() {
3346
+ return {
3347
+ tokenEndpoint: this.template.tokenEndpoint,
3348
+ clientId: this.template.clientId,
3349
+ subject: this.subject.toJSON(),
3350
+ ...this.actor ? { actor: this.actor.toJSON() } : {},
3351
+ hasToken: this.cached !== null
3352
+ };
3353
+ }
3354
+ [INSPECT_CUSTOM]() {
3355
+ return `DelegatedProvider ${JSON.stringify(this.toJSON())}`;
3356
+ }
3357
+ // ─── Private ───────────────────────────────────────────────────────────────
3358
+ liveToken() {
3359
+ if (this.cached === null) return void 0;
3360
+ if (isDelegatedTokenExpired(this.cached)) return void 0;
3361
+ return this.cached.access_token;
3362
+ }
3363
+ async exchange() {
3364
+ if (this.actor === void 0 && !this.template.isImpersonation) {
3365
+ throw new DelegationError(
3366
+ "missing_actor",
3367
+ "no actor_token \u2014 delegation requires one; call impersonation() to opt out explicitly"
3368
+ );
3369
+ }
3370
+ const request = this.template.clone().subjectToken(await this.subject.resolve(), this.subject.tokenType);
3371
+ if (this.actor !== void 0) {
3372
+ request.actorToken(await this.actor.resolve(), this.actor.tokenType);
3373
+ }
3374
+ return request.exchange(this.fetchImpl);
3375
+ }
3376
+ };
3377
+ function nowSecs2() {
3378
+ return Math.floor(Date.now() / 1e3);
3379
+ }
3380
+
3381
+ // src/loopback.ts
3382
+ var import_node_http = require("http");
3383
+ var LoopbackListener = class _LoopbackListener {
3384
+ server;
3385
+ boundPort;
3386
+ path;
3387
+ settled = false;
3388
+ constructor(server, port, path3) {
3389
+ this.server = server;
3390
+ this.boundPort = port;
3391
+ this.path = path3;
3392
+ }
3393
+ /**
3394
+ * Bind an OS-assigned port on `127.0.0.1`.
3395
+ *
3396
+ * Letting the OS choose avoids fighting whatever else owns a fixed port —
3397
+ * and because registration happens after binding, the real port is already
3398
+ * known by the time the redirect URI is registered.
3399
+ */
3400
+ static bind() {
3401
+ return _LoopbackListener.bindOn(0, "/callback");
3402
+ }
3403
+ /**
3404
+ * Bind a specific port and path.
3405
+ *
3406
+ * Use when the client was registered out of band against a fixed redirect
3407
+ * URI and the authorization server will accept no other.
3408
+ */
3409
+ static bindOn(port, path3) {
3410
+ const normalized = path3.startsWith("/") ? path3 : `/${path3}`;
3411
+ return new Promise((resolve, reject) => {
3412
+ const server = (0, import_node_http.createServer)();
3413
+ server.once("error", (err) => {
3414
+ reject(new AuthCodeError("http", `cannot bind loopback port: ${err}`));
3415
+ });
3416
+ server.listen(port, "127.0.0.1", () => {
3417
+ const address = server.address();
3418
+ if (!address) {
3419
+ server.close();
3420
+ reject(
3421
+ new AuthCodeError("http", "cannot bind loopback port: no address")
3422
+ );
3423
+ return;
3424
+ }
3425
+ resolve(new _LoopbackListener(server, address.port, normalized));
3426
+ });
3427
+ });
3428
+ }
3429
+ /**
3430
+ * Re-bind the exact port and path of a previously registered redirect URI.
3431
+ *
3432
+ * Needed whenever a saved registration is reused: the authorization server
3433
+ * matches the redirect URI exactly, so the listener has to come back on the
3434
+ * same port it registered.
3435
+ *
3436
+ * Rejects if that port is occupied. The right recovery is to {@link bind} a
3437
+ * fresh port and register a new client — not to retry, and not to authorize
3438
+ * against a URI the server will reject.
3439
+ */
3440
+ static bindFor(redirectUri) {
3441
+ let parsed;
3442
+ try {
3443
+ parsed = new URL(redirectUri);
3444
+ } catch (err) {
3445
+ return Promise.reject(
3446
+ new AuthCodeError("http", `bad redirect_uri ${redirectUri}: ${err}`)
3447
+ );
3448
+ }
3449
+ if (!parsed.port) {
3450
+ return Promise.reject(
3451
+ new AuthCodeError("http", `redirect_uri ${redirectUri} names no port`)
3452
+ );
3453
+ }
3454
+ return _LoopbackListener.bindOn(Number(parsed.port), parsed.pathname);
3455
+ }
3456
+ /** The port actually bound. */
3457
+ get port() {
3458
+ return this.boundPort;
3459
+ }
3460
+ /**
3461
+ * The redirect URI to register and to send in the authorize request.
3462
+ *
3463
+ * Uses `127.0.0.1` rather than `localhost`: RFC 8252 recommends the literal
3464
+ * address, and it sidesteps hosts where `localhost` resolves to IPv6 first
3465
+ * while the listener is bound to IPv4.
3466
+ */
3467
+ get redirectUri() {
3468
+ return `http://127.0.0.1:${this.boundPort}${this.path}`;
3469
+ }
3470
+ /** Stop listening. Safe to call more than once. */
3471
+ close() {
3472
+ this.server.close();
3473
+ }
3474
+ /**
3475
+ * Wait for the browser's redirect, up to `timeoutMs`.
3476
+ *
3477
+ * Serves a small page either way so the user sees an outcome rather than a
3478
+ * browser error, then stops listening. Requests to other paths are answered
3479
+ * 404 and ignored — browsers routinely ask for `/favicon.ico`, and treating
3480
+ * that as the redirect would abort the flow.
3481
+ */
3482
+ wait(timeoutMs) {
3483
+ return new Promise((resolve, reject) => {
3484
+ const finish = (fn) => {
3485
+ if (this.settled) return;
3486
+ this.settled = true;
3487
+ clearTimeout(timer);
3488
+ this.server.removeAllListeners("request");
3489
+ this.close();
3490
+ fn();
3491
+ };
3492
+ const timer = setTimeout(() => {
3493
+ finish(
3494
+ () => reject(
3495
+ new AuthCodeError(
3496
+ "http",
3497
+ `timed out after ${Math.round(
3498
+ timeoutMs / 1e3
3499
+ )}s waiting for the authorization redirect`
3500
+ )
3501
+ )
3502
+ );
3503
+ }, timeoutMs);
3504
+ timer.unref?.();
3505
+ this.server.on("request", (req, res) => {
3506
+ const target = new URL(req.url ?? "/", "http://127.0.0.1");
3507
+ if (target.pathname !== this.path) {
3508
+ respond(res, 404, "Not found");
3509
+ return;
3510
+ }
3511
+ const error = target.searchParams.get("error");
3512
+ if (error) {
3513
+ respond(
3514
+ res,
3515
+ 200,
3516
+ "Authorization was denied. You can close this window."
3517
+ );
3518
+ const description = target.searchParams.get("error_description");
3519
+ finish(
3520
+ () => reject(
3521
+ new AuthCodeError(
3522
+ "denied",
3523
+ `authorization denied: ${error}${description ? ` \u2014 ${description}` : ""}`
3524
+ )
3525
+ )
3526
+ );
3527
+ return;
3528
+ }
3529
+ const code = target.searchParams.get("code");
3530
+ const state = target.searchParams.get("state");
3531
+ if (code !== null && state !== null) {
3532
+ respond(
3533
+ res,
3534
+ 200,
3535
+ "Signed in. You can close this window and return to the app."
3536
+ );
3537
+ finish(() => resolve({ code, state }));
3538
+ return;
3539
+ }
3540
+ respond(res, 400, "Missing code or state.");
3541
+ finish(
3542
+ () => reject(
3543
+ new AuthCodeError(
3544
+ "discovery",
3545
+ "redirect carried neither an error nor a code/state pair"
3546
+ )
3547
+ )
3548
+ );
3549
+ });
3550
+ });
3551
+ }
3552
+ };
3553
+ function respond(res, status, message) {
3554
+ const body = `<!DOCTYPE html><html><head><meta charset="utf-8"><title>DataGrout</title><style>body{font:15px/1.5 system-ui,sans-serif;margin:16vh auto;max-width:26rem;text-align:center;color-scheme:light dark}</style></head><body><p>${message}</p></body></html>`;
3555
+ res.writeHead(status, {
3556
+ "content-type": "text/html; charset=utf-8",
3557
+ "content-length": Buffer.byteLength(body),
3558
+ connection: "close"
3559
+ });
3560
+ res.end(body);
3561
+ }
3562
+
2340
3563
  // src/types.ts
2341
3564
  function extractMeta(result) {
2342
3565
  const rich = result?._meta?.datagrout;
@@ -2405,36 +3628,57 @@ function buildToolMeta(raw) {
2405
3628
 
2406
3629
  // src/index.ts
2407
3630
  init_onramp();
2408
- var version = "0.5.0";
2409
3631
  // Annotate the CommonJS export names for ESM import in node:
2410
3632
  0 && (module.exports = {
3633
+ AuthCodeError,
3634
+ AuthCodeFlow,
3635
+ AuthCodeProvider,
2411
3636
  AuthError,
2412
3637
  Client,
2413
3638
  ConduitError,
2414
3639
  ConduitIdentity,
2415
3640
  DEFAULT_IDENTITY_DIR,
3641
+ DEFAULT_SCOPE,
3642
+ DELEGATION_GRANT_TYPE,
3643
+ DELEGATION_SERVER_ERROR_CODES,
2416
3644
  DG_CA_URL,
2417
3645
  DG_SUBSTRATE_ENDPOINT,
3646
+ DelegatedProvider,
3647
+ DelegationError,
3648
+ DelegationRequest,
2418
3649
  GuidedSession,
2419
3650
  InvalidConfigError,
3651
+ LoopbackListener,
2420
3652
  NetworkError,
2421
3653
  NotInitializedError,
2422
3654
  OAuthTokenProvider,
2423
3655
  RateLimitError,
2424
3656
  ServerError,
3657
+ TOKEN_TYPES,
3658
+ TokenSource,
2425
3659
  WS_SUBPROTOCOL,
2426
3660
  WsTransport,
3661
+ authCodeProviderFrom,
3662
+ challengeS256,
2427
3663
  deriveTokenEndpoint,
2428
3664
  extractMeta,
2429
3665
  fetchDgCaCert,
2430
3666
  fetchWithIdentity,
2431
3667
  generateKeypair,
3668
+ generateVerifier,
3669
+ isDelegatedTokenExpired,
3670
+ isDgNativeTool,
2432
3671
  isDgUrl,
3672
+ isGrantExpired,
3673
+ isGrantRefreshable,
2433
3674
  refreshCaCert,
3675
+ refreshGrant,
2434
3676
  registerAndExchange,
2435
3677
  registerIdentity,
2436
3678
  registerOnly,
2437
3679
  rotateIdentity,
2438
3680
  saveIdentity,
3681
+ supportsS256,
3682
+ tokenTypeName,
2439
3683
  version
2440
3684
  });