@slashfi/agents-sdk 0.75.0 → 0.77.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.
Files changed (43) hide show
  1. package/dist/adk.js +186 -150
  2. package/dist/adk.js.map +1 -1
  3. package/dist/cjs/config-store.js +642 -38
  4. package/dist/cjs/config-store.js.map +1 -1
  5. package/dist/cjs/define-config.js.map +1 -1
  6. package/dist/cjs/index.js.map +1 -1
  7. package/dist/cjs/mcp-client.js +98 -0
  8. package/dist/cjs/mcp-client.js.map +1 -1
  9. package/dist/cjs/registry-consumer.js +76 -10
  10. package/dist/cjs/registry-consumer.js.map +1 -1
  11. package/dist/cjs/server.js +8 -0
  12. package/dist/cjs/server.js.map +1 -1
  13. package/dist/config-store.d.ts +43 -8
  14. package/dist/config-store.d.ts.map +1 -1
  15. package/dist/config-store.js +643 -39
  16. package/dist/config-store.js.map +1 -1
  17. package/dist/define-config.d.ts +83 -17
  18. package/dist/define-config.d.ts.map +1 -1
  19. package/dist/define-config.js.map +1 -1
  20. package/dist/index.d.ts +2 -2
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js.map +1 -1
  23. package/dist/mcp-client.d.ts +44 -0
  24. package/dist/mcp-client.d.ts.map +1 -1
  25. package/dist/mcp-client.js +95 -0
  26. package/dist/mcp-client.js.map +1 -1
  27. package/dist/registry-consumer.d.ts +10 -0
  28. package/dist/registry-consumer.d.ts.map +1 -1
  29. package/dist/registry-consumer.js +76 -10
  30. package/dist/registry-consumer.js.map +1 -1
  31. package/dist/server.d.ts +11 -0
  32. package/dist/server.d.ts.map +1 -1
  33. package/dist/server.js +8 -0
  34. package/dist/server.js.map +1 -1
  35. package/package.json +1 -1
  36. package/src/adk.ts +107 -65
  37. package/src/config-store.test.ts +381 -1
  38. package/src/config-store.ts +750 -55
  39. package/src/define-config.ts +89 -23
  40. package/src/index.ts +0 -2
  41. package/src/mcp-client.ts +121 -0
  42. package/src/registry-consumer.ts +101 -12
  43. package/src/server.ts +19 -0
@@ -439,15 +439,309 @@ function createAdk(fs, options = {}) {
439
439
  return fallback;
440
440
  }
441
441
  // ==========================================
442
+ // Proxy Routing
443
+ // ==========================================
444
+ /**
445
+ * Find the configured RegistryEntry for a ref, consulting `sourceRegistry`
446
+ * first and falling back to the first registry in config. Returns `null` when
447
+ * the ref is sourced from a raw URL (no registry), in which case proxy routing
448
+ * does not apply.
449
+ */
450
+ async function findRegistryEntryForRef(entry) {
451
+ const sourceUrl = entry.sourceRegistry?.url;
452
+ if (!sourceUrl)
453
+ return null;
454
+ const config = await readConfig();
455
+ const match = (config.registries ?? []).find((r) => {
456
+ if (typeof r === "string")
457
+ return r === sourceUrl;
458
+ return r.url === sourceUrl;
459
+ });
460
+ if (!match || typeof match === "string")
461
+ return null;
462
+ return match;
463
+ }
464
+ /**
465
+ * Returns the proxy settings for a ref when its source registry has
466
+ * `proxy` configured. `null` means "run locally".
467
+ *
468
+ * Callers pass `{ preferLocal: true }` to opt out of `mode: 'optional'`
469
+ * proxying when they already hold credentials locally. `mode: 'required'`
470
+ * cannot be bypassed — the registry owns auth server-side and there is
471
+ * nothing useful the local SDK can do.
472
+ */
473
+ async function resolveProxyForRef(entry, opts) {
474
+ const reg = await findRegistryEntryForRef(entry);
475
+ if (!reg?.proxy)
476
+ return null;
477
+ if (reg.proxy.mode === "optional" && opts?.preferLocal)
478
+ return null;
479
+ return { reg, agent: reg.proxy.agent ?? "@config" };
480
+ }
481
+ /**
482
+ * Forward an `@config ref` operation to the proxy agent on a remote registry.
483
+ *
484
+ * The remote side speaks the standard adk-tools surface, so the call shape is
485
+ * identical to what the local `ref` API would do — the only difference is
486
+ * that tokens and secrets live server-side. `callRegistry` returns the
487
+ * standard CallAgentResponse envelope: `{ success: true, result }` on
488
+ * success or `{ success: false, error }` on failure. We unwrap once and
489
+ * throw on error so callers get a result that matches the local signature.
490
+ */
491
+ async function forwardRefOpToProxy(reg, agent, operation, params) {
492
+ const consumer = await buildConsumerForRef({ ref: "", sourceRegistry: { url: reg.url, agentPath: agent } });
493
+ const resolved = consumer.registries().find((r) => r.url === reg.url);
494
+ if (!resolved)
495
+ throw new Error(`Registry ${reg.url} not resolvable for proxy forwarding`);
496
+ const response = await consumer.callRegistry(resolved, {
497
+ action: "execute_tool",
498
+ path: agent,
499
+ tool: "ref",
500
+ params: { operation, ...params },
501
+ });
502
+ if (!response.success) {
503
+ const errResponse = response;
504
+ const msg = errResponse.error ?? `Proxy ${agent}.ref(${operation}) failed`;
505
+ throw new Error(msg);
506
+ }
507
+ return response.result;
508
+ }
509
+ // ==========================================
442
510
  // Registry API
