@slashfi/agents-sdk 0.90.2 → 0.90.5

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.
@@ -480,6 +480,159 @@ function createAdk(fs, options = {}) {
480
480
  });
481
481
  };
482
482
  }
483
+ /**
484
+ * Call-time credential lookup: stored ref config first, then the host
485
+ * `resolveCredentials` callback. Does not persist resolved values.
486
+ */
487
+ async function resolveCallCredential(ctx, field) {
488
+ const stored = await readRefSecret(ctx.name, field);
489
+ if (stored)
490
+ return stored;
491
+ return makeTryResolve(ctx)(field);
492
+ }
493
+ const CALL_BEARER_FIELDS = ["access_token", "api_key", "token"];
494
+ function isBearerAuthField(field) {
495
+ return CALL_BEARER_FIELDS.includes(field);
496
+ }
497
+ /** Legacy cache entries may omit `outbound`; these are never call-time creds. */
498
+ const LEGACY_CONNECT_ONLY_FIELDS = new Set([
499
+ "client_id",
500
+ "client_secret",
501
+ "refresh_token",
502
+ ]);
503
+ function isCallOutboundAuthField(field, info) {
504
+ if (info.outbound === false)
505
+ return false;
506
+ if (info.outbound === true)
507
+ return true;
508
+ return !LEGACY_CONNECT_ONLY_FIELDS.has(field);
509
+ }
510
+ function readRegistryDeclaredAuthFields(security) {
511
+ if (!security || typeof security !== "object")
512
+ return undefined;
513
+ const raw = security.authFields;
514
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
515
+ return undefined;
516
+ const out = {};
517
+ for (const [field, meta] of Object.entries(raw)) {
518
+ if (!meta || typeof meta !== "object" || Array.isArray(meta))
519
+ continue;
520
+ const m = meta;
521
+ if (typeof m.required !== "boolean" || typeof m.automated !== "boolean") {
522
+ continue;
523
+ }
524
+ out[field] = { required: m.required, automated: m.automated };
525
+ if (typeof m.outbound === "boolean") {
526
+ out[field].outbound = m.outbound;
527
+ }
528
+ }
529
+ return Object.keys(out).length > 0 ? out : undefined;
530
+ }
531
+ async function mergeRegistryDeclaredAuthFields(fields, declared, canResolve, configKeys, refConfig) {
532
+ if (!declared)
533
+ return fields;
534
+ const next = {};
535
+ for (const [field, meta] of Object.entries(declared)) {
536
+ next[field] = {
537
+ required: meta.required,
538
+ automated: meta.automated,
539
+ present: configKeys.includes(field) || hasCredentialField(refConfig, field),
540
+ resolvable: await canResolve(field),
541
+ ...(meta.format && { format: meta.format }),
542
+ ...(meta.parts && { parts: meta.parts }),
543
+ ...(meta.outbound === false && { outbound: false }),
544
+ };
545
+ }
546
+ return next;
547
+ }
548
+ function bearerFieldSatisfied(accessToken, refConfig, field) {
549
+ if (accessToken)
550
+ return true;
551
+ return hasCredentialField(refConfig, field);
552
+ }
553
+ function fallbackCallAuthFields() {
554
+ return {
555
+ access_token: { required: true, automated: true },
556
+ api_key: { required: false, automated: true },
557
+ token: { required: false, automated: true },
558
+ };
559
+ }
560
+ function headerFieldSatisfied(headers, field) {
561
+ const wanted = normalizeCredentialKey(field);
562
+ return Object.keys(headers).some((key) => normalizeCredentialKey(key) === wanted);
563
+ }
564
+ function resolveHeaderNameForField(field, refConfig) {
565
+ const wanted = normalizeCredentialKey(field);
566
+ const configHeaders = refConfig.headers;
567
+ if (configHeaders &&
568
+ typeof configHeaders === "object" &&
569
+ !Array.isArray(configHeaders)) {
570
+ for (const key of Object.keys(configHeaders)) {
571
+ if (normalizeCredentialKey(key) === wanted)
572
+ return key;
573
+ }
574
+ }
575
+ // x_api_key → X-API-KEY (registry codegen declares the canonical name;
576
+ // env-resolved keys use the normalized storage field name).
577
+ return field
578
+ .split("_")
579
+ .filter(Boolean)
580
+ .map((part) => part.toUpperCase())
581
+ .join("-");
582
+ }
583
+ /**
584
+ * Supplement call-time credentials from `resolveCredentials` when they
585
+ * are not already present in consumer-config. Stored values and config
586
+ * headers win — this only fills gaps. Walks cached `authFields` as the
587
+ * source of truth (registry-declared when auth-status has run).
588
+ */
589
+ async function resolveAllCallCredentials(opts) {
590
+ if (!options.resolveCredentials) {
591
+ return {
592
+ accessToken: opts.accessToken,
593
+ resolvedHeaders: opts.resolvedHeaders,
594
+ };
595
+ }
596
+ let accessToken = opts.accessToken;
597
+ let resolvedHeaders = opts.resolvedHeaders;
598
+ const { ctx, refConfig } = opts;
599
+ const cache = await readRegistryCache();
600
+ const authFields = cache.refs[ctx.name]?.authFields ?? fallbackCallAuthFields();
601
+ for (const [field, info] of Object.entries(authFields)) {
602
+ if (!isCallOutboundAuthField(field, info))
603
+ continue;
604
+ if (!info.required && !info.automated)
605
+ continue;
606
+ if (isBearerAuthField(field)) {
607
+ if (bearerFieldSatisfied(accessToken, refConfig, field))
608
+ continue;
609
+ const value = await resolveCallCredential(ctx, field);
610
+ if (value)
611
+ accessToken = accessToken ?? value;
612
+ continue;
613
+ }
614
+ if (hasCredentialField(refConfig, field))
615
+ continue;
616
+ if (resolvedHeaders && headerFieldSatisfied(resolvedHeaders, field)) {
617
+ continue;
618
+ }
619
+ const value = await resolveCallCredential(ctx, field);
620
+ if (!value)
621
+ continue;
622
+ resolvedHeaders = resolvedHeaders ?? {};
623
+ if (!headerFieldSatisfied(resolvedHeaders, field)) {
624
+ resolvedHeaders[resolveHeaderNameForField(field, refConfig)] = value;
625
+ }
626
+ }
627
+ if (!accessToken) {
628
+ const username = await resolveCallCredential(ctx, "username");
629
+ const password = await resolveCallCredential(ctx, "password");
630
+ if (username && password) {
631
+ accessToken = btoa(`${username}:${password}`);
632
+ }
633
+ }
634
+ return { accessToken, resolvedHeaders };
635
+ }
483
636
  /**
484
637
  * Resolve OAuth client credentials (client_id + client_secret) for a
485
638
  * ref. Walks: `resolveCredentials` callback → per-ref VCS storage.
@@ -671,6 +824,79 @@ function createAdk(fs, options = {}) {
671
824
  return null;
672
825
  }
673
826
  }
827
+ /**
828
+ * Resolve OAuth server metadata from a registry security scheme.
829
+ * Shared by `ref.auth` and `ref.refreshToken` so both paths discover
830
+ * token endpoints the same way — explicit manifest URLs, RFC 8414
831
+ * discovery via `discoveryUrl`, authorization-server discovery, and
832
+ * finally the MCP upstream URL for redirect-mode agents.
833
+ */
834
+ async function resolveOAuthMetadataFromSecurity(security, opts) {
835
+ if (!security || security.type !== "oauth2")
836
+ return null;
837
+ const securityExt = security;
838
+ const authCodeFlow = securityExt.flows?.authorizationCode;
839
+ const explicitEndpoint = authCodeFlow?.refreshUrl ?? authCodeFlow?.tokenUrl;
840
+ if (explicitEndpoint) {
841
+ const flowScopes = authCodeFlow
842
+ ?.scopes;
843
+ const authUrl = authCodeFlow?.authorizationUrl;
844
+ return {
845
+ issuer: authUrl
846
+ ? new URL(authUrl).origin
847
+ : new URL(explicitEndpoint).origin,
848
+ authorization_endpoint: authUrl ?? explicitEndpoint,
849
+ token_endpoint: explicitEndpoint,
850
+ scopes_supported: flowScopes ? Object.keys(flowScopes) : undefined,
851
+ };
852
+ }
853
+ if (securityExt.discoveryUrl) {
854
+ const fromDiscovery = (await tryFetchOAuthMetadata(securityExt.discoveryUrl)) ??
855
+ (await (0, mcp_client_js_1.discoverOAuthMetadata)(securityExt.discoveryUrl));
856
+ if (fromDiscovery)
857
+ return fromDiscovery;
858
+ }
859
+ const authUrl = authCodeFlow?.authorizationUrl;
860
+ if (authUrl) {
861
+ let metadata = await tryFetchOAuthMetadata(authUrl);
862
+ if (!metadata) {
863
+ metadata = await (0, mcp_client_js_1.discoverOAuthMetadata)(new URL(authUrl).origin);
864
+ }
865
+ if (metadata)
866
+ return metadata;
867
+ }
868
+ const serverUrl = opts?.serverUrl;
869
+ if (serverUrl) {
870
+ let metadata = await (0, mcp_client_js_1.discoverOAuthMetadata)(serverUrl);
871
+ if (!metadata) {
872
+ metadata = await (0, mcp_client_js_1.discoverOAuthMetadata)(serverUrl.replace(/\/(mcp|sse)$/, ""));
873
+ }
874
+ if (metadata)
875
+ return metadata;
876
+ }
877
+ return null;
878
+ }
879
+ function resolveClientAuthMethod(security, metadata) {
880
+ const flowAuth = security.flows?.authorizationCode?.clientAuth;
881
+ if (flowAuth)
882
+ return flowAuth;
883
+ const supported = metadata?.token_endpoint_auth_methods_supported;
884
+ if (supported?.length === 1 && supported[0] === "client_secret_basic") {
885
+ return "client_secret_basic";
886
+ }
887
+ const tokenEndpoint = metadata?.token_endpoint;
888
+ if (tokenEndpoint) {
889
+ try {
890
+ if (new URL(tokenEndpoint).hostname === "api.x.com") {
891
+ return "client_secret_basic";
892
+ }
893
+ }
894
+ catch {
895
+ /* ignore malformed token endpoint */
896
+ }
897
+ }
898
+ return "client_secret_post";
899
+ }
674
900
  /**
675
901
  * Build a registryConsumer from the current config.
676
902
  * Decrypts secret: values in registry headers/auth before connecting.
@@ -771,7 +997,8 @@ function createAdk(fs, options = {}) {
771
997
  if (!found)
772
998
  return false;
773
999
  for (const r of registries) {
774
- if (typeof r !== "string" && (registryDisplayName(r) === nameOrUrl || registryUrl(r) === nameOrUrl)) {
1000
+ if (typeof r !== "string" &&
1001
+ (registryDisplayName(r) === nameOrUrl || registryUrl(r) === nameOrUrl)) {
775
1002
  await mutate(r);
776
1003
  }
777
1004
  }
@@ -1542,7 +1769,7 @@ function createAdk(fs, options = {}) {
1542
1769
  const entry = findRef(config.refs ?? [], name);
1543
1770
  if (!entry)
1544
1771
  throw new Error(`Ref "${name}" not found`);
1545
- const accessToken = (await readRefSecret(name, "access_token")) ??
1772
+ let accessToken = (await readRefSecret(name, "access_token")) ??
1546
1773
  (await readRefSecret(name, "api_key")) ??
1547
1774
  (await readRefSecret(name, "token"));
1548
1775
  // Resolve custom headers from config (e.g. { "X-API-Key": "secret:..." })
@@ -1589,6 +1816,16 @@ function createAdk(fs, options = {}) {
1589
1816
  }
1590
1817
  }
1591
1818
  }
1819
+ if (options.resolveCredentials) {
1820
+ const supplemented = await resolveAllCallCredentials({
1821
+ ctx: { name, entry, security: null },
1822
+ refConfig,
1823
+ accessToken,
1824
+ resolvedHeaders,
1825
+ });
1826
+ accessToken = supplemented.accessToken;
1827
+ resolvedHeaders = supplemented.resolvedHeaders;
1828
+ }
1592
1829
  const doCall = async (token) => {
1593
1830
  // Direct MCP only for redirect/proxy agents with an MCP upstream.
1594
1831
  // API-mode agents must go through the registry (it does REST translation).
@@ -1700,7 +1937,7 @@ function createAdk(fs, options = {}) {
1700
1937
  async function canResolve(field, oauthMetadata) {
1701
1938
  return (await tryResolveField(field, oauthMetadata)) !== null;
1702
1939
  }
1703
- const fields = {};
1940
+ let fields = {};
1704
1941
  if (security.type === "oauth2") {
1705
1942
  const securityExt = security;
1706
1943
  const hasRegistration = !!securityExt.dynamicRegistration;
@@ -1734,6 +1971,7 @@ function createAdk(fs, options = {}) {
1734
1971
  automated: hasRegistration,
1735
1972
  present: configKeys.includes("client_id"),
1736
1973
  resolvable: await canResolve("client_id", oauthMetadata),
1974
+ outbound: false,
1737
1975
  };
1738
1976
  if (needsSecret) {
1739
1977
  fields.client_secret = {
@@ -1741,13 +1979,14 @@ function createAdk(fs, options = {}) {
1741
1979
  automated: hasRegistration,
1742
1980
  present: configKeys.includes("client_secret"),
1743
1981
  resolvable: await canResolve("client_secret", oauthMetadata),
1982
+ outbound: false,
1744
1983
  };
1745
1984
  }
1746
1985
  fields.access_token = {
1747
1986
  required: true,
1748
1987
  automated: accessTokenAutomated,
1749
1988
  present: configKeys.includes("access_token"),
1750
- resolvable: false,
1989
+ resolvable: await canResolve("access_token"),
1751
1990
  };
1752
1991
  }
1753
1992
  else if (security.type === "apiKey") {
@@ -1804,7 +2043,12 @@ function createAdk(fs, options = {}) {
1804
2043
  format: "basic",
1805
2044
  parts: [
1806
2045
  { name: "username", label: "Username", secret: false },
1807
- { name: "password", label: "Password", secret: true, optional: true },
2046
+ {
2047
+ name: "password",
2048
+ label: "Password",
2049
+ secret: true,
2050
+ optional: true,
2051
+ },
1808
2052
  ],
1809
2053
  }),
1810
2054
  };
@@ -1825,7 +2069,8 @@ function createAdk(fs, options = {}) {
1825
2069
  resolvable: await canResolve("access_token"),
1826
2070
  };
1827
2071
  }
1828
- const complete = Object.values(fields).every((f) => !f.required || f.present || f.resolvable);
2072
+ fields = await mergeRegistryDeclaredAuthFields(fields, readRegistryDeclaredAuthFields(security), canResolve, configKeys, (entry.config ?? {}));
2073
+ const complete = Object.values(fields).every((f) => !f.required || f.automated || f.present || f.resolvable);
1829
2074
  // Persist the slim {required, automated} per-field shape into the
1830
2075
  // registry cache so `isRefAuthComplete` can answer subsequent
1831
2076
  // host-side "is this ref ready?" checks without re-fetching the
@@ -1839,6 +2084,7 @@ function createAdk(fs, options = {}) {
1839
2084
  automated: info.automated,
1840
2085
  ...(info.format && { format: info.format }),
1841
2086
  ...(info.parts && { parts: info.parts }),
2087
+ ...(info.outbound === false && { outbound: false }),
1842
2088
  };
1843
2089
  }
1844
2090
  await upsertRegistryCacheAuthFields(name, entry.ref, authFields);
@@ -1936,7 +2182,9 @@ function createAdk(fs, options = {}) {
1936
2182
  const isBasic = httpSec.scheme === "basic";
1937
2183
  if (isBasic) {
1938
2184
  const username = opts?.credentials?.["username"] ?? (await tryResolve("username"));
1939
- const password = opts?.credentials?.["password"] ?? (await tryResolve("password")) ?? "";
2185
+ const password = opts?.credentials?.["password"] ??
2186
+ (await tryResolve("password")) ??
2187
+ "";
1940
2188
  const hasUsername = username !== undefined && username !== null && username !== "";
1941
2189
  if (!hasUsername) {
1942
2190
  return {
@@ -1985,21 +2233,9 @@ function createAdk(fs, options = {}) {
1985
2233
  };
1986
2234
  }
1987
2235
  const authUrl = authCodeFlow.authorizationUrl;
1988
- let metadata = await tryFetchOAuthMetadata(authUrl);
1989
- if (!metadata) {
1990
- const origin = new URL(authUrl).origin;
1991
- metadata = await (0, mcp_client_js_1.discoverOAuthMetadata)(origin);
1992
- }
1993
- // Fallback: construct metadata from the security scheme's explicit URLs
1994
- if (!metadata && authCodeFlow.tokenUrl) {
1995
- const flowScopes = authCodeFlow.scopes;
1996
- metadata = {
1997
- issuer: new URL(authUrl).origin,
1998
- authorization_endpoint: authUrl,
1999
- token_endpoint: authCodeFlow.tokenUrl,
2000
- scopes_supported: flowScopes ? Object.keys(flowScopes) : undefined,
2001
- };
2002
- }
2236
+ const metadata = await resolveOAuthMetadataFromSecurity(security, {
2237
+ serverUrl: entry.url,
2238
+ });
2003
2239
  if (!metadata) {
2004
2240
  throw new Error(`Could not discover OAuth metadata from ${authUrl}`);
2005
2241
  }
@@ -2087,11 +2323,13 @@ function createAdk(fs, options = {}) {
2087
2323
  extraParams: authorizationParams,
2088
2324
  });
2089
2325
  // Persist pending state so handleCallback works across processes
2326
+ const clientAuthMethod = resolveClientAuthMethod(security, metadata);
2090
2327
  await storePendingOAuth(state, {
2091
2328
  refName: name,
2092
2329
  codeVerifier,
2093
2330
  clientId,
2094
2331
  clientSecret,
2332
+ clientAuthMethod,
2095
2333
  tokenEndpoint: metadata.token_endpoint,
2096
2334
  redirectUri,
2097
2335
  createdAt: Date.now(),
@@ -2227,43 +2465,43 @@ function createAdk(fs, options = {}) {
2227
2465
  return null;
2228
2466
  const status = await ref.authStatus(name);
2229
2467
  const security = status.security;
2230
- const flows = security && "flows" in security
2231
- ? security.flows
2232
- : undefined;
2233
- const authCodeFlow = flows?.authorizationCode;
2234
- const tokenUrl = authCodeFlow?.refreshUrl ?? authCodeFlow?.tokenUrl;
2235
- if (!tokenUrl)
2468
+ const metadata = await resolveOAuthMetadataFromSecurity(security, {
2469
+ serverUrl: entry.url,
2470
+ });
2471
+ const tokenEndpoint = metadata?.token_endpoint;
2472
+ if (!tokenEndpoint)
2236
2473
  return null;
2237
- const oauthClient = await resolveOAuthClient({ name, entry, security });
2474
+ const oauthClient = await resolveOAuthClient({
2475
+ name,
2476
+ entry,
2477
+ security,
2478
+ metadata,
2479
+ });
2238
2480
  if (!oauthClient)
2239
2481
  return null;
2240
- const { clientId, clientSecret } = oauthClient;
2241
- // POST to the token endpoint with grant_type=refresh_token
2242
- const body = new URLSearchParams({
2243
- grant_type: "refresh_token",
2244
- refresh_token: refreshToken,
2245
- client_id: clientId,
2246
- });
2247
- if (clientSecret) {
2248
- body.set("client_secret", clientSecret);
2482
+ const clientAuthMethod = resolveClientAuthMethod(security, metadata);
2483
+ const fetchFn = options.fetch ?? globalThis.fetch;
2484
+ let tokens;
2485
+ try {
2486
+ tokens = await (0, mcp_client_js_1.refreshAccessToken)(tokenEndpoint, {
2487
+ refreshToken,
2488
+ clientId: oauthClient.clientId,
2489
+ clientSecret: oauthClient.clientSecret,
2490
+ clientAuthMethod,
2491
+ }, fetchFn);
2249
2492
  }
2250
- const res = await globalThis.fetch(tokenUrl, {
2251
- method: "POST",
2252
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
2253
- body: body.toString(),
2254
- });
2255
- if (!res.ok)
2256
- return null;
2257
- const data = (await res.json());
2258
- const newAccessToken = data.access_token;
2259
- if (!newAccessToken)
2493
+ catch {
2260
2494
  return null;
2261
- // Store the new tokens
2262
- await storeRefSecret(name, "access_token", newAccessToken);
2263
- if (data.refresh_token && typeof data.refresh_token === "string") {
2264
- await storeRefSecret(name, "refresh_token", data.refresh_token);
2265
2495
  }
2266
- return { accessToken: newAccessToken };
2496
+ await storeRefSecret(name, "access_token", tokens.accessToken);
2497
+ if (tokens.refreshToken) {
2498
+ await storeRefSecret(name, "refresh_token", tokens.refreshToken);
2499
+ }
2500
+ if (tokens.expiresIn) {
2501
+ const expiresAt = new Date(Date.now() + tokens.expiresIn * 1000).toISOString();
2502
+ await storeRefSecret(name, "expires_at", expiresAt);
2503
+ }
2504
+ return { accessToken: tokens.accessToken };
2267
2505
  },
2268
2506
  };
2269
2507
  // ==========================================
@@ -2274,13 +2512,15 @@ function createAdk(fs, options = {}) {
2274
2512
  if (!pending) {
2275
2513
  throw new Error(`No pending OAuth flow for state "${params.state}".`);
2276
2514
  }
2515
+ const fetchFn = options.fetch ?? globalThis.fetch;
2277
2516
  const tokens = await (0, mcp_client_js_1.exchangeCodeForTokens)(pending.tokenEndpoint, {
2278
2517
  code: params.code,
2279
2518
  codeVerifier: pending.codeVerifier,
2280
2519
  clientId: pending.clientId,
2281
2520
  clientSecret: pending.clientSecret,
2282
2521
  redirectUri: pending.redirectUri,
2283
- });
2522
+ clientAuthMethod: pending.clientAuthMethod,
2523
+ }, fetchFn);
2284
2524
  await storeRefSecret(pending.refName, "access_token", tokens.accessToken);
2285
2525
  if (tokens.refreshToken) {
2286
2526
  await storeRefSecret(pending.refName, "refresh_token", tokens.refreshToken);