@slashfi/agents-sdk 0.76.0 → 0.77.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adk-tools.d.ts +2 -2
- package/dist/adk-tools.d.ts.map +1 -1
- package/dist/adk-tools.js +9 -18
- package/dist/adk-tools.js.map +1 -1
- package/dist/adk.js +190 -120
- package/dist/adk.js.map +1 -1
- package/dist/agent-definitions/config.d.ts.map +1 -1
- package/dist/agent-definitions/config.js +12 -14
- package/dist/agent-definitions/config.js.map +1 -1
- package/dist/cjs/adk-tools.js +9 -18
- package/dist/cjs/adk-tools.js.map +1 -1
- package/dist/cjs/agent-definitions/config.js +12 -14
- package/dist/cjs/agent-definitions/config.js.map +1 -1
- package/dist/cjs/config-store.js +527 -30
- package/dist/cjs/config-store.js.map +1 -1
- package/dist/cjs/define-config.js +5 -7
- package/dist/cjs/define-config.js.map +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/materialize.js +1 -1
- package/dist/cjs/materialize.js.map +1 -1
- package/dist/cjs/mcp-client.js +98 -0
- package/dist/cjs/mcp-client.js.map +1 -1
- package/dist/cjs/registry-consumer.js +69 -11
- package/dist/cjs/registry-consumer.js.map +1 -1
- package/dist/config-store.d.ts +39 -4
- package/dist/config-store.d.ts.map +1 -1
- package/dist/config-store.js +528 -31
- package/dist/config-store.js.map +1 -1
- package/dist/define-config.d.ts +65 -18
- package/dist/define-config.d.ts.map +1 -1
- package/dist/define-config.js +5 -7
- package/dist/define-config.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/materialize.js +1 -1
- package/dist/materialize.js.map +1 -1
- package/dist/mcp-client.d.ts +44 -0
- package/dist/mcp-client.d.ts.map +1 -1
- package/dist/mcp-client.js +95 -0
- package/dist/mcp-client.js.map +1 -1
- package/dist/registry-consumer.d.ts +1 -1
- package/dist/registry-consumer.d.ts.map +1 -1
- package/dist/registry-consumer.js +69 -11
- package/dist/registry-consumer.js.map +1 -1
- package/dist/validate.d.ts +8 -8
- package/package.json +1 -1
- package/src/adk-tools.ts +11 -18
- package/src/adk.ts +78 -11
- package/src/agent-definitions/config.ts +15 -16
- package/src/config-store.test.ts +212 -0
- package/src/config-store.ts +615 -37
- package/src/consumer.test.ts +7 -7
- package/src/define-config.ts +69 -20
- package/src/index.ts +1 -0
- package/src/materialize.ts +1 -1
- package/src/mcp-client.ts +121 -0
- package/src/ref-naming.test.ts +115 -90
- package/src/registry-consumer.ts +75 -13
package/dist/cjs/config-store.js
CHANGED
|
@@ -73,9 +73,10 @@ function refName(entry) {
|
|
|
73
73
|
function findRef(refs, name) {
|
|
74
74
|
const match = refs.find((r) => refName(r) === name);
|
|
75
75
|
if (match)
|
|
76
|
-
return match;
|
|
76
|
+
return (0, define_config_js_1.normalizeRef)(match);
|
|
77
77
|
const alt = name.startsWith("@") ? name.slice(1) : `@${name}`;
|
|
78
|
-
|
|
78
|
+
const altMatch = refs.find((r) => refName(r) === alt);
|
|
79
|
+
return altMatch ? (0, define_config_js_1.normalizeRef)(altMatch) : undefined;
|
|
79
80
|
}
|
|
80
81
|
/**
|
|
81
82
|
* Match a ref name with @ normalization (for filter/map operations).
|
|
@@ -227,7 +228,8 @@ function createAdk(fs, options = {}) {
|
|
|
227
228
|
const refs = (config.refs ?? []).map((r) => {
|
|
228
229
|
if (refName(r) !== name)
|
|
229
230
|
return r;
|
|
230
|
-
|
|
231
|
+
const normalized = (0, define_config_js_1.normalizeRef)(r);
|
|
232
|
+
return { ...normalized, config: { ...normalized.config, [key]: stored } };
|
|
231
233
|
});
|
|
232
234
|
await writeConfig({ ...config, refs });
|
|
233
235
|
}
|
|
@@ -489,7 +491,11 @@ function createAdk(fs, options = {}) {
|
|
|
489
491
|
* throw on error so callers get a result that matches the local signature.
|
|
490
492
|
*/
|
|
491
493
|
async function forwardRefOpToProxy(reg, agent, operation, params) {
|
|
492
|
-
const consumer = await buildConsumerForRef({
|
|
494
|
+
const consumer = await buildConsumerForRef({
|
|
495
|
+
ref: "",
|
|
496
|
+
name: "",
|
|
497
|
+
sourceRegistry: { url: reg.url, agentPath: agent },
|
|
498
|
+
});
|
|
493
499
|
const resolved = consumer.registries().find((r) => r.url === reg.url);
|
|
494
500
|
if (!resolved)
|
|
495
501
|
throw new Error(`Registry ${reg.url} not resolvable for proxy forwarding`);
|
|
@@ -509,16 +515,215 @@ function createAdk(fs, options = {}) {
|
|
|
509
515
|
// ==========================================
|
|
510
516
|
// Registry API
|
|
511
517
|
// ==========================================
|
|
518
|
+
/**
|
|
519
|
+
* Encrypt with `secret:` prefix when an encryption key is configured, so the
|
|
520
|
+
* value is readable by the existing `decryptConfigSecrets` path on the read
|
|
521
|
+
* side. Plaintext fallback preserves the "no key = dev mode" contract.
|
|
522
|
+
*/
|
|
523
|
+
async function protectSecret(value) {
|
|
524
|
+
if (!options.encryptionKey)
|
|
525
|
+
return value;
|
|
526
|
+
return `${SECRET_PREFIX}${await (0, crypto_js_1.encryptSecret)(value, options.encryptionKey)}`;
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Re-probe a registry with the current stored credentials to see whether it
|
|
530
|
+
* advertises `capabilities.registry.proxy` in its MCP `initialize` response,
|
|
531
|
+
* and persist the proxy config when it does. Safe to call after a successful
|
|
532
|
+
* `auth()` / `authLocal()` — on the add path we skip the proxy probe when
|
|
533
|
+
* auth is required, so this is the second chance to back-fill it.
|
|
534
|
+
*
|
|
535
|
+
* Respects explicit user config: if `proxy` is already set, we leave it
|
|
536
|
+
* alone. Any discovery failure is swallowed — proxy is an optimization,
|
|
537
|
+
* not a correctness requirement.
|
|
538
|
+
*/
|
|
539
|
+
async function discoverProxyAfterAuth(nameOrUrl) {
|
|
540
|
+
const config = await readConfig();
|
|
541
|
+
const target = findRegistry(config.registries ?? [], nameOrUrl);
|
|
542
|
+
if (!target || typeof target === "string")
|
|
543
|
+
return;
|
|
544
|
+
if (target.proxy)
|
|
545
|
+
return;
|
|
546
|
+
try {
|
|
547
|
+
const consumer = await buildConsumer(nameOrUrl);
|
|
548
|
+
const discovered = await consumer.discover(target.url);
|
|
549
|
+
if (!discovered.proxy?.mode)
|
|
550
|
+
return;
|
|
551
|
+
await updateRegistryEntry(nameOrUrl, (existing) => {
|
|
552
|
+
if (existing.proxy)
|
|
553
|
+
return;
|
|
554
|
+
existing.proxy = {
|
|
555
|
+
mode: discovered.proxy.mode,
|
|
556
|
+
...(discovered.proxy.agent && { agent: discovered.proxy.agent }),
|
|
557
|
+
};
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
catch {
|
|
561
|
+
// Proxy probe is best-effort — auth itself already succeeded.
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* Atomic read-modify-write on a registry entry by name or URL. Used by
|
|
566
|
+
* `authLocal` to persist both `auth` and `oauth` together, which `auth()`
|
|
567
|
+
* alone can't express. Returns true when the entry was found and written.
|
|
568
|
+
*/
|
|
569
|
+
async function updateRegistryEntry(nameOrUrl, mutate) {
|
|
570
|
+
const config = await readConfig();
|
|
571
|
+
if (!config.registries?.length)
|
|
572
|
+
return false;
|
|
573
|
+
let found = false;
|
|
574
|
+
const registries = config.registries.map((r) => {
|
|
575
|
+
const rName = registryDisplayName(r);
|
|
576
|
+
if (rName !== nameOrUrl && registryUrl(r) !== nameOrUrl)
|
|
577
|
+
return r;
|
|
578
|
+
found = true;
|
|
579
|
+
const existing = typeof r === "string" ? { url: r } : { ...r };
|
|
580
|
+
mutate(existing);
|
|
581
|
+
return existing;
|
|
582
|
+
});
|
|
583
|
+
if (!found)
|
|
584
|
+
return false;
|
|
585
|
+
await writeConfig({ ...config, registries });
|
|
586
|
+
return true;
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Decrypt a `secret:`-prefixed value if we hold the encryption key. Plaintext
|
|
590
|
+
* values pass through unchanged so dev configs keep working.
|
|
591
|
+
*/
|
|
592
|
+
async function revealSecret(value) {
|
|
593
|
+
if (!value)
|
|
594
|
+
return value;
|
|
595
|
+
if (!value.startsWith(SECRET_PREFIX))
|
|
596
|
+
return value;
|
|
597
|
+
if (!options.encryptionKey)
|
|
598
|
+
return undefined;
|
|
599
|
+
return (0, crypto_js_1.decryptSecret)(value.slice(SECRET_PREFIX.length), options.encryptionKey);
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* Refresh a registry's OAuth access token using the stored refresh token.
|
|
603
|
+
* Persists the new access token (encrypted) and updates `expiresAt`. If the
|
|
604
|
+
* provider rotates the refresh token, that's encrypted and stored too.
|
|
605
|
+
* Returns `true` when the refresh succeeded. Callers should catch and fall
|
|
606
|
+
* back to full re-auth on failure.
|
|
607
|
+
*/
|
|
608
|
+
async function refreshRegistryToken(nameOrUrl) {
|
|
609
|
+
const config = await readConfig();
|
|
610
|
+
const target = findRegistry(config.registries ?? [], nameOrUrl);
|
|
611
|
+
if (!target || typeof target === "string")
|
|
612
|
+
return false;
|
|
613
|
+
const oauth = target.oauth;
|
|
614
|
+
if (!oauth?.refreshToken || !oauth.tokenEndpoint || !oauth.clientId)
|
|
615
|
+
return false;
|
|
616
|
+
const refreshToken = await revealSecret(oauth.refreshToken);
|
|
617
|
+
const clientSecret = await revealSecret(oauth.clientSecret);
|
|
618
|
+
if (!refreshToken)
|
|
619
|
+
return false;
|
|
620
|
+
const refreshed = await (0, mcp_client_js_1.refreshAccessToken)(oauth.tokenEndpoint, {
|
|
621
|
+
refreshToken,
|
|
622
|
+
clientId: oauth.clientId,
|
|
623
|
+
...(clientSecret && { clientSecret }),
|
|
624
|
+
});
|
|
625
|
+
const expiresAt = refreshed.expiresIn
|
|
626
|
+
? new Date(Date.now() + refreshed.expiresIn * 1000).toISOString()
|
|
627
|
+
: undefined;
|
|
628
|
+
const encAccess = await protectSecret(refreshed.accessToken);
|
|
629
|
+
const encRefresh = refreshed.refreshToken
|
|
630
|
+
? await protectSecret(refreshed.refreshToken)
|
|
631
|
+
: undefined;
|
|
632
|
+
await updateRegistryEntry(nameOrUrl, (existing) => {
|
|
633
|
+
existing.auth = { type: "bearer", token: encAccess };
|
|
634
|
+
if (!existing.oauth)
|
|
635
|
+
return;
|
|
636
|
+
if (encRefresh)
|
|
637
|
+
existing.oauth.refreshToken = encRefresh;
|
|
638
|
+
if (expiresAt)
|
|
639
|
+
existing.oauth.expiresAt = expiresAt;
|
|
640
|
+
else
|
|
641
|
+
delete existing.oauth.expiresAt;
|
|
642
|
+
});
|
|
643
|
+
return true;
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* Run a registry op once; on 401 (`registry_auth_required`), try to refresh
|
|
647
|
+
* via the stored refresh token and retry exactly once. Any other AdkError
|
|
648
|
+
* propagates as-is.
|
|
649
|
+
*/
|
|
650
|
+
async function callWithRefresh(nameOrUrl, fn) {
|
|
651
|
+
try {
|
|
652
|
+
return await fn();
|
|
653
|
+
}
|
|
654
|
+
catch (err) {
|
|
655
|
+
if (!(err instanceof adk_error_js_1.AdkError) || err.code !== "registry_auth_required")
|
|
656
|
+
throw err;
|
|
657
|
+
let refreshed = false;
|
|
658
|
+
try {
|
|
659
|
+
refreshed = await refreshRegistryToken(nameOrUrl);
|
|
660
|
+
}
|
|
661
|
+
catch {
|
|
662
|
+
// Refresh failed — surface the original 401 below.
|
|
663
|
+
}
|
|
664
|
+
if (!refreshed)
|
|
665
|
+
throw err;
|
|
666
|
+
return fn();
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
/**
|
|
670
|
+
* Throw a typed error if the registry has a recorded auth challenge and
|
|
671
|
+
* no usable credentials on the entry. Callers should invoke this before
|
|
672
|
+
* running any op that talks to the registry.
|
|
673
|
+
*/
|
|
674
|
+
function assertRegistryAuthorized(entry) {
|
|
675
|
+
if (!entry.authRequirement)
|
|
676
|
+
return;
|
|
677
|
+
const hasUsableAuth = entry.auth && entry.auth.type !== "none"
|
|
678
|
+
? (entry.auth.type === "bearer" && !!entry.auth.token) ||
|
|
679
|
+
(entry.auth.type === "api-key" && !!entry.auth.key)
|
|
680
|
+
: false;
|
|
681
|
+
if (hasUsableAuth)
|
|
682
|
+
return;
|
|
683
|
+
const name = entry.name ?? entry.url;
|
|
684
|
+
const scope = entry.authRequirement.scopes?.join(" ");
|
|
685
|
+
throw new adk_error_js_1.AdkError({
|
|
686
|
+
code: "registry_auth_required",
|
|
687
|
+
message: `Registry "${name}" requires authentication.`,
|
|
688
|
+
hint: `Run: adk registry auth ${name} --token <token>${scope ? ` (scopes: ${scope})` : ""}`,
|
|
689
|
+
details: {
|
|
690
|
+
url: entry.url,
|
|
691
|
+
scheme: entry.authRequirement.scheme,
|
|
692
|
+
realm: entry.authRequirement.realm,
|
|
693
|
+
authorizationServers: entry.authRequirement.authorizationServers,
|
|
694
|
+
scopes: entry.authRequirement.scopes,
|
|
695
|
+
resourceMetadataUrl: entry.authRequirement.resourceMetadataUrl,
|
|
696
|
+
},
|
|
697
|
+
});
|
|
698
|
+
}
|
|
512
699
|
const registry = {
|
|
513
700
|
async add(entry) {
|
|
514
701
|
const config = await readConfig();
|
|
515
702
|
const alias = entry.name ?? entry.url;
|
|
516
703
|
const registries = (config.registries ?? []).filter((r) => registryDisplayName(r) !== alias);
|
|
517
|
-
// Probe the registry
|
|
518
|
-
//
|
|
519
|
-
//
|
|
704
|
+
// Probe the registry before saving. Two things fall out of the probe:
|
|
705
|
+
// 1. Auth challenge — 401 + WWW-Authenticate points at RFC 9728
|
|
706
|
+
// resource metadata; we persist it on `authRequirement` so
|
|
707
|
+
// subsequent ops can refuse early with a friendly message.
|
|
708
|
+
// 2. Proxy capability — the MCP `initialize` response may advertise
|
|
709
|
+
// `capabilities.registry.proxy`, which auto-populates `proxy`.
|
|
710
|
+
// Users who set `proxy` or `auth` explicitly on the entry always win:
|
|
711
|
+
// discovery only fills in blanks.
|
|
520
712
|
let final = entry;
|
|
521
|
-
|
|
713
|
+
let authRequirement;
|
|
714
|
+
const hasUsableAuth = entry.auth && entry.auth.type !== "none"
|
|
715
|
+
? (entry.auth.type === "bearer" && !!entry.auth.token) ||
|
|
716
|
+
(entry.auth.type === "api-key" && !!entry.auth.key)
|
|
717
|
+
: false;
|
|
718
|
+
if (!hasUsableAuth) {
|
|
719
|
+
const fetchFn = options.fetch ?? globalThis.fetch;
|
|
720
|
+
const probe = await (0, mcp_client_js_1.probeRegistryAuth)(entry.url, fetchFn);
|
|
721
|
+
if (probe.ok === false) {
|
|
722
|
+
authRequirement = probe.requirement;
|
|
723
|
+
final = { ...final, authRequirement };
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
if (!entry.proxy && !authRequirement) {
|
|
522
727
|
try {
|
|
523
728
|
const probeConsumer = await (0, registry_consumer_js_1.createRegistryConsumer)({ registries: [entry], refs: [] }, { token: options.token, fetch: options.fetch });
|
|
524
729
|
const resolved = probeConsumer.registries()[0];
|
|
@@ -526,7 +731,7 @@ function createAdk(fs, options = {}) {
|
|
|
526
731
|
const discovered = await probeConsumer.discover(resolved.url);
|
|
527
732
|
if (discovered.proxy?.mode) {
|
|
528
733
|
final = {
|
|
529
|
-
...
|
|
734
|
+
...final,
|
|
530
735
|
proxy: {
|
|
531
736
|
mode: discovered.proxy.mode,
|
|
532
737
|
...(discovered.proxy.agent && { agent: discovered.proxy.agent }),
|
|
@@ -542,6 +747,7 @@ function createAdk(fs, options = {}) {
|
|
|
542
747
|
}
|
|
543
748
|
registries.push(final);
|
|
544
749
|
await writeConfig({ ...config, registries });
|
|
750
|
+
return authRequirement ? { authRequirement } : {};
|
|
545
751
|
},
|
|
546
752
|
async remove(nameOrUrl) {
|
|
547
753
|
const config = await readConfig();
|
|
@@ -594,18 +800,26 @@ function createAdk(fs, options = {}) {
|
|
|
594
800
|
return true;
|
|
595
801
|
},
|
|
596
802
|
async browse(name, query) {
|
|
597
|
-
const consumer = await buildConsumer(name);
|
|
598
803
|
const config = await readConfig();
|
|
599
804
|
const target = findRegistry(config.registries ?? [], name);
|
|
600
|
-
|
|
601
|
-
|
|
805
|
+
if (target && typeof target !== "string")
|
|
806
|
+
assertRegistryAuthorized(target);
|
|
807
|
+
return callWithRefresh(name, async () => {
|
|
808
|
+
const consumer = await buildConsumer(name);
|
|
809
|
+
const url = target ? registryUrl(target) : name;
|
|
810
|
+
return consumer.browse(url, query);
|
|
811
|
+
});
|
|
602
812
|
},
|
|
603
813
|
async inspect(name) {
|
|
604
|
-
const consumer = await buildConsumer(name);
|
|
605
814
|
const config = await readConfig();
|
|
606
815
|
const target = findRegistry(config.registries ?? [], name);
|
|
607
|
-
|
|
608
|
-
|
|
816
|
+
if (target && typeof target !== "string")
|
|
817
|
+
assertRegistryAuthorized(target);
|
|
818
|
+
return callWithRefresh(name, async () => {
|
|
819
|
+
const consumer = await buildConsumer(name);
|
|
820
|
+
const url = target ? registryUrl(target) : name;
|
|
821
|
+
return consumer.discover(url);
|
|
822
|
+
});
|
|
609
823
|
},
|
|
610
824
|
async test(name) {
|
|
611
825
|
const config = await readConfig();
|
|
@@ -616,9 +830,28 @@ function createAdk(fs, options = {}) {
|
|
|
616
830
|
const results = await Promise.allSettled(targets.map(async (r) => {
|
|
617
831
|
const url = registryUrl(r);
|
|
618
832
|
const rName = registryDisplayName(r);
|
|
833
|
+
if (typeof r !== "string" && r.authRequirement) {
|
|
834
|
+
const hasUsableAuth = r.auth && r.auth.type !== "none"
|
|
835
|
+
? (r.auth.type === "bearer" && !!r.auth.token) ||
|
|
836
|
+
(r.auth.type === "api-key" && !!r.auth.key)
|
|
837
|
+
: false;
|
|
838
|
+
if (!hasUsableAuth) {
|
|
839
|
+
return {
|
|
840
|
+
name: rName,
|
|
841
|
+
url,
|
|
842
|
+
status: "error",
|
|
843
|
+
error: `auth required — run: adk registry auth ${rName} --token <token>`,
|
|
844
|
+
};
|
|
845
|
+
}
|
|
846
|
+
}
|
|
619
847
|
try {
|
|
620
|
-
|
|
621
|
-
|
|
848
|
+
// Route through buildConsumer so encrypted auth/headers get
|
|
849
|
+
// decrypted, then use callWithRefresh so a 401 triggers the
|
|
850
|
+
// stored refresh token before giving up.
|
|
851
|
+
const disc = await callWithRefresh(rName, async () => {
|
|
852
|
+
const consumer = await buildConsumer(rName);
|
|
853
|
+
return consumer.discover(url);
|
|
854
|
+
});
|
|
622
855
|
return { name: rName, url, status: "active", issuer: disc.issuer };
|
|
623
856
|
}
|
|
624
857
|
catch (err) {
|
|
@@ -630,15 +863,278 @@ function createAdk(fs, options = {}) {
|
|
|
630
863
|
? r.value
|
|
631
864
|
: { name: "unknown", url: "unknown", status: "error", error: "unknown" });
|
|
632
865
|
},
|
|
866
|
+
async auth(nameOrUrl, credential) {
|
|
867
|
+
// Encrypt the secret value up-front so the write path is uniform;
|
|
868
|
+
// `buildConsumer` decrypts on the read side via `decryptConfigSecrets`.
|
|
869
|
+
const protectedValue = "token" in credential
|
|
870
|
+
? await protectSecret(credential.token)
|
|
871
|
+
: await protectSecret(credential.apiKey);
|
|
872
|
+
const updated = await updateRegistryEntry(nameOrUrl, (existing) => {
|
|
873
|
+
if ("token" in credential) {
|
|
874
|
+
existing.auth = {
|
|
875
|
+
type: "bearer",
|
|
876
|
+
token: protectedValue,
|
|
877
|
+
...(credential.tokenUrl && { tokenUrl: credential.tokenUrl }),
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
else {
|
|
881
|
+
existing.auth = {
|
|
882
|
+
type: "api-key",
|
|
883
|
+
key: protectedValue,
|
|
884
|
+
...(credential.header && { header: credential.header }),
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
delete existing.authRequirement;
|
|
888
|
+
});
|
|
889
|
+
if (updated)
|
|
890
|
+
await discoverProxyAfterAuth(nameOrUrl);
|
|
891
|
+
return updated;
|
|
892
|
+
},
|
|
893
|
+
async authLocal(nameOrUrl, opts) {
|
|
894
|
+
const config = await readConfig();
|
|
895
|
+
const target = findRegistry(config.registries ?? [], nameOrUrl);
|
|
896
|
+
if (!target || typeof target === "string") {
|
|
897
|
+
throw new adk_error_js_1.AdkError({
|
|
898
|
+
code: "registry_not_found",
|
|
899
|
+
message: `Registry not found: ${nameOrUrl}`,
|
|
900
|
+
hint: "Run `adk registry list` to see configured registries.",
|
|
901
|
+
details: { nameOrUrl },
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
// When the caller forces re-auth, wipe the existing credentials and
|
|
905
|
+
// re-probe so we know what scheme the registry wants now. Servers can
|
|
906
|
+
// rotate auth server metadata between runs.
|
|
907
|
+
if (opts?.force) {
|
|
908
|
+
await updateRegistryEntry(nameOrUrl, (existing) => {
|
|
909
|
+
delete existing.auth;
|
|
910
|
+
delete existing.oauth;
|
|
911
|
+
});
|
|
912
|
+
const fetchFn = options.fetch ?? globalThis.fetch;
|
|
913
|
+
const probe = await (0, mcp_client_js_1.probeRegistryAuth)(target.url, fetchFn);
|
|
914
|
+
if (probe.ok === false) {
|
|
915
|
+
await updateRegistryEntry(nameOrUrl, (existing) => {
|
|
916
|
+
existing.authRequirement = probe.requirement;
|
|
917
|
+
});
|
|
918
|
+
// Re-read so the flow below sees the fresh requirement.
|
|
919
|
+
const refreshed = await readConfig();
|
|
920
|
+
const refreshedTarget = findRegistry(refreshed.registries ?? [], nameOrUrl);
|
|
921
|
+
if (refreshedTarget && typeof refreshedTarget !== "string") {
|
|
922
|
+
Object.assign(target, refreshedTarget);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
else if (probe.ok === true) {
|
|
926
|
+
// Registry no longer requires auth — nothing to do.
|
|
927
|
+
await updateRegistryEntry(nameOrUrl, (existing) => {
|
|
928
|
+
delete existing.authRequirement;
|
|
929
|
+
});
|
|
930
|
+
return { complete: true };
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
// Already authenticated — nothing to do (unless forced above).
|
|
934
|
+
const hasUsableAuth = target.auth && target.auth.type !== "none"
|
|
935
|
+
? (target.auth.type === "bearer" && !!target.auth.token) ||
|
|
936
|
+
(target.auth.type === "api-key" && !!target.auth.key)
|
|
937
|
+
: false;
|
|
938
|
+
if (hasUsableAuth && !target.authRequirement) {
|
|
939
|
+
return { complete: true };
|
|
940
|
+
}
|
|
941
|
+
const req = target.authRequirement;
|
|
942
|
+
const port = options.oauthCallbackPort ?? 8919;
|
|
943
|
+
const timeout = opts?.timeoutMs ?? 300_000;
|
|
944
|
+
const displayName = target.name ?? target.url;
|
|
945
|
+
const { createServer } = await Promise.resolve().then(() => __importStar(require("node:http")));
|
|
946
|
+
// OAuth path — the registry advertised authorization servers via
|
|
947
|
+
// RFC 9728 protected-resource metadata. Walk the full flow:
|
|
948
|
+
// AS metadata → dynamic client registration → PKCE authorize →
|
|
949
|
+
// local callback → token exchange → persist access token.
|
|
950
|
+
if (req?.authorizationServers?.length) {
|
|
951
|
+
const authServer = req.authorizationServers[0];
|
|
952
|
+
const metadata = (await (0, mcp_client_js_1.discoverOAuthMetadata)(authServer)) ??
|
|
953
|
+
(await tryFetchOAuthMetadata(authServer));
|
|
954
|
+
if (!metadata) {
|
|
955
|
+
throw new adk_error_js_1.AdkError({
|
|
956
|
+
code: "registry_oauth_discovery_failed",
|
|
957
|
+
message: `Could not discover OAuth metadata at ${authServer}.`,
|
|
958
|
+
hint: "The authorization server must expose /.well-known/oauth-authorization-server.",
|
|
959
|
+
details: { authServer, registry: displayName },
|
|
960
|
+
});
|
|
961
|
+
}
|
|
962
|
+
if (!metadata.registration_endpoint) {
|
|
963
|
+
throw new adk_error_js_1.AdkError({
|
|
964
|
+
code: "registry_oauth_no_registration",
|
|
965
|
+
message: `Authorization server ${authServer} does not support dynamic client registration.`,
|
|
966
|
+
hint: `Obtain a bearer token manually, then run: adk registry auth ${displayName} --token <token>`,
|
|
967
|
+
details: { authServer, registry: displayName },
|
|
968
|
+
});
|
|
969
|
+
}
|
|
970
|
+
const redirectUri = `http://localhost:${port}/callback`;
|
|
971
|
+
const registration = await (0, mcp_client_js_1.dynamicClientRegistration)(metadata.registration_endpoint, {
|
|
972
|
+
clientName: options.oauthClientName ?? "adk",
|
|
973
|
+
redirectUris: [redirectUri],
|
|
974
|
+
grantTypes: ["authorization_code"],
|
|
975
|
+
});
|
|
976
|
+
const state = crypto.randomUUID();
|
|
977
|
+
const { url: authorizeUrl, codeVerifier } = await (0, mcp_client_js_1.buildOAuthAuthorizeUrl)({
|
|
978
|
+
authorizationEndpoint: metadata.authorization_endpoint,
|
|
979
|
+
clientId: registration.clientId,
|
|
980
|
+
redirectUri,
|
|
981
|
+
scopes: req.scopes,
|
|
982
|
+
state,
|
|
983
|
+
});
|
|
984
|
+
return new Promise((resolve, reject) => {
|
|
985
|
+
const server = createServer(async (reqIn, resOut) => {
|
|
986
|
+
const reqUrl = new URL(reqIn.url ?? "/", `http://localhost:${port}`);
|
|
987
|
+
if (reqUrl.pathname !== "/callback") {
|
|
988
|
+
resOut.writeHead(404);
|
|
989
|
+
resOut.end();
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
const code = reqUrl.searchParams.get("code");
|
|
993
|
+
const returnedState = reqUrl.searchParams.get("state");
|
|
994
|
+
if (!code || returnedState !== state) {
|
|
995
|
+
const error = reqUrl.searchParams.get("error") ?? "missing code/state";
|
|
996
|
+
resOut.writeHead(400, { "Content-Type": "text/html" });
|
|
997
|
+
resOut.end(`<h1>Error</h1><p>${esc(error)}</p>`);
|
|
998
|
+
server.close();
|
|
999
|
+
reject(new adk_error_js_1.AdkError({
|
|
1000
|
+
code: "registry_oauth_denied",
|
|
1001
|
+
message: `OAuth callback rejected: ${error}`,
|
|
1002
|
+
hint: "Retry `adk registry auth` and complete the browser consent.",
|
|
1003
|
+
details: { registry: displayName, error },
|
|
1004
|
+
}));
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
try {
|
|
1008
|
+
const tokens = await (0, mcp_client_js_1.exchangeCodeForTokens)(metadata.token_endpoint, {
|
|
1009
|
+
code,
|
|
1010
|
+
codeVerifier,
|
|
1011
|
+
clientId: registration.clientId,
|
|
1012
|
+
clientSecret: registration.clientSecret,
|
|
1013
|
+
redirectUri,
|
|
1014
|
+
});
|
|
1015
|
+
const expiresAt = tokens.expiresIn
|
|
1016
|
+
? new Date(Date.now() + tokens.expiresIn * 1000).toISOString()
|
|
1017
|
+
: undefined;
|
|
1018
|
+
const encToken = await protectSecret(tokens.accessToken);
|
|
1019
|
+
const encRefresh = tokens.refreshToken
|
|
1020
|
+
? await protectSecret(tokens.refreshToken)
|
|
1021
|
+
: undefined;
|
|
1022
|
+
const encClientSecret = registration.clientSecret
|
|
1023
|
+
? await protectSecret(registration.clientSecret)
|
|
1024
|
+
: undefined;
|
|
1025
|
+
await updateRegistryEntry(displayName, (existing) => {
|
|
1026
|
+
existing.auth = { type: "bearer", token: encToken };
|
|
1027
|
+
existing.oauth = {
|
|
1028
|
+
tokenEndpoint: metadata.token_endpoint,
|
|
1029
|
+
clientId: registration.clientId,
|
|
1030
|
+
...(encClientSecret && { clientSecret: encClientSecret }),
|
|
1031
|
+
...(encRefresh && { refreshToken: encRefresh }),
|
|
1032
|
+
...(expiresAt && { expiresAt }),
|
|
1033
|
+
...(req.scopes?.length && { scopes: req.scopes }),
|
|
1034
|
+
};
|
|
1035
|
+
delete existing.authRequirement;
|
|
1036
|
+
});
|
|
1037
|
+
await discoverProxyAfterAuth(displayName);
|
|
1038
|
+
resOut.writeHead(200, { "Content-Type": "text/html" });
|
|
1039
|
+
resOut.end(renderAuthSuccess(displayName));
|
|
1040
|
+
server.close();
|
|
1041
|
+
resolve({ complete: true });
|
|
1042
|
+
}
|
|
1043
|
+
catch (err) {
|
|
1044
|
+
resOut.writeHead(500, { "Content-Type": "text/html" });
|
|
1045
|
+
resOut.end(`<h1>Error</h1><p>${esc(err instanceof Error ? err.message : String(err))}</p>`);
|
|
1046
|
+
server.close();
|
|
1047
|
+
reject(err);
|
|
1048
|
+
}
|
|
1049
|
+
});
|
|
1050
|
+
server.listen(port, () => {
|
|
1051
|
+
opts?.onAuthorizeUrl?.(authorizeUrl);
|
|
1052
|
+
});
|
|
1053
|
+
const timer = setTimeout(() => {
|
|
1054
|
+
server.close();
|
|
1055
|
+
reject(new Error("OAuth callback timed out"));
|
|
1056
|
+
}, timeout);
|
|
1057
|
+
server.on("close", () => clearTimeout(timer));
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
1060
|
+
// No OAuth metadata — serve a local HTTPS form asking for a token.
|
|
1061
|
+
// Used when the registry returned 401 without pointing at an AS, or
|
|
1062
|
+
// when the caller simply wants to paste a pre-issued token.
|
|
1063
|
+
const fields = [
|
|
1064
|
+
{
|
|
1065
|
+
name: "token",
|
|
1066
|
+
label: "Bearer token",
|
|
1067
|
+
description: req?.realm
|
|
1068
|
+
? `Token for realm "${req.realm}"`
|
|
1069
|
+
: "Token sent as `Authorization: Bearer <token>`.",
|
|
1070
|
+
secret: true,
|
|
1071
|
+
},
|
|
1072
|
+
];
|
|
1073
|
+
return new Promise((resolve, reject) => {
|
|
1074
|
+
const server = createServer(async (reqIn, resOut) => {
|
|
1075
|
+
const reqUrl = new URL(reqIn.url ?? "/", `http://localhost:${port}`);
|
|
1076
|
+
if (reqIn.method === "GET" && reqUrl.pathname === "/auth") {
|
|
1077
|
+
resOut.writeHead(200, { "Content-Type": "text/html" });
|
|
1078
|
+
resOut.end(renderCredentialForm(displayName, fields));
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1081
|
+
if (reqIn.method === "POST" && reqUrl.pathname === "/auth") {
|
|
1082
|
+
const chunks = [];
|
|
1083
|
+
for await (const chunk of reqIn)
|
|
1084
|
+
chunks.push(chunk);
|
|
1085
|
+
const body = Buffer.concat(chunks).toString();
|
|
1086
|
+
const params = new URLSearchParams(body);
|
|
1087
|
+
const token = params.get("token");
|
|
1088
|
+
if (!token) {
|
|
1089
|
+
resOut.writeHead(200, { "Content-Type": "text/html" });
|
|
1090
|
+
resOut.end(renderCredentialForm(displayName, fields, "Token is required."));
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
try {
|
|
1094
|
+
await registry.auth(displayName, { token });
|
|
1095
|
+
resOut.writeHead(200, { "Content-Type": "text/html" });
|
|
1096
|
+
resOut.end(renderAuthSuccess(displayName));
|
|
1097
|
+
server.close();
|
|
1098
|
+
resolve({ complete: true });
|
|
1099
|
+
}
|
|
1100
|
+
catch (err) {
|
|
1101
|
+
resOut.writeHead(500, { "Content-Type": "text/html" });
|
|
1102
|
+
resOut.end(renderCredentialForm(displayName, fields, err instanceof Error ? err.message : String(err)));
|
|
1103
|
+
}
|
|
1104
|
+
return;
|
|
1105
|
+
}
|
|
1106
|
+
resOut.writeHead(404);
|
|
1107
|
+
resOut.end();
|
|
1108
|
+
});
|
|
1109
|
+
server.listen(port, () => {
|
|
1110
|
+
opts?.onAuthorizeUrl?.(`http://localhost:${port}/auth`);
|
|
1111
|
+
});
|
|
1112
|
+
const timer = setTimeout(() => {
|
|
1113
|
+
server.close();
|
|
1114
|
+
reject(new Error("Auth timed out"));
|
|
1115
|
+
}, timeout);
|
|
1116
|
+
server.on("close", () => clearTimeout(timer));
|
|
1117
|
+
});
|
|
1118
|
+
},
|
|
633
1119
|
};
|
|
634
1120
|
// ==========================================
|
|
635
1121
|
// Ref API
|
|
636
1122
|
// ==========================================
|
|
637
1123
|
const ref = {
|
|
638
|
-
async add(
|
|
1124
|
+
async add(entryInput) {
|
|
639
1125
|
let security = null;
|
|
640
1126
|
const config = await readConfig();
|
|
641
1127
|
const hasRegistries = (config.registries ?? []).length > 0;
|
|
1128
|
+
const name = entryInput.name ?? entryInput.ref;
|
|
1129
|
+
let entry = { ...entryInput, name };
|
|
1130
|
+
if ((config.refs ?? []).some((r) => refNameMatches(r, name))) {
|
|
1131
|
+
throw new adk_error_js_1.AdkError({
|
|
1132
|
+
code: "REF_INVALID",
|
|
1133
|
+
message: `Cannot add ref "${entry.ref}" as "${name}": a ref with that name already exists`,
|
|
1134
|
+
hint: "Choose a different name, or remove/update the existing ref first.",
|
|
1135
|
+
details: { ref: entry.ref, name },
|
|
1136
|
+
});
|
|
1137
|
+
}
|
|
642
1138
|
// Auto-infer scheme from context
|
|
643
1139
|
if (!entry.scheme) {
|
|
644
1140
|
if (entry.sourceRegistry?.url) {
|
|
@@ -731,9 +1227,7 @@ function createAdk(fs, options = {}) {
|
|
|
731
1227
|
});
|
|
732
1228
|
}
|
|
733
1229
|
}
|
|
734
|
-
const
|
|
735
|
-
const refs = (config.refs ?? []).filter((r) => refName(r) !== name);
|
|
736
|
-
refs.push(entry);
|
|
1230
|
+
const refs = [...(config.refs ?? []), entry];
|
|
737
1231
|
await writeConfig({ ...config, refs });
|
|
738
1232
|
return { security };
|
|
739
1233
|
},
|
|
@@ -768,16 +1262,18 @@ function createAdk(fs, options = {}) {
|
|
|
768
1262
|
const updated = { ...r };
|
|
769
1263
|
if (updates.url)
|
|
770
1264
|
updated.url = updates.url;
|
|
771
|
-
// Rename: prefer `name`, fall back to legacy `as`. When the
|
|
772
|
-
// caller passes `name`, clear the legacy `as` so the stored
|
|
773
|
-
// entry has one source of truth.
|
|
774
1265
|
if (updates.name !== undefined) {
|
|
1266
|
+
const duplicate = config.refs?.some((candidate) => !refNameMatches(candidate, name) &&
|
|
1267
|
+
refNameMatches(candidate, updates.name));
|
|
1268
|
+
if (duplicate) {
|
|
1269
|
+
throw new adk_error_js_1.AdkError({
|
|
1270
|
+
code: "REF_INVALID",
|
|
1271
|
+
message: `Cannot rename ref "${name}" to "${updates.name}": a ref with that name already exists`,
|
|
1272
|
+
hint: "Choose a different name, or remove/update the existing ref first.",
|
|
1273
|
+
details: { name, newName: updates.name },
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
775
1276
|
updated.name = updates.name;
|
|
776
|
-
if (updated.as !== undefined)
|
|
777
|
-
updated.as = undefined;
|
|
778
|
-
}
|
|
779
|
-
else if (updates.as !== undefined) {
|
|
780
|
-
updated.as = updates.as;
|
|
781
1277
|
}
|
|
782
1278
|
if (updates.scheme)
|
|
783
1279
|
updated.scheme = updates.scheme;
|
|
@@ -1188,7 +1684,8 @@ function createAdk(fs, options = {}) {
|
|
|
1188
1684
|
// so callers can include extra context (tenant/user IDs).
|
|
1189
1685
|
const statePayload = {
|
|
1190
1686
|
...opts?.stateContext,
|
|
1191
|
-
ref:
|
|
1687
|
+
ref: entry.ref,
|
|
1688
|
+
name,
|
|
1192
1689
|
ts: Date.now(),
|
|
1193
1690
|
};
|
|
1194
1691
|
const state = btoa(JSON.stringify(statePayload));
|