@datagrout/conduit 0.7.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/dist/index.mjs CHANGED
@@ -255,12 +255,504 @@ var init_identity = __esm({
255
255
  // src/client.ts
256
256
  import * as path2 from "path";
257
257
 
258
+ // src/version.ts
259
+ var version = "0.8.0";
260
+
258
261
  // src/transports/base.ts
259
262
  var Transport = class {
260
263
  };
261
264
 
262
265
  // src/transports/mcp.ts
263
266
  init_oauth();
267
+
268
+ // src/authcode.ts
269
+ import { createHash, randomBytes, timingSafeEqual } from "crypto";
270
+
271
+ // src/errors.ts
272
+ var ConduitError = class extends Error {
273
+ constructor(message) {
274
+ super(message);
275
+ this.name = this.constructor.name;
276
+ Object.setPrototypeOf(this, new.target.prototype);
277
+ }
278
+ };
279
+ var NotInitializedError = class extends ConduitError {
280
+ constructor() {
281
+ super("Client not initialized. Call connect() first.");
282
+ }
283
+ };
284
+ var RateLimitError = class extends ConduitError {
285
+ status;
286
+ retryAfter;
287
+ constructor(status, retryAfter) {
288
+ const limitStr = status.limit === "unlimited" ? "unlimited" : `${status.limit.perHour}/hour`;
289
+ super(`Rate limit exceeded (${status.used} / ${limitStr} calls this hour)`);
290
+ this.status = status;
291
+ this.retryAfter = retryAfter;
292
+ }
293
+ };
294
+ var AuthError = class extends ConduitError {
295
+ constructor(message = "Authentication failed") {
296
+ super(message);
297
+ }
298
+ };
299
+ var NetworkError = class extends ConduitError {
300
+ constructor(message) {
301
+ super(message);
302
+ }
303
+ };
304
+ var ServerError = class extends ConduitError {
305
+ code;
306
+ serverMessage;
307
+ constructor(code, serverMessage) {
308
+ super(`Server error ${code}: ${serverMessage}`);
309
+ this.code = code;
310
+ this.serverMessage = serverMessage;
311
+ }
312
+ };
313
+ var InvalidConfigError = class extends ConduitError {
314
+ constructor(message) {
315
+ super(message);
316
+ }
317
+ };
318
+
319
+ // src/authcode.ts
320
+ var DEFAULT_SCOPE = "mcp tools";
321
+ var REFRESH_SKEW_SECS = 60;
322
+ var AuthCodeError = class extends ConduitError {
323
+ kind;
324
+ /** HTTP status, for `registration_rejected` and `token_exchange`. */
325
+ status;
326
+ /** Response body, for `registration_rejected` and `token_exchange`. */
327
+ body;
328
+ constructor(kind, message, extra) {
329
+ super(message);
330
+ this.kind = kind;
331
+ this.status = extra?.status;
332
+ this.body = extra?.body;
333
+ }
334
+ };
335
+ function supportsS256(metadata) {
336
+ const methods = metadata.code_challenge_methods_supported;
337
+ if (!methods || methods.length === 0) return true;
338
+ return methods.some((m) => m.toUpperCase() === "S256");
339
+ }
340
+ function isGrantExpired(grant) {
341
+ if (grant.expires_at === void 0) return false;
342
+ return nowSecs() + REFRESH_SKEW_SECS >= grant.expires_at;
343
+ }
344
+ function isGrantRefreshable(grant) {
345
+ return grant.refresh_token !== void 0 && grant.refresh_token !== "";
346
+ }
347
+ async function refreshGrant(grant, fetchImpl = globalThis.fetch) {
348
+ if (!isGrantRefreshable(grant)) {
349
+ throw new AuthCodeError(
350
+ "not_refreshable",
351
+ "grant has expired and carries no refresh_token \u2014 re-authorize"
352
+ );
353
+ }
354
+ const form = {
355
+ grant_type: "refresh_token",
356
+ refresh_token: grant.refresh_token,
357
+ client_id: grant.client_id
358
+ };
359
+ if (grant.resource) form.resource = grant.resource;
360
+ const token = await postForm(fetchImpl, grant.token_endpoint, form);
361
+ return {
362
+ access_token: token.access_token,
363
+ // A server that does not rotate returns no new refresh token; keep the
364
+ // existing one rather than silently making the grant unrefreshable.
365
+ refresh_token: token.refresh_token ?? grant.refresh_token,
366
+ expires_at: token.expires_in === void 0 ? void 0 : nowSecs() + token.expires_in,
367
+ client_id: grant.client_id,
368
+ token_endpoint: grant.token_endpoint,
369
+ scope: token.scope ?? grant.scope,
370
+ resource: grant.resource
371
+ };
372
+ }
373
+ var AuthCodeFlow = class _AuthCodeFlow {
374
+ fetchImpl;
375
+ metadataDoc;
376
+ /** The protected resource this grant will be bound to (RFC 8707). */
377
+ resource;
378
+ clientIdValue;
379
+ redirectUriValue;
380
+ scope = DEFAULT_SCOPE;
381
+ constructor(fetchImpl, metadata, resource) {
382
+ this.fetchImpl = fetchImpl;
383
+ this.metadataDoc = metadata;
384
+ this.resource = resource;
385
+ }
386
+ /**
387
+ * Discover the authorization server protecting `resourceUrl`.
388
+ *
389
+ * `resourceUrl` is the MCP endpoint being connected to — for DataGrout,
390
+ * `https://gateway.datagrout.ai/connect` or a `.../servers/{uuid}/mcp` URL.
391
+ *
392
+ * Tries RFC 9728 protected-resource metadata first, then RFC 8414
393
+ * authorization-server metadata on whatever that names. Falls back to the
394
+ * resource's own origin, which is where DataGrout serves it.
395
+ */
396
+ static async discover(resourceUrl, fetchImpl = globalThis.fetch) {
397
+ const resource = resourceUrl.replace(/\/+$/, "");
398
+ const prm = await fetchResourceMetadata(fetchImpl, resource);
399
+ let issuer;
400
+ if (prm?.authorization_servers && prm.authorization_servers.length > 0) {
401
+ issuer = prm.authorization_servers[0];
402
+ } else {
403
+ issuer = originOf(resource);
404
+ }
405
+ if (!issuer) {
406
+ throw new AuthCodeError("discovery", `not a URL: ${resource}`);
407
+ }
408
+ const metadata = await fetchAsMetadata(fetchImpl, issuer);
409
+ if (!supportsS256(metadata)) {
410
+ throw new AuthCodeError(
411
+ "pkce_unsupported",
412
+ "authorization server does not support PKCE S256; refusing to downgrade"
413
+ );
414
+ }
415
+ return new _AuthCodeFlow(fetchImpl, metadata, resource);
416
+ }
417
+ /** Use a client id registered out of band, skipping dynamic registration. */
418
+ withClientId(clientId, redirectUri) {
419
+ this.clientIdValue = clientId;
420
+ this.redirectUriValue = redirectUri;
421
+ return this;
422
+ }
423
+ /**
424
+ * Reuse a client registered on a previous run.
425
+ *
426
+ * Prefer this over {@link withClientId}: it carries the redirect URI with the
427
+ * id, which is not optional bookkeeping — an authorization server matches the
428
+ * redirect URI **exactly** against what was registered, so a client id reused
429
+ * with a different URI is rejected.
430
+ */
431
+ withRegisteredClient(client) {
432
+ return this.withClientId(client.client_id, client.redirect_uri);
433
+ }
434
+ /** Request scopes other than {@link DEFAULT_SCOPE}. */
435
+ withScope(scope) {
436
+ this.scope = scope;
437
+ return this;
438
+ }
439
+ /** The discovered metadata. */
440
+ get metadata() {
441
+ return this.metadataDoc;
442
+ }
443
+ /** The client id, once registered or supplied. */
444
+ get clientId() {
445
+ return this.clientIdValue;
446
+ }
447
+ /** The redirect URI this flow is bound to. */
448
+ get redirectUri() {
449
+ return this.redirectUriValue;
450
+ }
451
+ /**
452
+ * Register this application via RFC 7591 dynamic client registration.
453
+ *
454
+ * Registers a **public client** — `token_endpoint_auth_method: "none"`, no
455
+ * secret issued. A desktop or CLI application cannot keep a secret, and PKCE
456
+ * is what stands in for one.
457
+ *
458
+ * Returns the id **and** the redirect URI it is bound to. Persist the pair
459
+ * and restore it with {@link withRegisteredClient} — re-registering on every
460
+ * launch creates a new client record each time, and reusing an id against a
461
+ * different redirect URI is rejected.
462
+ */
463
+ async register(clientName, redirectUri) {
464
+ const endpoint = this.metadataDoc.registration_endpoint;
465
+ if (!endpoint) {
466
+ throw new AuthCodeError(
467
+ "no_registration_endpoint",
468
+ "authorization server has no registration endpoint \u2014 register a client manually and use withClientId()"
469
+ );
470
+ }
471
+ let response;
472
+ try {
473
+ response = await this.fetchImpl(endpoint, {
474
+ method: "POST",
475
+ headers: { "Content-Type": "application/json" },
476
+ body: JSON.stringify({
477
+ client_name: clientName,
478
+ redirect_uris: [redirectUri],
479
+ grant_types: ["authorization_code", "refresh_token"],
480
+ response_types: ["code"],
481
+ token_endpoint_auth_method: "none",
482
+ application_type: "native"
483
+ })
484
+ });
485
+ } catch (err) {
486
+ throw new AuthCodeError("http", `HTTP error: ${err}`);
487
+ }
488
+ if (!response.ok) {
489
+ const body = await response.text().catch(() => "");
490
+ throw new AuthCodeError(
491
+ "registration_rejected",
492
+ `client registration rejected (HTTP ${response.status}): ${body}`,
493
+ { status: response.status, body }
494
+ );
495
+ }
496
+ let clientId;
497
+ try {
498
+ const parsed = await response.json();
499
+ clientId = parsed.client_id;
500
+ } catch (err) {
501
+ throw new AuthCodeError("http", `bad registration response: ${err}`);
502
+ }
503
+ this.clientIdValue = clientId;
504
+ this.redirectUriValue = redirectUri;
505
+ return { client_id: clientId, redirect_uri: redirectUri };
506
+ }
507
+ /**
508
+ * Build the consent URL, plus the {@link PendingAuthorization} needed to
509
+ * redeem the resulting code.
510
+ *
511
+ * The caller opens the URL however suits it — a browser, a printed
512
+ * instruction, a QR code. This SDK does not launch browsers.
513
+ */
514
+ authorizeUrl() {
515
+ const clientId = this.clientIdValue;
516
+ const redirectUri = this.redirectUriValue;
517
+ if (!clientId || !redirectUri) {
518
+ throw new AuthCodeError(
519
+ "no_client_id",
520
+ "no client_id \u2014 call register() or withClientId() first"
521
+ );
522
+ }
523
+ const codeVerifier = generateVerifier();
524
+ const state = generateState();
525
+ const query = [
526
+ ["response_type", "code"],
527
+ ["client_id", clientId],
528
+ ["redirect_uri", redirectUri],
529
+ ["scope", this.scope],
530
+ ["state", state],
531
+ ["code_challenge", challengeS256(codeVerifier)],
532
+ ["code_challenge_method", "S256"],
533
+ // RFC 8707: bind the token to this resource so it cannot be replayed
534
+ // against a different one.
535
+ ["resource", this.resource]
536
+ ].map(([k, v]) => `${k}=${urlencode(v)}`).join("&");
537
+ const separator = this.metadataDoc.authorization_endpoint.includes("?") ? "&" : "?";
538
+ const url = `${this.metadataDoc.authorization_endpoint}${separator}${query}`;
539
+ return { url, pending: { codeVerifier, state, redirectUri } };
540
+ }
541
+ /**
542
+ * Redeem an authorization code for a {@link Grant}.
543
+ *
544
+ * `returnedState` is the `state` parameter from the redirect. It is checked
545
+ * against the pending request before anything is sent: a mismatch means the
546
+ * response belongs to a different authorization request, and the exchange is
547
+ * refused rather than attempted.
548
+ */
549
+ async exchange(pending, code, returnedState) {
550
+ if (!constantTimeEqual(pending.state, returnedState)) {
551
+ throw new AuthCodeError(
552
+ "state_mismatch",
553
+ "state mismatch \u2014 the authorization response does not match this request"
554
+ );
555
+ }
556
+ const clientId = this.clientIdValue;
557
+ if (!clientId) {
558
+ throw new AuthCodeError(
559
+ "no_client_id",
560
+ "no client_id \u2014 call register() or withClientId() first"
561
+ );
562
+ }
563
+ const token = await postForm(
564
+ this.fetchImpl,
565
+ this.metadataDoc.token_endpoint,
566
+ {
567
+ grant_type: "authorization_code",
568
+ code,
569
+ redirect_uri: pending.redirectUri,
570
+ client_id: clientId,
571
+ code_verifier: pending.codeVerifier,
572
+ resource: this.resource
573
+ }
574
+ );
575
+ return {
576
+ access_token: token.access_token,
577
+ refresh_token: token.refresh_token,
578
+ expires_at: token.expires_in === void 0 ? void 0 : nowSecs() + token.expires_in,
579
+ client_id: clientId,
580
+ token_endpoint: this.metadataDoc.token_endpoint,
581
+ scope: token.scope,
582
+ resource: this.resource
583
+ };
584
+ }
585
+ };
586
+ var AuthCodeProvider = class {
587
+ grantValue;
588
+ dirty = false;
589
+ refreshPromise = null;
590
+ fetchImpl;
591
+ constructor(grant, fetchImpl = globalThis.fetch) {
592
+ this.grantValue = grant;
593
+ this.fetchImpl = fetchImpl;
594
+ }
595
+ /** The current access token, refreshing first if it is at or near expiry. */
596
+ async getToken() {
597
+ if (!isGrantExpired(this.grantValue)) {
598
+ return this.grantValue.access_token;
599
+ }
600
+ if (!this.refreshPromise) {
601
+ this.refreshPromise = refreshGrant(this.grantValue, this.fetchImpl).then((refreshed2) => {
602
+ this.grantValue = refreshed2;
603
+ this.dirty = true;
604
+ return refreshed2;
605
+ }).finally(() => {
606
+ this.refreshPromise = null;
607
+ });
608
+ }
609
+ const refreshed = await this.refreshPromise;
610
+ return refreshed.access_token;
611
+ }
612
+ /** A snapshot of the current grant, for persisting. */
613
+ grant() {
614
+ return { ...this.grantValue };
615
+ }
616
+ /** Whether the grant changed since the last {@link takeIfDirty}. */
617
+ isDirty() {
618
+ return this.dirty;
619
+ }
620
+ /**
621
+ * Return the grant if it has changed since the last call, clearing the flag.
622
+ *
623
+ * The intended use is a persistence loop: call periodically and write
624
+ * whatever comes back, so a rotated refresh token is never lost.
625
+ */
626
+ takeIfDirty() {
627
+ if (!this.dirty) return null;
628
+ this.dirty = false;
629
+ return this.grant();
630
+ }
631
+ /** Force the next {@link getToken} to refresh. Call on a 401. */
632
+ invalidate() {
633
+ this.grantValue = { ...this.grantValue, expires_at: 0 };
634
+ }
635
+ };
636
+ function authCodeProviderFrom(value, fetchImpl = globalThis.fetch) {
637
+ if (value === void 0) return void 0;
638
+ if (value instanceof AuthCodeProvider) return value;
639
+ return new AuthCodeProvider(value, fetchImpl);
640
+ }
641
+ function generateVerifier() {
642
+ return randomBytes(32).toString("base64url");
643
+ }
644
+ function challengeS256(verifier) {
645
+ return createHash("sha256").update(verifier, "utf8").digest("base64url");
646
+ }
647
+ function generateState() {
648
+ return randomBytes(16).toString("base64url");
649
+ }
650
+ function nowSecs() {
651
+ return Math.floor(Date.now() / 1e3);
652
+ }
653
+ function constantTimeEqual(a, b) {
654
+ const ab = Buffer.from(a, "utf8");
655
+ const bb = Buffer.from(b, "utf8");
656
+ if (ab.length !== bb.length) return false;
657
+ if (ab.length === 0) return true;
658
+ return timingSafeEqual(ab, bb);
659
+ }
660
+ function urlencode(value) {
661
+ let out = "";
662
+ for (const byte of Buffer.from(value, "utf8")) {
663
+ const ch = String.fromCharCode(byte);
664
+ if (/[A-Za-z0-9\-._~]/.test(ch)) {
665
+ out += ch;
666
+ } else {
667
+ out += `%${byte.toString(16).toUpperCase().padStart(2, "0")}`;
668
+ }
669
+ }
670
+ return out;
671
+ }
672
+ function originOf(url) {
673
+ try {
674
+ const parsed = new URL(url);
675
+ if (!parsed.hostname) return void 0;
676
+ return parsed.port ? `${parsed.protocol}//${parsed.hostname}:${parsed.port}` : `${parsed.protocol}//${parsed.hostname}`;
677
+ } catch {
678
+ return void 0;
679
+ }
680
+ }
681
+ async function fetchResourceMetadata(fetchImpl, resource) {
682
+ const origin = originOf(resource);
683
+ const candidates = [
684
+ `${resource}/.well-known/oauth-protected-resource`,
685
+ origin ? `${origin}/.well-known/oauth-protected-resource` : void 0
686
+ ].filter((u) => u !== void 0);
687
+ for (const url of candidates) {
688
+ try {
689
+ const resp = await fetchImpl(url);
690
+ if (resp.ok) {
691
+ return await resp.json();
692
+ }
693
+ } catch {
694
+ }
695
+ }
696
+ return void 0;
697
+ }
698
+ async function fetchAsMetadata(fetchImpl, issuer) {
699
+ const base = issuer.replace(/\/+$/, "");
700
+ const candidates = [
701
+ `${base}/.well-known/oauth-authorization-server`,
702
+ `${base}/.well-known/openid-configuration`
703
+ ];
704
+ let last = "";
705
+ for (const url of candidates) {
706
+ try {
707
+ const resp = await fetchImpl(url);
708
+ if (resp.ok) {
709
+ try {
710
+ return await resp.json();
711
+ } catch (err) {
712
+ throw new AuthCodeError(
713
+ "discovery",
714
+ `OAuth discovery failed: bad metadata at ${url}: ${err}`
715
+ );
716
+ }
717
+ }
718
+ last = `${url} \u2192 HTTP ${resp.status}`;
719
+ } catch (err) {
720
+ if (err instanceof AuthCodeError) throw err;
721
+ last = `${url} \u2192 ${err}`;
722
+ }
723
+ }
724
+ throw new AuthCodeError(
725
+ "discovery",
726
+ `OAuth discovery failed: no authorization server metadata found (last attempt: ${last})`
727
+ );
728
+ }
729
+ async function postForm(fetchImpl, endpoint, form) {
730
+ let response;
731
+ try {
732
+ response = await fetchImpl(endpoint, {
733
+ method: "POST",
734
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
735
+ body: new URLSearchParams(form).toString()
736
+ });
737
+ } catch (err) {
738
+ throw new AuthCodeError("http", `HTTP error: ${err}`);
739
+ }
740
+ if (!response.ok) {
741
+ const body = await response.text().catch(() => "");
742
+ throw new AuthCodeError(
743
+ "token_exchange",
744
+ `token exchange failed (HTTP ${response.status}): ${body}`,
745
+ { status: response.status, body }
746
+ );
747
+ }
748
+ try {
749
+ return await response.json();
750
+ } catch (err) {
751
+ throw new AuthCodeError("http", `bad token response: ${err}`);
752
+ }
753
+ }
754
+
755
+ // src/transports/mcp.ts
264
756
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
265
757
  import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
266
758
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
@@ -271,6 +763,10 @@ var MCPTransport = class extends Transport {
271
763
  client;
272
764
  clientTransport;
273
765
  oauthProvider;
766
+ /** Present only when `auth.authorizationCode` is set. */
767
+ authCodeProvider;
768
+ /** Present only when `auth.delegation` is set (RFC 8693). */
769
+ delegatedProvider;
274
770
  constructor(url, auth, identity) {
275
771
  super();
276
772
  this.url = url;
@@ -286,12 +782,20 @@ var MCPTransport = class extends Transport {
286
782
  scope: cc.scope
287
783
  });
288
784
  }
785
+ this.authCodeProvider = authCodeProviderFrom(auth?.authorizationCode);
786
+ this.delegatedProvider = auth?.delegation;
289
787
  }
290
788
  async buildHeaders() {
291
789
  const headers = {};
292
- if (this.oauthProvider) {
790
+ if (this.delegatedProvider) {
791
+ const token = await this.delegatedProvider.getToken();
792
+ headers["Authorization"] = `Bearer ${token}`;
793
+ } else if (this.oauthProvider) {
293
794
  const token = await this.oauthProvider.getToken();
294
795
  headers["Authorization"] = `Bearer ${token}`;
796
+ } else if (this.authCodeProvider) {
797
+ const token = await this.authCodeProvider.getToken();
798
+ headers["Authorization"] = `Bearer ${token}`;
295
799
  } else if (this.auth?.bearer) {
296
800
  headers["Authorization"] = `Bearer ${this.auth.bearer}`;
297
801
  } else if (this.auth?.basic) {
@@ -321,7 +825,10 @@ var MCPTransport = class extends Transport {
321
825
  if (this.identity) {
322
826
  const id = this.identity;
323
827
  transportOpts.fetcher = (url, init) => {
324
- const { fetchWithIdentity: fetchWithIdentity2 } = (init_identity(), __toCommonJS(identity_exports));
828
+ const { fetchWithIdentity: fetchWithIdentity2 } = (
829
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
830
+ (init_identity(), __toCommonJS(identity_exports))
831
+ );
325
832
  return fetchWithIdentity2(
326
833
  typeof url === "string" ? url : url instanceof URL ? url.toString() : url.url,
327
834
  init ?? {},
@@ -337,7 +844,7 @@ var MCPTransport = class extends Transport {
337
844
  throw new Error(`Unsupported MCP URL scheme: ${this.url}`);
338
845
  }
339
846
  this.client = new Client(
340
- { name: "datagrout-conduit", version: "0.1.0" },
847
+ { name: "datagrout-conduit", version },
341
848
  { capabilities: {} }
342
849
  );
343
850
  await this.client.connect(this.clientTransport);
@@ -359,7 +866,11 @@ var MCPTransport = class extends Transport {
359
866
  annotations: tool.annotations
360
867
  }));
361
868
  }
362
- async callTool(name, args, options) {
869
+ // `_options` on this and the four methods below: accepted for signature
870
+ // parity with the other conduit SDKs, not yet consulted here. The leading
871
+ // underscore is the codebase's marker for a deliberately unused parameter,
872
+ // and what tsconfig's `noUnusedParameters` exempts.
873
+ async callTool(name, args, _options) {
363
874
  if (!this.client) {
364
875
  throw new Error("Not connected. Call connect() first.");
365
876
  }
@@ -381,7 +892,7 @@ var MCPTransport = class extends Transport {
381
892
  }
382
893
  return result;
383
894
  }
384
- async listResources(options) {
895
+ async listResources(_options) {
385
896
  if (!this.client) {
386
897
  throw new Error("Not connected. Call connect() first.");
387
898
  }
@@ -393,14 +904,14 @@ var MCPTransport = class extends Transport {
393
904
  mimeType: resource.mimeType
394
905
  }));
395
906
  }
396
- async readResource(uri, options) {
907
+ async readResource(uri, _options) {
397
908
  if (!this.client) {
398
909
  throw new Error("Not connected. Call connect() first.");
399
910
  }
400
911
  const result = await this.client.readResource({ uri });
401
912
  return result.contents;
402
913
  }
403
- async listPrompts(options) {
914
+ async listPrompts(_options) {
404
915
  if (!this.client) {
405
916
  throw new Error("Not connected. Call connect() first.");
406
917
  }
@@ -411,7 +922,7 @@ var MCPTransport = class extends Transport {
411
922
  arguments: prompt.arguments
412
923
  }));
413
924
  }
414
- async getPrompt(name, args, options) {
925
+ async getPrompt(name, args, _options) {
415
926
  if (!this.client) {
416
927
  throw new Error("Not connected. Call connect() first.");
417
928
  }
@@ -423,56 +934,6 @@ var MCPTransport = class extends Transport {
423
934
  // src/transports/jsonrpc.ts
424
935
  init_identity();
425
936
  init_oauth();
426
-
427
- // src/errors.ts
428
- var ConduitError = class extends Error {
429
- constructor(message) {
430
- super(message);
431
- this.name = this.constructor.name;
432
- Object.setPrototypeOf(this, new.target.prototype);
433
- }
434
- };
435
- var NotInitializedError = class extends ConduitError {
436
- constructor() {
437
- super("Client not initialized. Call connect() first.");
438
- }
439
- };
440
- var RateLimitError = class extends ConduitError {
441
- status;
442
- retryAfter;
443
- constructor(status, retryAfter) {
444
- const limitStr = status.limit === "unlimited" ? "unlimited" : `${status.limit.perHour}/hour`;
445
- super(`Rate limit exceeded (${status.used} / ${limitStr} calls this hour)`);
446
- this.status = status;
447
- this.retryAfter = retryAfter;
448
- }
449
- };
450
- var AuthError = class extends ConduitError {
451
- constructor(message = "Authentication failed") {
452
- super(message);
453
- }
454
- };
455
- var NetworkError = class extends ConduitError {
456
- constructor(message) {
457
- super(message);
458
- }
459
- };
460
- var ServerError = class extends ConduitError {
461
- code;
462
- serverMessage;
463
- constructor(code, serverMessage) {
464
- super(`Server error ${code}: ${serverMessage}`);
465
- this.code = code;
466
- this.serverMessage = serverMessage;
467
- }
468
- };
469
- var InvalidConfigError = class extends ConduitError {
470
- constructor(message) {
471
- super(message);
472
- }
473
- };
474
-
475
- // src/transports/jsonrpc.ts
476
937
  function unwrapContent(result) {
477
938
  if (!result) return result;
478
939
  if (result.structuredContent !== void 0) {
@@ -507,6 +968,10 @@ var JSONRPCTransport = class extends Transport {
507
968
  requestId = 0;
508
969
  /** Resolved token provider, present only when `auth.clientCredentials` is set. */
509
970
  oauthProvider;
971
+ /** Present only when `auth.authorizationCode` is set. */
972
+ authCodeProvider;
973
+ /** Present only when `auth.delegation` is set (RFC 8693). */
974
+ delegatedProvider;
510
975
  constructor(url, auth, timeout = 3e4, identity) {
511
976
  super();
512
977
  this.url = url;
@@ -521,7 +986,10 @@ var JSONRPCTransport = class extends Transport {
521
986
  if (auth?.clientCredentials) {
522
987
  const cc = auth.clientCredentials;
523
988
  const tokenEndpoint = cc.tokenEndpoint ?? (() => {
524
- const { deriveTokenEndpoint: deriveTokenEndpoint2 } = (init_oauth(), __toCommonJS(oauth_exports));
989
+ const { deriveTokenEndpoint: deriveTokenEndpoint2 } = (
990
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
991
+ (init_oauth(), __toCommonJS(oauth_exports))
992
+ );
525
993
  return deriveTokenEndpoint2(url);
526
994
  })();
527
995
  this.oauthProvider = new OAuthTokenProvider({
@@ -531,6 +999,8 @@ var JSONRPCTransport = class extends Transport {
531
999
  scope: cc.scope
532
1000
  });
533
1001
  }
1002
+ this.authCodeProvider = authCodeProviderFrom(auth?.authorizationCode);
1003
+ this.delegatedProvider = auth?.delegation;
534
1004
  }
535
1005
  async connect() {
536
1006
  }
@@ -543,9 +1013,15 @@ var JSONRPCTransport = class extends Transport {
543
1013
  const headers = {
544
1014
  "Content-Type": "application/json"
545
1015
  };
546
- if (this.oauthProvider) {
1016
+ if (this.delegatedProvider) {
1017
+ const token = await this.delegatedProvider.getToken();
1018
+ headers["Authorization"] = `Bearer ${token}`;
1019
+ } else if (this.oauthProvider) {
547
1020
  const token = await this.oauthProvider.getToken();
548
1021
  headers["Authorization"] = `Bearer ${token}`;
1022
+ } else if (this.authCodeProvider) {
1023
+ const token = await this.authCodeProvider.getToken();
1024
+ headers["Authorization"] = `Bearer ${token}`;
549
1025
  } else if (this.auth?.bearer) {
550
1026
  headers["Authorization"] = `Bearer ${this.auth.bearer}`;
551
1027
  } else if (this.auth?.basic) {
@@ -575,9 +1051,19 @@ var JSONRPCTransport = class extends Transport {
575
1051
  if (response.status === 429) {
576
1052
  throw parseRateLimitError(response);
577
1053
  }
578
- if (response.status === 401 && this.oauthProvider && !isRetry) {
579
- this.oauthProvider.invalidate();
580
- return this._callWithRetry(method, params, true);
1054
+ if (response.status === 401 && !isRetry) {
1055
+ if (this.delegatedProvider) {
1056
+ this.delegatedProvider.invalidate();
1057
+ return this._callWithRetry(method, params, true);
1058
+ }
1059
+ if (this.oauthProvider) {
1060
+ this.oauthProvider.invalidate();
1061
+ return this._callWithRetry(method, params, true);
1062
+ }
1063
+ if (this.authCodeProvider) {
1064
+ this.authCodeProvider.invalidate();
1065
+ return this._callWithRetry(method, params, true);
1066
+ }
581
1067
  }
582
1068
  if (!response.ok) {
583
1069
  throw new Error(`HTTP ${response.status}: ${response.statusText}`);
@@ -619,6 +1105,7 @@ var JSONRPCTransport = class extends Transport {
619
1105
  };
620
1106
 
621
1107
  // src/transports/ws.ts
1108
+ init_oauth();
622
1109
  var SUBPROTOCOL = "datagrout-jsonrpc.v1";
623
1110
  var SUBSCRIPTION_BUFFER = 256;
624
1111
  var PING_INTERVAL_MS = 25e3;
@@ -682,6 +1169,23 @@ var Subscription = class {
682
1169
  var WsTransport = class extends Transport {
683
1170
  _url;
684
1171
  _auth;
1172
+ /**
1173
+ * mTLS identity presented on the `wss://` handshake, if any. The HTTP
1174
+ * transports route through {@link fetchWithIdentity}; here the PEMs go to the
1175
+ * `ws` client as `cert` / `key` / `ca` options, which it forwards to
1176
+ * `tls.connect`. Mirrors `build_connector` in the Rust reference.
1177
+ */
1178
+ _identity;
1179
+ /**
1180
+ * Resolved OAuth providers, built once so a token survives reconnects.
1181
+ *
1182
+ * All are consulted in {@link _resolveBearer} before the upgrade request is
1183
+ * built — see the note there on why that has to happen up front.
1184
+ */
1185
+ _oauthProvider;
1186
+ _authCodeProvider;
1187
+ /** RFC 8693 delegation, when `auth.delegation` is set. */
1188
+ _delegatedProvider;
685
1189
  _ws = null;
686
1190
  _nextId = 0;
687
1191
  _pending = /* @__PURE__ */ new Map();
@@ -698,7 +1202,7 @@ var WsTransport = class extends Transport {
698
1202
  * defaults to {@link PING_INTERVAL_MS}.
699
1203
  */
700
1204
  _pingIntervalMs = PING_INTERVAL_MS;
701
- constructor(url, auth, _timeout, _identity) {
1205
+ constructor(url, auth, _timeout, identity) {
702
1206
  super();
703
1207
  const scheme = new URL(url).protocol.replace(":", "");
704
1208
  if (scheme !== "ws" && scheme !== "wss") {
@@ -708,14 +1212,46 @@ var WsTransport = class extends Transport {
708
1212
  }
709
1213
  this._url = url;
710
1214
  this._auth = auth;
1215
+ this._identity = identity;
1216
+ if (auth?.clientCredentials) {
1217
+ const cc = auth.clientCredentials;
1218
+ const tokenEndpoint = cc.tokenEndpoint ?? deriveTokenEndpoint(url.replace(/^ws/, "http"));
1219
+ this._oauthProvider = new OAuthTokenProvider({
1220
+ clientId: cc.clientId,
1221
+ clientSecret: cc.clientSecret,
1222
+ tokenEndpoint,
1223
+ scope: cc.scope
1224
+ });
1225
+ }
1226
+ this._authCodeProvider = authCodeProviderFrom(auth?.authorizationCode);
1227
+ this._delegatedProvider = auth?.delegation;
711
1228
  }
712
1229
  // ── Lifecycle ─────────────────────────────────────────────────────────────
1230
+ /**
1231
+ * The bearer to put on the upgrade request, if any.
1232
+ *
1233
+ * Resolved *before* the handshake is built. Fetching a token is async while
1234
+ * header construction is not, so a provider-backed token could never reach
1235
+ * the upgrade if it were resolved inside the header builder — which is
1236
+ * exactly the bug this replaced: an OAuth client authenticated over WS only
1237
+ * if it also happened to present an mTLS identity.
1238
+ */
1239
+ async _resolveBearer() {
1240
+ if (this._delegatedProvider) return this._delegatedProvider.getToken();
1241
+ if (this._oauthProvider) return this._oauthProvider.getToken();
1242
+ if (this._authCodeProvider) return this._authCodeProvider.getToken();
1243
+ return void 0;
1244
+ }
713
1245
  async connect() {
714
1246
  if (this._ws !== null) return;
715
1247
  const WsImpl = await resolveWebSocketImpl();
716
- const headers = buildUpgradeHeaders(this._auth);
1248
+ const headers = buildUpgradeHeaders(
1249
+ this._auth,
1250
+ await this._resolveBearer()
1251
+ );
717
1252
  const ws = new WsImpl(this._url, [SUBPROTOCOL], {
718
- headers
1253
+ headers,
1254
+ ...buildTlsOptions(this._url, this._identity)
719
1255
  });
720
1256
  await new Promise((resolve, reject) => {
721
1257
  ws.onopen = () => resolve();
@@ -949,8 +1485,12 @@ var WsTransport = class extends Transport {
949
1485
  this._subscriptions.clear();
950
1486
  }
951
1487
  };
952
- function buildUpgradeHeaders(auth) {
1488
+ function buildUpgradeHeaders(auth, resolvedBearer) {
953
1489
  const headers = {};
1490
+ if (resolvedBearer !== void 0) {
1491
+ headers["Authorization"] = `Bearer ${resolvedBearer}`;
1492
+ return headers;
1493
+ }
954
1494
  if (auth === void 0) return headers;
955
1495
  if ("bearer" in auth && auth.bearer !== void 0) {
956
1496
  headers["Authorization"] = `Bearer ${auth.bearer}`;
@@ -964,6 +1504,21 @@ function buildUpgradeHeaders(auth) {
964
1504
  }
965
1505
  return headers;
966
1506
  }
1507
+ function buildTlsOptions(url, identity) {
1508
+ if (identity === void 0) return {};
1509
+ if (!url.startsWith("wss:")) return {};
1510
+ if (typeof process === "undefined" || !process.versions?.node) {
1511
+ console.warn(
1512
+ "[conduit] mTLS identity is set but this environment does not support client certificates on WebSocket. The connection will proceed without mTLS."
1513
+ );
1514
+ return {};
1515
+ }
1516
+ return {
1517
+ cert: identity.certPem,
1518
+ key: identity.keyPem,
1519
+ ...identity.caPem ? { ca: identity.caPem } : {}
1520
+ };
1521
+ }
967
1522
  async function resolveWebSocketImpl() {
968
1523
  if (typeof globalThis.WebSocket !== "undefined") {
969
1524
  return globalThis.WebSocket;
@@ -1149,6 +1704,8 @@ var PrismNamespace = class {
1149
1704
  this.callDg = callDg;
1150
1705
  this.warn = warn;
1151
1706
  }
1707
+ callDg;
1708
+ warn;
1152
1709
  /** AI-driven data transformation (`data-grout/prism.refract`). */
1153
1710
  async refract(options) {
1154
1711
  this.warn("prism.refract");
@@ -1218,6 +1775,8 @@ var LogicNamespace = class {
1218
1775
  this.callDg = callDg;
1219
1776
  this.warn = warn;
1220
1777
  }
1778
+ callDg;
1779
+ warn;
1221
1780
  async remember(statementOrOptions, optionsArg) {
1222
1781
  let statement;
1223
1782
  let opts;
@@ -1326,6 +1885,8 @@ var WardenNamespace = class {
1326
1885
  this.callDg = callDg;
1327
1886
  this.warn = warn;
1328
1887
  }
1888
+ callDg;
1889
+ warn;
1329
1890
  /** Run a canary safety check (`data-grout/warden.canary`). */
1330
1891
  async canary(params) {
1331
1892
  this.warn("warden.canary");
@@ -1355,6 +1916,8 @@ var DeliverablesNamespace = class {
1355
1916
  this.callDg = callDg;
1356
1917
  this.warn = warn;
1357
1918
  }
1919
+ callDg;
1920
+ warn;
1358
1921
  /** Register a work product (`data-grout/deliverables.register`). */
1359
1922
  async register(params) {
1360
1923
  this.warn("deliverables.register");
@@ -1379,6 +1942,8 @@ var EphemeralsNamespace = class {
1379
1942
  this.callDg = callDg;
1380
1943
  this.warn = warn;
1381
1944
  }
1945
+ callDg;
1946
+ warn;
1382
1947
  /** List cached results (`data-grout/ephemerals.list`). */
1383
1948
  async list(params = {}) {
1384
1949
  this.warn("ephemerals.list");
@@ -1398,6 +1963,8 @@ var FlowNamespace = class {
1398
1963
  this.callDg = callDg;
1399
1964
  this.warn = warn;
1400
1965
  }
1966
+ callDg;
1967
+ warn;
1401
1968
  /** Execute a multi-step workflow plan (`data-grout/flow.into`). */
1402
1969
  async run(options) {
1403
1970
  this.warn("flow.into");
@@ -1784,7 +2351,7 @@ var Client2 = class _Client {
1784
2351
  async listTools(options) {
1785
2352
  this.ensureInitialized();
1786
2353
  return this.sendWithRetry(async () => {
1787
- let allTools = [];
2354
+ const allTools = [];
1788
2355
  let cursor;
1789
2356
  do {
1790
2357
  const response = await this.transport.listTools({ ...options, cursor });
@@ -2122,6 +2689,637 @@ var Client2 = class _Client {
2122
2689
  init_identity();
2123
2690
  init_oauth();
2124
2691
 
2692
+ // src/delegation.ts
2693
+ var GRANT_TYPE = "urn:ietf:params:oauth:grant-type:token-exchange";
2694
+ var INSPECT_CUSTOM = /* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom");
2695
+ var REFRESH_SKEW_SECS2 = 60;
2696
+ var SERVER_ERROR_CODES = Object.freeze([
2697
+ /** Malformed request, or a required parameter missing. */
2698
+ "invalid_request",
2699
+ /** Client authentication failed. */
2700
+ "invalid_client",
2701
+ /** The subject or actor token is invalid, expired, or revoked. */
2702
+ "invalid_grant",
2703
+ /** This client may not use this grant — including a client that is not the actor. */
2704
+ "unauthorized_client",
2705
+ /** The requested `audience` or `resource` is not served here (RFC 8693 §2.2.2). */
2706
+ "invalid_target",
2707
+ /** A requested scope is unknown or exceeds what the subject token allows. */
2708
+ "invalid_scope",
2709
+ /** The server does not support the exchange. */
2710
+ "unsupported_grant_type"
2711
+ ]);
2712
+ var TOKEN_TYPES = Object.freeze({
2713
+ /** The default for both subject and actor, and what DataGrout issues. */
2714
+ access_token: "urn:ietf:params:oauth:token-type:access_token",
2715
+ /** A JWT presented as a JWT rather than as an opaque access token. */
2716
+ jwt: "urn:ietf:params:oauth:token-type:jwt",
2717
+ id_token: "urn:ietf:params:oauth:token-type:id_token",
2718
+ refresh_token: "urn:ietf:params:oauth:token-type:refresh_token",
2719
+ saml2: "urn:ietf:params:oauth:token-type:saml2"
2720
+ });
2721
+ function tokenTypeName(tokenType) {
2722
+ for (const [name, urn] of Object.entries(TOKEN_TYPES)) {
2723
+ if (urn === tokenType) return name;
2724
+ }
2725
+ return "other";
2726
+ }
2727
+ var DelegationError = class extends ConduitError {
2728
+ kind;
2729
+ /** HTTP status, for `server`. */
2730
+ status;
2731
+ /** RFC 6749 error code, for `server`; see {@link SERVER_ERROR_CODES}. */
2732
+ error;
2733
+ /** Human-readable description, when the server gave one. */
2734
+ errorDescription;
2735
+ constructor(kind, message, extra) {
2736
+ super(message);
2737
+ this.kind = kind;
2738
+ this.status = extra?.status;
2739
+ this.error = extra?.error;
2740
+ this.errorDescription = extra?.errorDescription;
2741
+ }
2742
+ };
2743
+ var DelegationRequest = class _DelegationRequest {
2744
+ endpoint;
2745
+ client;
2746
+ secret;
2747
+ auth = "body";
2748
+ subject;
2749
+ actor;
2750
+ audienceValue;
2751
+ resourceValue;
2752
+ scopeValue;
2753
+ requestedTokenTypeValue;
2754
+ impersonating = false;
2755
+ /**
2756
+ * Start a request against `tokenEndpoint`, authenticating as `clientId`.
2757
+ *
2758
+ * The client should be the actor — see the module docs.
2759
+ */
2760
+ constructor(tokenEndpoint, clientId) {
2761
+ this.endpoint = tokenEndpoint;
2762
+ this.client = clientId;
2763
+ }
2764
+ /** The client secret, for confidential clients. */
2765
+ clientSecret(secret) {
2766
+ this.secret = secret;
2767
+ return this;
2768
+ }
2769
+ /** Where the client secret travels. Defaults to `"body"`. */
2770
+ clientAuth(auth) {
2771
+ this.auth = auth;
2772
+ return this;
2773
+ }
2774
+ /**
2775
+ * The token being exchanged: the **user's**, whose identity the issued token
2776
+ * will carry as `sub`.
2777
+ */
2778
+ subjectToken(token, tokenType = TOKEN_TYPES.access_token) {
2779
+ this.subject = { token, tokenType };
2780
+ return this;
2781
+ }
2782
+ /** The **agent's** own token, which the issued token will name in `act`. */
2783
+ actorToken(token, tokenType = TOKEN_TYPES.access_token) {
2784
+ this.actor = { token, tokenType };
2785
+ return this;
2786
+ }
2787
+ /** Logical name of the service the token is for (RFC 8693 `audience`). */
2788
+ audience(audience) {
2789
+ this.audienceValue = audience;
2790
+ return this;
2791
+ }
2792
+ /**
2793
+ * URI of the resource the token is for (RFC 8707 `resource`). Always sent
2794
+ * when set, so the token cannot be replayed elsewhere.
2795
+ */
2796
+ resource(resource) {
2797
+ this.resourceValue = resource;
2798
+ return this;
2799
+ }
2800
+ /** Scopes to request, space-separated. */
2801
+ scope(scope) {
2802
+ this.scopeValue = scope;
2803
+ return this;
2804
+ }
2805
+ /** The kind of token wanted back. Servers default to an access token. */
2806
+ requestedTokenType(tokenType) {
2807
+ this.requestedTokenTypeValue = tokenType;
2808
+ return this;
2809
+ }
2810
+ /**
2811
+ * Opt out of delegation: send no `actor_token`, so the issued token has no
2812
+ * `act` claim and the agent is indistinguishable from the user.
2813
+ *
2814
+ * DataGrout does not issue these. This exists for other RFC 8693 servers, and
2815
+ * it is a builder call rather than a default precisely so that forgetting to
2816
+ * set an actor is an error instead of a silent downgrade.
2817
+ */
2818
+ impersonation() {
2819
+ this.impersonating = true;
2820
+ return this;
2821
+ }
2822
+ /** The token endpoint this request posts to. */
2823
+ get tokenEndpoint() {
2824
+ return this.endpoint;
2825
+ }
2826
+ /** The client id this request authenticates as. */
2827
+ get clientId() {
2828
+ return this.client;
2829
+ }
2830
+ /** Whether {@link impersonation} was called. */
2831
+ get isImpersonation() {
2832
+ return this.impersonating;
2833
+ }
2834
+ /** An independent copy, so a template can be filled in per exchange. */
2835
+ clone() {
2836
+ const copy = new _DelegationRequest(this.endpoint, this.client);
2837
+ copy.secret = this.secret;
2838
+ copy.auth = this.auth;
2839
+ copy.subject = this.subject && { ...this.subject };
2840
+ copy.actor = this.actor && { ...this.actor };
2841
+ copy.audienceValue = this.audienceValue;
2842
+ copy.resourceValue = this.resourceValue;
2843
+ copy.scopeValue = this.scopeValue;
2844
+ copy.requestedTokenTypeValue = this.requestedTokenTypeValue;
2845
+ copy.impersonating = this.impersonating;
2846
+ return copy;
2847
+ }
2848
+ /**
2849
+ * The form body this request will post, in wire order.
2850
+ *
2851
+ * Throws before any network activity when the request is incomplete:
2852
+ * `missing_subject`, or `missing_actor` unless {@link impersonation} was
2853
+ * called. Public so a caller — or another SDK's test suite — can check the
2854
+ * body against the contract fixture without a server.
2855
+ */
2856
+ formParams() {
2857
+ if (this.subject === void 0) {
2858
+ throw new DelegationError(
2859
+ "missing_subject",
2860
+ "no subject_token \u2014 call subjectToken() first"
2861
+ );
2862
+ }
2863
+ const form = [
2864
+ ["grant_type", GRANT_TYPE],
2865
+ ["subject_token", this.subject.token],
2866
+ ["subject_token_type", this.subject.tokenType]
2867
+ ];
2868
+ if (this.actor !== void 0) {
2869
+ form.push(["actor_token", this.actor.token]);
2870
+ form.push(["actor_token_type", this.actor.tokenType]);
2871
+ } else if (!this.impersonating) {
2872
+ throw new DelegationError(
2873
+ "missing_actor",
2874
+ "no actor_token \u2014 delegation requires one; call impersonation() to opt out explicitly"
2875
+ );
2876
+ }
2877
+ form.push(["client_id", this.client]);
2878
+ if (this.secret !== void 0 && this.auth === "body") {
2879
+ form.push(["client_secret", this.secret]);
2880
+ }
2881
+ for (const [key, value] of [
2882
+ ["audience", this.audienceValue],
2883
+ ["resource", this.resourceValue],
2884
+ ["scope", this.scopeValue],
2885
+ ["requested_token_type", this.requestedTokenTypeValue]
2886
+ ]) {
2887
+ if (value !== void 0) form.push([key, value]);
2888
+ }
2889
+ return form;
2890
+ }
2891
+ /** Perform the exchange. */
2892
+ async exchange(fetchImpl = globalThis.fetch) {
2893
+ const form = this.formParams();
2894
+ const headers = {
2895
+ "Content-Type": "application/x-www-form-urlencoded"
2896
+ };
2897
+ if (this.secret !== void 0 && this.auth === "basic") {
2898
+ const credentials = Buffer.from(
2899
+ `${this.client}:${this.secret}`,
2900
+ "utf8"
2901
+ ).toString("base64");
2902
+ headers["Authorization"] = `Basic ${credentials}`;
2903
+ }
2904
+ let response;
2905
+ try {
2906
+ response = await fetchImpl(this.endpoint, {
2907
+ method: "POST",
2908
+ headers,
2909
+ // `URLSearchParams` keeps the order it is given, which is the wire
2910
+ // order the contract fixture pins.
2911
+ body: new URLSearchParams(form).toString()
2912
+ });
2913
+ } catch (err) {
2914
+ throw new DelegationError("http", `HTTP error: ${err}`);
2915
+ }
2916
+ const body = await response.text().catch(() => "");
2917
+ if (!response.ok) {
2918
+ throw errorFromBody(response.status, body);
2919
+ }
2920
+ let parsed;
2921
+ try {
2922
+ parsed = JSON.parse(body);
2923
+ } catch (err) {
2924
+ throw new DelegationError(
2925
+ "invalid_response",
2926
+ `HTTP ${response.status}: ${err}`
2927
+ );
2928
+ }
2929
+ return tokenFromWire(parsed, response.status);
2930
+ }
2931
+ };
2932
+ function errorFromBody(status, body) {
2933
+ let parsed;
2934
+ try {
2935
+ parsed = JSON.parse(body);
2936
+ } catch {
2937
+ parsed = void 0;
2938
+ }
2939
+ const oauth = parsed;
2940
+ if (oauth && typeof oauth.error === "string") {
2941
+ const description = typeof oauth.error_description === "string" ? oauth.error_description : void 0;
2942
+ return new DelegationError(
2943
+ "server",
2944
+ `delegation exchange refused (HTTP ${status}): ${oauth.error}` + (description ? ` \u2014 ${description}` : ""),
2945
+ { status, error: oauth.error, errorDescription: description }
2946
+ );
2947
+ }
2948
+ return new DelegationError(
2949
+ "invalid_response",
2950
+ `HTTP ${status} with a non-OAuth body: ${body.slice(0, 200)}`
2951
+ );
2952
+ }
2953
+ function tokenFromWire(parsed, status) {
2954
+ const wire = parsed;
2955
+ const missing = ["access_token", "issued_token_type", "token_type"].filter(
2956
+ (field) => typeof wire?.[field] !== "string"
2957
+ );
2958
+ if (missing.length > 0) {
2959
+ throw new DelegationError(
2960
+ "invalid_response",
2961
+ `HTTP ${status}: delegation response is missing required field(s): ${missing.join(", ")}`
2962
+ );
2963
+ }
2964
+ const expiresIn = wire["expires_in"];
2965
+ const scope = wire["scope"];
2966
+ return {
2967
+ access_token: wire["access_token"],
2968
+ issued_token_type: wire["issued_token_type"],
2969
+ token_type: wire["token_type"],
2970
+ ...typeof expiresIn === "number" ? { expires_at: nowSecs2() + expiresIn } : {},
2971
+ ...typeof scope === "string" ? { scope } : {}
2972
+ };
2973
+ }
2974
+ function isDelegatedTokenExpired(token) {
2975
+ if (token.expires_at === void 0) return false;
2976
+ return nowSecs2() + REFRESH_SKEW_SECS2 >= token.expires_at;
2977
+ }
2978
+ var TokenSource = class _TokenSource {
2979
+ resolver;
2980
+ sourceKind;
2981
+ declaredType;
2982
+ constructor(kind, tokenType, resolver) {
2983
+ this.sourceKind = kind;
2984
+ this.declaredType = tokenType;
2985
+ this.resolver = resolver;
2986
+ }
2987
+ /** A fixed token, e.g. one handed to the agent for this run. */
2988
+ static staticToken(token, tokenType = TOKEN_TYPES.access_token) {
2989
+ return new _TokenSource("static", tokenType, async () => token);
2990
+ }
2991
+ /** The agent's own `client_credentials` provider — the usual **actor**. */
2992
+ static clientCredentials(provider, tokenType = TOKEN_TYPES.access_token) {
2993
+ return new _TokenSource(
2994
+ "client_credentials",
2995
+ tokenType,
2996
+ () => provider.getToken()
2997
+ );
2998
+ }
2999
+ /**
3000
+ * A user's authorization-code provider — the usual **subject** in an app that
3001
+ * signed the user in itself. Refreshes its grant as needed, so the exchange
3002
+ * always sees a live subject token.
3003
+ */
3004
+ static authorizationCode(provider, tokenType = TOKEN_TYPES.access_token) {
3005
+ return new _TokenSource(
3006
+ "authorization_code",
3007
+ tokenType,
3008
+ () => provider.getToken()
3009
+ );
3010
+ }
3011
+ /**
3012
+ * Any function that yields a token — a vault lookup, a header from an inbound
3013
+ * request, another SDK's provider. Called on every exchange.
3014
+ */
3015
+ static dynamic(fn, tokenType = TOKEN_TYPES.access_token) {
3016
+ return new _TokenSource("dynamic", tokenType, async () => fn());
3017
+ }
3018
+ /** Declare a different {@link TokenType} for this source. */
3019
+ withTokenType(tokenType) {
3020
+ return new _TokenSource(this.sourceKind, tokenType, this.resolver);
3021
+ }
3022
+ /** The declared token type. */
3023
+ get tokenType() {
3024
+ return this.declaredType;
3025
+ }
3026
+ /** Where the token comes from. */
3027
+ get kind() {
3028
+ return this.sourceKind;
3029
+ }
3030
+ /** Draw a token. */
3031
+ resolve() {
3032
+ return this.resolver();
3033
+ }
3034
+ /** Never print tokens — only where they come from. */
3035
+ toJSON() {
3036
+ return { kind: this.sourceKind, tokenType: this.declaredType };
3037
+ }
3038
+ [INSPECT_CUSTOM]() {
3039
+ return `TokenSource { kind: '${this.sourceKind}', tokenType: '${this.declaredType}' }`;
3040
+ }
3041
+ };
3042
+ var DelegatedProvider = class {
3043
+ template;
3044
+ subject;
3045
+ actor;
3046
+ fetchImpl;
3047
+ cached = null;
3048
+ /**
3049
+ * The in-flight exchange, so concurrent callers make one request rather than
3050
+ * a stampede. Mirrors `AuthCodeProvider`'s refresh de-duplication.
3051
+ */
3052
+ exchangePromise = null;
3053
+ /**
3054
+ * Wrap a request template with the sources of its two tokens.
3055
+ *
3056
+ * Any `subjectToken` or `actorToken` already on `request` is ignored; the
3057
+ * sources supply them. Omit `actor` only with a request that called
3058
+ * {@link DelegationRequest.impersonation} — otherwise every `getToken` fails
3059
+ * with `missing_actor`, which is the intended loud failure rather than a
3060
+ * silent downgrade.
3061
+ */
3062
+ constructor(request, subject, actor, fetchImpl = globalThis.fetch) {
3063
+ this.template = request;
3064
+ this.subject = subject;
3065
+ this.actor = actor;
3066
+ this.fetchImpl = fetchImpl;
3067
+ }
3068
+ /**
3069
+ * The current delegated bearer, exchanging first if there is none or it is at
3070
+ * or near expiry.
3071
+ */
3072
+ async getToken() {
3073
+ const live = this.liveToken();
3074
+ if (live !== void 0) return live;
3075
+ if (!this.exchangePromise) {
3076
+ this.exchangePromise = this.exchange().then((token2) => {
3077
+ this.cached = token2;
3078
+ return token2;
3079
+ }).finally(() => {
3080
+ this.exchangePromise = null;
3081
+ });
3082
+ }
3083
+ const token = await this.exchangePromise;
3084
+ return token.access_token;
3085
+ }
3086
+ /**
3087
+ * Force the next {@link getToken} to exchange again. Call on a 401.
3088
+ *
3089
+ * Only the delegated token is dropped. The subject and actor sources are left
3090
+ * alone: a provider-backed source tracks its own expiry, and a 401 from the
3091
+ * resource server says nothing about them.
3092
+ */
3093
+ invalidate() {
3094
+ this.cached = null;
3095
+ }
3096
+ /** A snapshot of the cached token, if any — for inspection or logging. */
3097
+ token() {
3098
+ return this.cached === null ? void 0 : { ...this.cached };
3099
+ }
3100
+ /** The request template, without tokens. */
3101
+ get request() {
3102
+ return this.template;
3103
+ }
3104
+ /** Never print tokens, and never the client secret the template carries. */
3105
+ toJSON() {
3106
+ return {
3107
+ tokenEndpoint: this.template.tokenEndpoint,
3108
+ clientId: this.template.clientId,
3109
+ subject: this.subject.toJSON(),
3110
+ ...this.actor ? { actor: this.actor.toJSON() } : {},
3111
+ hasToken: this.cached !== null
3112
+ };
3113
+ }
3114
+ [INSPECT_CUSTOM]() {
3115
+ return `DelegatedProvider ${JSON.stringify(this.toJSON())}`;
3116
+ }
3117
+ // ─── Private ───────────────────────────────────────────────────────────────
3118
+ liveToken() {
3119
+ if (this.cached === null) return void 0;
3120
+ if (isDelegatedTokenExpired(this.cached)) return void 0;
3121
+ return this.cached.access_token;
3122
+ }
3123
+ async exchange() {
3124
+ if (this.actor === void 0 && !this.template.isImpersonation) {
3125
+ throw new DelegationError(
3126
+ "missing_actor",
3127
+ "no actor_token \u2014 delegation requires one; call impersonation() to opt out explicitly"
3128
+ );
3129
+ }
3130
+ const request = this.template.clone().subjectToken(await this.subject.resolve(), this.subject.tokenType);
3131
+ if (this.actor !== void 0) {
3132
+ request.actorToken(await this.actor.resolve(), this.actor.tokenType);
3133
+ }
3134
+ return request.exchange(this.fetchImpl);
3135
+ }
3136
+ };
3137
+ function nowSecs2() {
3138
+ return Math.floor(Date.now() / 1e3);
3139
+ }
3140
+
3141
+ // src/loopback.ts
3142
+ import { createServer } from "http";
3143
+ var LoopbackListener = class _LoopbackListener {
3144
+ server;
3145
+ boundPort;
3146
+ path;
3147
+ settled = false;
3148
+ constructor(server, port, path3) {
3149
+ this.server = server;
3150
+ this.boundPort = port;
3151
+ this.path = path3;
3152
+ }
3153
+ /**
3154
+ * Bind an OS-assigned port on `127.0.0.1`.
3155
+ *
3156
+ * Letting the OS choose avoids fighting whatever else owns a fixed port —
3157
+ * and because registration happens after binding, the real port is already
3158
+ * known by the time the redirect URI is registered.
3159
+ */
3160
+ static bind() {
3161
+ return _LoopbackListener.bindOn(0, "/callback");
3162
+ }
3163
+ /**
3164
+ * Bind a specific port and path.
3165
+ *
3166
+ * Use when the client was registered out of band against a fixed redirect
3167
+ * URI and the authorization server will accept no other.
3168
+ */
3169
+ static bindOn(port, path3) {
3170
+ const normalized = path3.startsWith("/") ? path3 : `/${path3}`;
3171
+ return new Promise((resolve, reject) => {
3172
+ const server = createServer();
3173
+ server.once("error", (err) => {
3174
+ reject(new AuthCodeError("http", `cannot bind loopback port: ${err}`));
3175
+ });
3176
+ server.listen(port, "127.0.0.1", () => {
3177
+ const address = server.address();
3178
+ if (!address) {
3179
+ server.close();
3180
+ reject(
3181
+ new AuthCodeError("http", "cannot bind loopback port: no address")
3182
+ );
3183
+ return;
3184
+ }
3185
+ resolve(new _LoopbackListener(server, address.port, normalized));
3186
+ });
3187
+ });
3188
+ }
3189
+ /**
3190
+ * Re-bind the exact port and path of a previously registered redirect URI.
3191
+ *
3192
+ * Needed whenever a saved registration is reused: the authorization server
3193
+ * matches the redirect URI exactly, so the listener has to come back on the
3194
+ * same port it registered.
3195
+ *
3196
+ * Rejects if that port is occupied. The right recovery is to {@link bind} a
3197
+ * fresh port and register a new client — not to retry, and not to authorize
3198
+ * against a URI the server will reject.
3199
+ */
3200
+ static bindFor(redirectUri) {
3201
+ let parsed;
3202
+ try {
3203
+ parsed = new URL(redirectUri);
3204
+ } catch (err) {
3205
+ return Promise.reject(
3206
+ new AuthCodeError("http", `bad redirect_uri ${redirectUri}: ${err}`)
3207
+ );
3208
+ }
3209
+ if (!parsed.port) {
3210
+ return Promise.reject(
3211
+ new AuthCodeError("http", `redirect_uri ${redirectUri} names no port`)
3212
+ );
3213
+ }
3214
+ return _LoopbackListener.bindOn(Number(parsed.port), parsed.pathname);
3215
+ }
3216
+ /** The port actually bound. */
3217
+ get port() {
3218
+ return this.boundPort;
3219
+ }
3220
+ /**
3221
+ * The redirect URI to register and to send in the authorize request.
3222
+ *
3223
+ * Uses `127.0.0.1` rather than `localhost`: RFC 8252 recommends the literal
3224
+ * address, and it sidesteps hosts where `localhost` resolves to IPv6 first
3225
+ * while the listener is bound to IPv4.
3226
+ */
3227
+ get redirectUri() {
3228
+ return `http://127.0.0.1:${this.boundPort}${this.path}`;
3229
+ }
3230
+ /** Stop listening. Safe to call more than once. */
3231
+ close() {
3232
+ this.server.close();
3233
+ }
3234
+ /**
3235
+ * Wait for the browser's redirect, up to `timeoutMs`.
3236
+ *
3237
+ * Serves a small page either way so the user sees an outcome rather than a
3238
+ * browser error, then stops listening. Requests to other paths are answered
3239
+ * 404 and ignored — browsers routinely ask for `/favicon.ico`, and treating
3240
+ * that as the redirect would abort the flow.
3241
+ */
3242
+ wait(timeoutMs) {
3243
+ return new Promise((resolve, reject) => {
3244
+ const finish = (fn) => {
3245
+ if (this.settled) return;
3246
+ this.settled = true;
3247
+ clearTimeout(timer);
3248
+ this.server.removeAllListeners("request");
3249
+ this.close();
3250
+ fn();
3251
+ };
3252
+ const timer = setTimeout(() => {
3253
+ finish(
3254
+ () => reject(
3255
+ new AuthCodeError(
3256
+ "http",
3257
+ `timed out after ${Math.round(
3258
+ timeoutMs / 1e3
3259
+ )}s waiting for the authorization redirect`
3260
+ )
3261
+ )
3262
+ );
3263
+ }, timeoutMs);
3264
+ timer.unref?.();
3265
+ this.server.on("request", (req, res) => {
3266
+ const target = new URL(req.url ?? "/", "http://127.0.0.1");
3267
+ if (target.pathname !== this.path) {
3268
+ respond(res, 404, "Not found");
3269
+ return;
3270
+ }
3271
+ const error = target.searchParams.get("error");
3272
+ if (error) {
3273
+ respond(
3274
+ res,
3275
+ 200,
3276
+ "Authorization was denied. You can close this window."
3277
+ );
3278
+ const description = target.searchParams.get("error_description");
3279
+ finish(
3280
+ () => reject(
3281
+ new AuthCodeError(
3282
+ "denied",
3283
+ `authorization denied: ${error}${description ? ` \u2014 ${description}` : ""}`
3284
+ )
3285
+ )
3286
+ );
3287
+ return;
3288
+ }
3289
+ const code = target.searchParams.get("code");
3290
+ const state = target.searchParams.get("state");
3291
+ if (code !== null && state !== null) {
3292
+ respond(
3293
+ res,
3294
+ 200,
3295
+ "Signed in. You can close this window and return to the app."
3296
+ );
3297
+ finish(() => resolve({ code, state }));
3298
+ return;
3299
+ }
3300
+ respond(res, 400, "Missing code or state.");
3301
+ finish(
3302
+ () => reject(
3303
+ new AuthCodeError(
3304
+ "discovery",
3305
+ "redirect carried neither an error nor a code/state pair"
3306
+ )
3307
+ )
3308
+ );
3309
+ });
3310
+ });
3311
+ }
3312
+ };
3313
+ function respond(res, status, message) {
3314
+ 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>`;
3315
+ res.writeHead(status, {
3316
+ "content-type": "text/html; charset=utf-8",
3317
+ "content-length": Buffer.byteLength(body),
3318
+ connection: "close"
3319
+ });
3320
+ res.end(body);
3321
+ }
3322
+
2125
3323
  // src/types.ts
2126
3324
  function extractMeta(result) {
2127
3325
  const rich = result?._meta?.datagrout;
@@ -2187,37 +3385,55 @@ function buildToolMeta(raw) {
2187
3385
  } : void 0;
2188
3386
  return { receipt, creditEstimate };
2189
3387
  }
2190
-
2191
- // src/index.ts
2192
- var version = "0.5.0";
2193
3388
  export {
3389
+ AuthCodeError,
3390
+ AuthCodeFlow,
3391
+ AuthCodeProvider,
2194
3392
  AuthError,
2195
3393
  Client2 as Client,
2196
3394
  ConduitError,
2197
3395
  ConduitIdentity,
2198
3396
  DEFAULT_IDENTITY_DIR,
3397
+ DEFAULT_SCOPE,
3398
+ GRANT_TYPE as DELEGATION_GRANT_TYPE,
3399
+ SERVER_ERROR_CODES as DELEGATION_SERVER_ERROR_CODES,
2199
3400
  DG_CA_URL,
2200
3401
  DG_SUBSTRATE_ENDPOINT,
3402
+ DelegatedProvider,
3403
+ DelegationError,
3404
+ DelegationRequest,
2201
3405
  GuidedSession,
2202
3406
  InvalidConfigError,
3407
+ LoopbackListener,
2203
3408
  NetworkError,
2204
3409
  NotInitializedError,
2205
3410
  OAuthTokenProvider,
2206
3411
  RateLimitError,
2207
3412
  ServerError,
3413
+ TOKEN_TYPES,
3414
+ TokenSource,
2208
3415
  SUBPROTOCOL as WS_SUBPROTOCOL,
2209
3416
  WsTransport,
3417
+ authCodeProviderFrom,
3418
+ challengeS256,
2210
3419
  deriveTokenEndpoint,
2211
3420
  extractMeta,
2212
3421
  fetchDgCaCert,
2213
3422
  fetchWithIdentity,
2214
3423
  generateKeypair,
3424
+ generateVerifier,
3425
+ isDelegatedTokenExpired,
2215
3426
  isDgUrl,
3427
+ isGrantExpired,
3428
+ isGrantRefreshable,
2216
3429
  refreshCaCert,
3430
+ refreshGrant,
2217
3431
  registerAndExchange,
2218
3432
  registerIdentity,
2219
3433
  registerOnly,
2220
3434
  rotateIdentity,
2221
3435
  saveIdentity,
3436
+ supportsS256,
3437
+ tokenTypeName,
2222
3438
  version
2223
3439
  };