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