443
511
  // ==========================================
512
+ /**
513
+ * Encrypt with `secret:` prefix when an encryption key is configured, so the
514
+ * value is readable by the existing `decryptConfigSecrets` path on the read
515
+ * side. Plaintext fallback preserves the "no key = dev mode" contract.
516
+ */
517
+ async function protectSecret(value) {
518
+ if (!options.encryptionKey)
519
+ return value;
520
+ return `${SECRET_PREFIX}${await (0, crypto_js_1.encryptSecret)(value, options.encryptionKey)}`;
521
+ }
522
+ /**
523
+ * Re-probe a registry with the current stored credentials to see whether it
524
+ * advertises `capabilities.registry.proxy` in its MCP `initialize` response,
525
+ * and persist the proxy config when it does. Safe to call after a successful
526
+ * `auth()` / `authLocal()` — on the add path we skip the proxy probe when
527
+ * auth is required, so this is the second chance to back-fill it.
528
+ *
529
+ * Respects explicit user config: if `proxy` is already set, we leave it
530
+ * alone. Any discovery failure is swallowed — proxy is an optimization,
531
+ * not a correctness requirement.
532
+ */
533
+ async function discoverProxyAfterAuth(nameOrUrl) {
534
+ const config = await readConfig();
535
+ const target = findRegistry(config.registries ?? [], nameOrUrl);
536
+ if (!target || typeof target === "string")
537
+ return;
538
+ if (target.proxy)
539
+ return;
540
+ try {
541
+ const consumer = await buildConsumer(nameOrUrl);
542
+ const discovered = await consumer.discover(target.url);
543
+ if (!discovered.proxy?.mode)
544
+ return;
545
+ await updateRegistryEntry(nameOrUrl, (existing) => {
546
+ if (existing.proxy)
547
+ return;
548
+ existing.proxy = {
549
+ mode: discovered.proxy.mode,
550
+ ...(discovered.proxy.agent && { agent: discovered.proxy.agent }),
551
+ };
552
+ });
553
+ }
554
+ catch {
555
+ // Proxy probe is best-effort — auth itself already succeeded.
556
+ }
557
+ }
558
+ /**
559
+ * Atomic read-modify-write on a registry entry by name or URL. Used by
560
+ * `authLocal` to persist both `auth` and `oauth` together, which `auth()`
561
+ * alone can't express. Returns true when the entry was found and written.
562
+ */
563
+ async function updateRegistryEntry(nameOrUrl, mutate) {
564
+ const config = await readConfig();
565
+ if (!config.registries?.length)
566
+ return false;
567
+ let found = false;
568
+ const registries = config.registries.map((r) => {
569
+ const rName = registryDisplayName(r);
570
+ if (rName !== nameOrUrl && registryUrl(r) !== nameOrUrl)
571
+ return r;
572
+ found = true;
573
+ const existing = typeof r === "string" ? { url: r } : { ...r };
574
+ mutate(existing);
575
+ return existing;
576
+ });
577
+ if (!found)
578
+ return false;
579
+ await writeConfig({ ...config, registries });
580
+ return true;
581
+ }
582
+ /**
583
+ * Decrypt a `secret:`-prefixed value if we hold the encryption key. Plaintext
584
+ * values pass through unchanged so dev configs keep working.
585
+ */
586
+ async function revealSecret(value) {
587
+ if (!value)
588
+ return value;
589
+ if (!value.startsWith(SECRET_PREFIX))
590
+ return value;
591
+ if (!options.encryptionKey)
592
+ return undefined;
593
+ return (0, crypto_js_1.decryptSecret)(value.slice(SECRET_PREFIX.length), options.encryptionKey);
594
+ }
595
+ /**
596
+ * Refresh a registry's OAuth access token using the stored refresh token.
597
+ * Persists the new access token (encrypted) and updates `expiresAt`. If the
598
+ * provider rotates the refresh token, that's encrypted and stored too.
599
+ * Returns `true` when the refresh succeeded. Callers should catch and fall
600
+ * back to full re-auth on failure.
601
+ */
602
+ async function refreshRegistryToken(nameOrUrl) {
603
+ const config = await readConfig();
604
+ const target = findRegistry(config.registries ?? [], nameOrUrl);
605
+ if (!target || typeof target === "string")
606
+ return false;
607
+ const oauth = target.oauth;
608
+ if (!oauth?.refreshToken || !oauth.tokenEndpoint || !oauth.clientId)
609
+ return false;
610
+ const refreshToken = await revealSecret(oauth.refreshToken);
611
+ const clientSecret = await revealSecret(oauth.clientSecret);
612
+ if (!refreshToken)
613
+ return false;
614
+ const refreshed = await (0, mcp_client_js_1.refreshAccessToken)(oauth.tokenEndpoint, {
615
+ refreshToken,
616
+ clientId: oauth.clientId,
617
+ ...(clientSecret && { clientSecret }),
618
+ });
619
+ const expiresAt = refreshed.expiresIn
620
+ ? new Date(Date.now() + refreshed.expiresIn * 1000).toISOString()
621
+ : undefined;
622
+ const encAccess = await protectSecret(refreshed.accessToken);
623
+ const encRefresh = refreshed.refreshToken
624
+ ? await protectSecret(refreshed.refreshToken)
625
+ : undefined;
626
+ await updateRegistryEntry(nameOrUrl, (existing) => {
627
+ existing.auth = { type: "bearer", token: encAccess };
628
+ if (!existing.oauth)
629
+ return;
630
+ if (encRefresh)
631
+ existing.oauth.refreshToken = encRefresh;
632
+ if (expiresAt)
633
+ existing.oauth.expiresAt = expiresAt;
634
+ else
635
+ delete existing.oauth.expiresAt;
636
+ });
637
+ return true;
638
+ }
639
+ /**
640
+ * Run a registry op once; on 401 (`registry_auth_required`), try to refresh
641
+ * via the stored refresh token and retry exactly once. Any other AdkError
642
+ * propagates as-is.
643
+ */
644
+ async function callWithRefresh(nameOrUrl, fn) {
645
+ try {
646
+ return await fn();
647
+ }
648
+ catch (err) {
649
+ if (!(err instanceof adk_error_js_1.AdkError) || err.code !== "registry_auth_required")
650
+ throw err;
651
+ let refreshed = false;
652
+ try {
653
+ refreshed = await refreshRegistryToken(nameOrUrl);
654
+ }
655
+ catch {
656
+ // Refresh failed — surface the original 401 below.
657
+ }
658
+ if (!refreshed)
659
+ throw err;
660
+ return fn();
661
+ }
662
+ }
663
+ /**
664
+ * Throw a typed error if the registry has a recorded auth challenge and
665
+ * no usable credentials on the entry. Callers should invoke this before
666
+ * running any op that talks to the registry.
667
+ */
668
+ function assertRegistryAuthorized(entry) {
669
+ if (!entry.authRequirement)
670
+ return;
671
+ const hasUsableAuth = entry.auth && entry.auth.type !== "none"
672
+ ? (entry.auth.type === "bearer" && !!entry.auth.token) ||
673
+ (entry.auth.type === "api-key" && !!entry.auth.key)
674
+ : false;
675
+ if (hasUsableAuth)
676
+ return;
677
+ const name = entry.name ?? entry.url;
678
+ const scope = entry.authRequirement.scopes?.join(" ");
679
+ throw new adk_error_js_1.AdkError({
680
+ code: "registry_auth_required",
681
+ message: `Registry "${name}" requires authentication.`,
682
+ hint: `Run: adk registry auth ${name} --token <token>${scope ? ` (scopes: ${scope})` : ""}`,
683
+ details: {
684
+ url: entry.url,
685
+ scheme: entry.authRequirement.scheme,
686
+ realm: entry.authRequirement.realm,
687
+ authorizationServers: entry.authRequirement.authorizationServers,
688
+ scopes: entry.authRequirement.scopes,
689
+ resourceMetadataUrl: entry.authRequirement.resourceMetadataUrl,
690
+ },
691
+ });
692
+ }
444
693
  const registry = {
445
694
  async add(entry) {
446
695
  const config = await readConfig();
447
696
  const alias = entry.name ?? entry.url;
448
697
  const registries = (config.registries ?? []).filter((r) => registryDisplayName(r) !== alias);
449
- registries.push(entry);
698
+ // Probe the registry before saving. Two things fall out of the probe:
699
+ // 1. Auth challenge — 401 + WWW-Authenticate points at RFC 9728
700
+ // resource metadata; we persist it on `authRequirement` so
701
+ // subsequent ops can refuse early with a friendly message.
702
+ // 2. Proxy capability — the MCP `initialize` response may advertise
703
+ // `capabilities.registry.proxy`, which auto-populates `proxy`.
704
+ // Users who set `proxy` or `auth` explicitly on the entry always win:
705
+ // discovery only fills in blanks.
706
+ let final = entry;
707
+ let authRequirement;
708
+ const hasUsableAuth = entry.auth && entry.auth.type !== "none"
709
+ ? (entry.auth.type === "bearer" && !!entry.auth.token) ||
710
+ (entry.auth.type === "api-key" && !!entry.auth.key)
711
+ : false;
712
+ if (!hasUsableAuth) {
713
+ const fetchFn = options.fetch ?? globalThis.fetch;
714
+ const probe = await (0, mcp_client_js_1.probeRegistryAuth)(entry.url, fetchFn);
715
+ if (probe.ok === false) {
716
+ authRequirement = probe.requirement;
717
+ final = { ...final, authRequirement };
718
+ }
719
+ }
720
+ if (!entry.proxy && !authRequirement) {
721
+ try {
722
+ const probeConsumer = await (0, registry_consumer_js_1.createRegistryConsumer)({ registries: [entry], refs: [] }, { token: options.token, fetch: options.fetch });
723
+ const resolved = probeConsumer.registries()[0];
724
+ if (resolved) {
725
+ const discovered = await probeConsumer.discover(resolved.url);
726
+ if (discovered.proxy?.mode) {
727
+ final = {
728
+ ...final,
729
+ proxy: {
730
+ mode: discovered.proxy.mode,
731
+ ...(discovered.proxy.agent && { agent: discovered.proxy.agent }),
732
+ },
733
+ };
734
+ }
735
+ }
736
+ }
737
+ catch {
738
+ // Discovery is best-effort — offline, unreachable, or non-adk
739
+ // registries simply skip proxy auto-configuration.
740
+ }
741
+ }
742
+ registries.push(final);
450
743
  await writeConfig({ ...config, registries });
744
+ return authRequirement ? { authRequirement } : {};
451
745
  },
452
746
  async remove(nameOrUrl) {
453
747
  const config = await readConfig();
@@ -490,6 +784,8 @@ function createAdk(fs, options = {}) {
490
784
  existing.auth = updates.auth;
491
785
  if (updates.headers)
492
786
  existing.headers = { ...existing.headers, ...updates.headers };
787
+ if (updates.proxy !== undefined)
788
+ existing.proxy = updates.proxy;
493
789
  return existing;
494
790
  });
495
791
  if (!found)
@@ -498,18 +794,26 @@ function createAdk(fs, options = {}) {
498
794
  return true;
499
795
  },
500
796
  async browse(name, query) {
501
- const consumer = await buildConsumer(name);
502
797
  const config = await readConfig();
503
798
  const target = findRegistry(config.registries ?? [], name);
504
- const url = target ? registryUrl(target) : name;
505
- return consumer.browse(url, query);
799
+ if (target && typeof target !== "string")
800
+ assertRegistryAuthorized(target);
801
+ return callWithRefresh(name, async () => {
802
+ const consumer = await buildConsumer(name);
803
+ const url = target ? registryUrl(target) : name;
804
+ return consumer.browse(url, query);
805
+ });
506
806
  },
507
807
  async inspect(name) {
508
- const consumer = await buildConsumer(name);
509
808
  const config = await readConfig();
510
809
  const target = findRegistry(config.registries ?? [], name);
511
- const url = target ? registryUrl(target) : name;
512
- return consumer.discover(url);
810
+ if (target && typeof target !== "string")
811
+ assertRegistryAuthorized(target);
812
+ return callWithRefresh(name, async () => {
813
+ const consumer = await buildConsumer(name);
814
+ const url = target ? registryUrl(target) : name;
815
+ return consumer.discover(url);
816
+ });
513
817
  },
514
818
  async test(name) {
515
819
  const config = await readConfig();
@@ -520,9 +824,28 @@ function createAdk(fs, options = {}) {
520
824
  const results = await Promise.allSettled(targets.map(async (r) => {
521
825
  const url = registryUrl(r);
522
826
  const rName = registryDisplayName(r);
827
+ if (typeof r !== "string" && r.authRequirement) {
828
+ const hasUsableAuth = r.auth && r.auth.type !== "none"
829
+ ? (r.auth.type === "bearer" && !!r.auth.token) ||
830
+ (r.auth.type === "api-key" && !!r.auth.key)
831
+ : false;
832
+ if (!hasUsableAuth) {
833
+ return {
834
+ name: rName,
835
+ url,
836
+ status: "error",
837
+ error: `auth required — run: adk registry auth ${rName} --token <token>`,
838
+ };
839
+ }
840
+ }
523
841
  try {
524
- const consumer = await (0, registry_consumer_js_1.createRegistryConsumer)({ registries: [r] }, { token: options.token, fetch: options.fetch });
525
- const disc = await consumer.discover(url);
842
+ // Route through buildConsumer so encrypted auth/headers get
843
+ // decrypted, then use callWithRefresh so a 401 triggers the
844
+ // stored refresh token before giving up.
845
+ const disc = await callWithRefresh(rName, async () => {
846
+ const consumer = await buildConsumer(rName);
847
+ return consumer.discover(url);
848
+ });
526
849
  return { name: rName, url, status: "active", issuer: disc.issuer };
527
850
  }
528
851
  catch (err) {
@@ -534,6 +857,259 @@ function createAdk(fs, options = {}) {
534
857
  ? r.value
535
858
  : { name: "unknown", url: "unknown", status: "error", error: "unknown" });
536
859
  },
860
+ async auth(nameOrUrl, credential) {
861
+ // Encrypt the secret value up-front so the write path is uniform;
862
+ // `buildConsumer` decrypts on the read side via `decryptConfigSecrets`.
863
+ const protectedValue = "token" in credential
864
+ ? await protectSecret(credential.token)
865
+ : await protectSecret(credential.apiKey);
866
+ const updated = await updateRegistryEntry(nameOrUrl, (existing) => {
867
+ if ("token" in credential) {
868
+ existing.auth = {
869
+ type: "bearer",
870
+ token: protectedValue,
871
+ ...(credential.tokenUrl && { tokenUrl: credential.tokenUrl }),
872
+ };
873
+ }
874
+ else {
875
+ existing.auth = {
876
+ type: "api-key",
877
+ key: protectedValue,
878
+ ...(credential.header && { header: credential.header }),
879
+ };
880
+ }
881
+ delete existing.authRequirement;
882
+ });
883
+ if (updated)
884
+ await discoverProxyAfterAuth(nameOrUrl);
885
+ return updated;
886
+ },
887
+ async authLocal(nameOrUrl, opts) {
888
+ const config = await readConfig();
889
+ const target = findRegistry(config.registries ?? [], nameOrUrl);
890
+ if (!target || typeof target === "string") {
891
+ throw new adk_error_js_1.AdkError({
892
+ code: "registry_not_found",
893
+ message: `Registry not found: ${nameOrUrl}`,
894
+ hint: "Run `adk registry list` to see configured registries.",
895
+ details: { nameOrUrl },
896
+ });
897
+ }
898
+ // When the caller forces re-auth, wipe the existing credentials and
899
+ // re-probe so we know what scheme the registry wants now. Servers can
900
+ // rotate auth server metadata between runs.
901
+ if (opts?.force) {
902
+ await updateRegistryEntry(nameOrUrl, (existing) => {
903
+ delete existing.auth;
904
+ delete existing.oauth;
905
+ });
906
+ const fetchFn = options.fetch ?? globalThis.fetch;
907
+ const probe = await (0, mcp_client_js_1.probeRegistryAuth)(target.url, fetchFn);
908
+ if (probe.ok === false) {
909
+ await updateRegistryEntry(nameOrUrl, (existing) => {
910
+ existing.authRequirement = probe.requirement;
911
+ });
912
+ // Re-read so the flow below sees the fresh requirement.
913
+ const refreshed = await readConfig();
914
+ const refreshedTarget = findRegistry(refreshed.registries ?? [], nameOrUrl);
915
+ if (refreshedTarget && typeof refreshedTarget !== "string") {
916
+ Object.assign(target, refreshedTarget);
917
+ }
918
+ }
919
+ else if (probe.ok === true) {
920
+ // Registry no longer requires auth — nothing to do.
921
+ await updateRegistryEntry(nameOrUrl, (existing) => {
922
+ delete existing.authRequirement;
923
+ });
924
+ return { complete: true };
925
+ }
926
+ }
927
+ // Already authenticated — nothing to do (unless forced above).
928
+ const hasUsableAuth = target.auth && target.auth.type !== "none"
929
+ ? (target.auth.type === "bearer" && !!target.auth.token) ||
930
+ (target.auth.type === "api-key" && !!target.auth.key)
931
+ : false;
932
+ if (hasUsableAuth && !target.authRequirement) {
933
+ return { complete: true };
934
+ }
935
+ const req = target.authRequirement;
936
+ const port = options.oauthCallbackPort ?? 8919;
937
+ const timeout = opts?.timeoutMs ?? 300_000;
938
+ const displayName = target.name ?? target.url;
939
+ const { createServer } = await Promise.resolve().then(() => __importStar(require("node:http")));
940
+ // OAuth path — the registry advertised authorization servers via
941
+ // RFC 9728 protected-resource metadata. Walk the full flow:
942
+ // AS metadata → dynamic client registration → PKCE authorize →
943
+ // local callback → token exchange → persist access token.
944
+ if (req?.authorizationServers?.length) {
945
+ const authServer = req.authorizationServers[0];
946
+ const metadata = (await (0, mcp_client_js_1.discoverOAuthMetadata)(authServer)) ??
947
+ (await tryFetchOAuthMetadata(authServer));
948
+ if (!metadata) {
949
+ throw new adk_error_js_1.AdkError({
950
+ code: "registry_oauth_discovery_failed",
951
+ message: `Could not discover OAuth metadata at ${authServer}.`,
952
+ hint: "The authorization server must expose /.well-known/oauth-authorization-server.",
953
+ details: { authServer, registry: displayName },
954
+ });
955
+ }
956
+ if (!metadata.registration_endpoint) {
957
+ throw new adk_error_js_1.AdkError({
958
+ code: "registry_oauth_no_registration",
959
+ message: `Authorization server ${authServer} does not support dynamic client registration.`,
960
+ hint: `Obtain a bearer token manually, then run: adk registry auth ${displayName} --token <token>`,
961
+ details: { authServer, registry: displayName },
962
+ });
963
+ }
964
+ const redirectUri = `http://localhost:${port}/callback`;
965
+ const registration = await (0, mcp_client_js_1.dynamicClientRegistration)(metadata.registration_endpoint, {
966
+ clientName: options.oauthClientName ?? "adk",
967
+ redirectUris: [redirectUri],
968
+ grantTypes: ["authorization_code"],
969
+ });
970
+ const state = crypto.randomUUID();
971
+ const { url: authorizeUrl, codeVerifier } = await (0, mcp_client_js_1.buildOAuthAuthorizeUrl)({
972
+ authorizationEndpoint: metadata.authorization_endpoint,
973
+ clientId: registration.clientId,
974
+ redirectUri,
975
+ scopes: req.scopes,
976
+ state,
977
+ });
978
+ return new Promise((resolve, reject) => {
979
+ const server = createServer(async (reqIn, resOut) => {
980
+ const reqUrl = new URL(reqIn.url ?? "/", `http://localhost:${port}`);
981
+ if (reqUrl.pathname !== "/callback") {
982
+ resOut.writeHead(404);
983
+ resOut.end();
984
+ return;
985
+ }
986
+ const code = reqUrl.searchParams.get("code");
987
+ const returnedState = reqUrl.searchParams.get("state");
988
+ if (!code || returnedState !== state) {
989
+ const error = reqUrl.searchParams.get("error") ?? "missing code/state";
990
+ resOut.writeHead(400, { "Content-Type": "text/html" });
991
+ resOut.end(`<h1>Error</h1><p>${esc(error)}</p>`);
992
+ server.close();
993
+ reject(new adk_error_js_1.AdkError({
994
+ code: "registry_oauth_denied",
995
+ message: `OAuth callback rejected: ${error}`,
996
+ hint: "Retry `adk registry auth` and complete the browser consent.",
997
+ details: { registry: displayName, error },
998
+ }));
999
+ return;
1000
+ }
1001
+ try {
1002
+ const tokens = await (0, mcp_client_js_1.exchangeCodeForTokens)(metadata.token_endpoint, {
1003
+ code,
1004
+ codeVerifier,
1005
+ clientId: registration.clientId,
1006
+ clientSecret: registration.clientSecret,
1007
+ redirectUri,
1008
+ });
1009
+ const expiresAt = tokens.expiresIn
1010
+ ? new Date(Date.now() + tokens.expiresIn * 1000).toISOString()
1011
+ : undefined;
1012
+ const encToken = await protectSecret(tokens.accessToken);
1013
+ const encRefresh = tokens.refreshToken
1014
+ ? await protectSecret(tokens.refreshToken)
1015
+ : undefined;
1016
+ const encClientSecret = registration.clientSecret
1017
+ ? await protectSecret(registration.clientSecret)
1018
+ : undefined;
1019
+ await updateRegistryEntry(displayName, (existing) => {
1020
+ existing.auth = { type: "bearer", token: encToken };
1021
+ existing.oauth = {
1022
+ tokenEndpoint: metadata.token_endpoint,
1023
+ clientId: registration.clientId,
1024
+ ...(encClientSecret && { clientSecret: encClientSecret }),
1025
+ ...(encRefresh && { refreshToken: encRefresh }),
1026
+ ...(expiresAt && { expiresAt }),
1027
+ ...(req.scopes?.length && { scopes: req.scopes }),
1028
+ };
1029
+ delete existing.authRequirement;
1030
+ });
1031
+ await discoverProxyAfterAuth(displayName);
1032
+ resOut.writeHead(200, { "Content-Type": "text/html" });
1033
+ resOut.end(renderAuthSuccess(displayName));
1034
+ server.close();
1035
+ resolve({ complete: true });
1036
+ }
1037
+ catch (err) {
1038
+ resOut.writeHead(500, { "Content-Type": "text/html" });
1039
+ resOut.end(`<h1>Error</h1><p>${esc(err instanceof Error ? err.message : String(err))}</p>`);
1040
+ server.close();
1041
+ reject(err);
1042
+ }
1043
+ });
1044
+ server.listen(port, () => {
1045
+ opts?.onAuthorizeUrl?.(authorizeUrl);
1046
+ });
1047
+ const timer = setTimeout(() => {
1048
+ server.close();
1049
+ reject(new Error("OAuth callback timed out"));
1050
+ }, timeout);
1051
+ server.on("close", () => clearTimeout(timer));
1052
+ });
1053
+ }
1054
+ // No OAuth metadata — serve a local HTTPS form asking for a token.
1055
+ // Used when the registry returned 401 without pointing at an AS, or
1056
+ // when the caller simply wants to paste a pre-issued token.
1057
+ const fields = [
1058
+ {
1059
+ name: "token",
1060
+ label: "Bearer token",
1061
+ description: req?.realm
1062
+ ? `Token for realm "${req.realm}"`
1063
+ : "Token sent as `Authorization: Bearer <token>`.",
1064
+ secret: true,
1065
+ },
1066
+ ];
1067
+ return new Promise((resolve, reject) => {
1068
+ const server = createServer(async (reqIn, resOut) => {
1069
+ const reqUrl = new URL(reqIn.url ?? "/", `http://localhost:${port}`);
1070
+ if (reqIn.method === "GET" && reqUrl.pathname === "/auth") {
1071
+ resOut.writeHead(200, { "Content-Type": "text/html" });
1072
+ resOut.end(renderCredentialForm(displayName, fields));
1073
+ return;
1074
+ }
1075
+ if (reqIn.method === "POST" && reqUrl.pathname === "/auth") {
1076
+ const chunks = [];
1077
+ for await (const chunk of reqIn)
1078
+ chunks.push(chunk);
1079
+ const body = Buffer.concat(chunks).toString();
1080
+ const params = new URLSearchParams(body);
1081
+ const token = params.get("token");
1082
+ if (!token) {
1083
+ resOut.writeHead(200, { "Content-Type": "text/html" });
1084
+ resOut.end(renderCredentialForm(displayName, fields, "Token is required."));
1085
+ return;
1086
+ }
1087
+ try {
1088
+ await registry.auth(displayName, { token });
1089
+ resOut.writeHead(200, { "Content-Type": "text/html" });
1090
+ resOut.end(renderAuthSuccess(displayName));
1091
+ server.close();
1092
+ resolve({ complete: true });
1093
+ }
1094
+ catch (err) {
1095
+ resOut.writeHead(500, { "Content-Type": "text/html" });
1096
+ resOut.end(renderCredentialForm(displayName, fields, err instanceof Error ? err.message : String(err)));
1097
+ }
1098
+ return;
1099
+ }
1100
+ resOut.writeHead(404);
1101
+ resOut.end();
1102
+ });
1103
+ server.listen(port, () => {
1104
+ opts?.onAuthorizeUrl?.(`http://localhost:${port}/auth`);
1105
+ });
1106
+ const timer = setTimeout(() => {
1107
+ server.close();
1108
+ reject(new Error("Auth timed out"));
1109
+ }, timeout);
1110
+ server.on("close", () => clearTimeout(timer));
1111
+ });
1112
+ },
537
1113
  };
538
1114
  // ==========================================
539
1115
  // Ref API
@@ -787,6 +1363,12 @@ function createAdk(fs, options = {}) {
787
1363
  const entry = findRef(config.refs ?? [], name);
788
1364
  if (!entry)
789
1365
  throw new Error(`Ref "${name}" not found`);
1366
+ // Registry-proxied refs: ask the remote @config for state (secrets live
1367
+ // server-side so local inspection would always return "missing").
1368
+ const proxy = await resolveProxyForRef(entry);
1369
+ if (proxy) {
1370
+ return forwardRefOpToProxy(proxy.reg, proxy.agent, "auth-status", { name });
1371
+ }
790
1372
  let security = null;
791
1373
  try {
792
1374
  const consumer = await buildConsumerForRef(entry);
@@ -892,6 +1474,23 @@ function createAdk(fs, options = {}) {
892
1474
  const entry = findRef(config.refs ?? [], name);
893
1475
  if (!entry)
894
1476
  throw new Error(`Ref "${name}" not found`);
1477
+ // Registry-proxied auth: forward the start-of-flow to the remote @config
1478
+ // agent. The registry owns the client_id/secret and returns an authorize
1479
+ // URL pointing at the registry's OAuth callback domain, so the user
1480
+ // completes the flow against the registry instead of localhost.
1481
+ const proxy = await resolveProxyForRef(entry, { preferLocal: opts?.preferLocal });
1482
+ if (proxy) {
1483
+ const params = { name };
1484
+ if (opts?.apiKey !== undefined)
1485
+ params.apiKey = opts.apiKey;
1486
+ if (opts?.credentials)
1487
+ params.credentials = opts.credentials;
1488
+ if (opts?.scopes)
1489
+ params.scopes = opts.scopes;
1490
+ if (opts?.stateContext)
1491
+ params.stateContext = opts.stateContext;
1492
+ return forwardRefOpToProxy(proxy.reg, proxy.agent, "auth", params);
1493
+ }
895
1494
  const status = await ref.authStatus(name);
896
1495
  const security = status.security;
897
1496
  const resolve = options.resolveCredentials;
@@ -1112,14 +1711,30 @@ function createAdk(fs, options = {}) {
1112
1711
  return { type: security.type, complete: false };
1113
1712
  },
1114
1713
  async authLocal(name, opts) {
1714
+ // `ref.auth` is already proxy-aware — for proxied refs it returns
1715
+ // the authorizeUrl that the registry minted against its own
1716
+ // callback domain. Everything below is identical for local and
1717
+ // proxied refs except the last step (polling for the callback),
1718
+ // which only makes sense when we own the redirect URI.
1115
1719
  const result = await ref.auth(name);
1116
1720
  if (result.complete)
1117
1721
  return { complete: true };
1722
+ const config = await readConfig();
1723
+ const entry = findRef(config.refs ?? [], name);
1724
+ const proxy = entry ? await resolveProxyForRef(entry) : null;
1118
1725
  const port = options.oauthCallbackPort ?? 8919;
1119
1726
  const timeout = opts?.timeoutMs ?? 300_000;
1120
1727
  const { createServer } = await Promise.resolve().then(() => __importStar(require("node:http")));
1121
- // API key / HTTP auth — serve a local credential form
1728
+ // API key / HTTP auth — local credential form.
1729
+ //
1730
+ // We refuse to serve the form for a proxied ref: the registry
1731
+ // owns the credential store, so the user needs to submit via
1732
+ // whatever UI the registry exposes. Supporting this through the
1733
+ // proxy would need a remote form endpoint — out of scope here.
1122
1734
  if (result.fields && result.fields.length > 0 && result.type !== "oauth2") {
1735
+ if (proxy) {
1736
+ throw new Error(`Ref "${name}" is sourced from a proxied registry; submit credentials through ${proxy.agent} instead of a local form.`);
1737
+ }
1123
1738
  return new Promise((resolve, reject) => {
1124
1739
  const server = createServer(async (req, res) => {
1125
1740
  const reqUrl = new URL(req.url ?? "/", `http://localhost:${port}`);
@@ -1174,13 +1789,20 @@ function createAdk(fs, options = {}) {
1174
1789
  server.on("close", () => clearTimeout(timer));
1175
1790
  });
1176
1791
  }
1177
- // OAuth2 — open authorize URL and wait for callback
1792
+ // OAuth2 — hand the authorize URL to the caller.
1178
1793
  if (result.type !== "oauth2" || !result.authorizeUrl) {
1179
1794
  throw new Error(`authLocal cannot handle auth type: ${result.type}`);
1180
1795
  }
1181
1796
  if (opts?.onAuthorizeUrl) {
1182
1797
  opts.onAuthorizeUrl(result.authorizeUrl);
1183
1798
  }
1799
+ // Proxied refs: the registry owns the callback endpoint, so there's
1800
+ // nothing to poll here. Callers poll `ref.authStatus` on their own
1801
+ // schedule once the user finishes the remote consent screen.
1802
+ if (proxy)
1803
+ return { complete: false };
1804
+ // Local refs: spin up the callback server on oauthCallbackPort and
1805
+ // block until the OAuth provider redirects back.
1184
1806
  return new Promise((resolve, reject) => {
1185
1807
  const server = createServer(async (req, res) => {
1186
1808
  const reqUrl = new URL(req.url ?? "/", `http://localhost:${port}`);
@@ -1219,6 +1841,14 @@ function createAdk(fs, options = {}) {
1219
1841
  });
1220
1842
  },
1221
1843
  async refreshToken(name) {
1844
+ // Registry-proxied refs: the remote @config holds the refresh_token.
1845
+ const entryForProxy = await ref.get(name);
1846
+ if (entryForProxy) {
1847
+ const proxy = await resolveProxyForRef(entryForProxy);
1848
+ if (proxy) {
1849
+ return forwardRefOpToProxy(proxy.reg, proxy.agent, "refresh-token", { name });
1850
+ }
1851
+ }
1222
1852
  // Read stored refresh_token
1223
1853
  const refreshToken = await readRefSecret(name, "refresh_token");
1224
1854
  if (!refreshToken)
@@ -1293,32 +1923,6 @@ function createAdk(fs, options = {}) {
1293
1923
  catch { /* state wasn't base64 JSON — legacy format */ }
1294
1924
  return { refName: pending.refName, complete: true, stateContext };
1295
1925
  }
1296
- // ==========================================
1297
- // Proxy API
1298
- // ==========================================
1299
- const proxy = {
1300
- async add(entry) {
1301
- const config = await readConfig();
1302
- const proxies = (config.proxies ?? []).filter((p) => p.name !== entry.name);
1303
- proxies.push(entry);
1304
- await writeConfig({ ...config, proxies });
1305
- },
1306
- async remove(name) {
1307
- const config = await readConfig();
1308
- if (!config.proxies?.length)
1309
- return false;
1310
- const before = config.proxies.length;
1311
- const proxies = config.proxies.filter((p) => p.name !== name);
1312
- if (proxies.length === before)
1313
- return false;
1314
- await writeConfig({ ...config, proxies });
1315
- return true;
1316
- },
1317
- async list() {
1318
- const config = await readConfig();
1319
- return config.proxies ?? [];
1320
- },
1321
- };
1322
- return { proxy, registry, ref, readConfig, writeConfig, handleCallback };
1926
+ return { registry, ref, readConfig, writeConfig, handleCallback };
1323
1927
  }
1324
1928
  //# sourceMappingURL=config-store.js.map