@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.
@@ -33,6 +33,7 @@ import type { RegistryAuthRequirement } from "./define-config.js";
33
33
  import type { FetchFn } from "./fetch-types.js";
34
34
  import type { Logger } from "./logger.js";
35
35
  import {
36
+ type OAuthClientAuthMethod,
36
37
  buildOAuthAuthorizeUrl,
37
38
  discoverOAuthMetadata,
38
39
  dynamicClientRegistration,
@@ -111,6 +112,11 @@ export interface RegistryCacheAuthField {
111
112
  * For example HTTP Basic stores one `token` but asks UI for username/password.
112
113
  */
113
114
  parts?: RegistryCacheAuthFieldPart[];
115
+ /**
116
+ * When `false`, connect/refresh bookkeeping only — not forwarded on
117
+ * `ref.call`. Omitted or `true` for bearer, header, and call-time creds.
118
+ */
119
+ outbound?: boolean;
114
120
  }
115
121
 
116
122
  /**
@@ -391,6 +397,8 @@ export interface CredentialField {
391
397
  format?: CompositeCredentialFormat;
392
398
  /** Structured inputs that compose this canonical stored credential */
393
399
  parts?: AuthChallengeField[];
400
+ /** Connect/refresh only — not forwarded on ref.call. */
401
+ outbound?: boolean;
394
402
  }
395
403
 
396
404
  /** Describes what auth a ref needs and what's already provided */
@@ -991,6 +999,205 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
991
999
  };
992
1000
  }
993
1001
 
1002
+ /**
1003
+ * Call-time credential lookup: stored ref config first, then the host
1004
+ * `resolveCredentials` callback. Does not persist resolved values.
1005
+ */
1006
+ async function resolveCallCredential(
1007
+ ctx: CredentialResolverContext,
1008
+ field: string,
1009
+ ): Promise<string | null> {
1010
+ const stored = await readRefSecret(ctx.name, field);
1011
+ if (stored) return stored;
1012
+ return makeTryResolve(ctx)(field);
1013
+ }
1014
+
1015
+ const CALL_BEARER_FIELDS = ["access_token", "api_key", "token"] as const;
1016
+
1017
+ function isBearerAuthField(field: string): boolean {
1018
+ return (CALL_BEARER_FIELDS as readonly string[]).includes(field);
1019
+ }
1020
+
1021
+ /** Legacy cache entries may omit `outbound`; these are never call-time creds. */
1022
+ const LEGACY_CONNECT_ONLY_FIELDS = new Set([
1023
+ "client_id",
1024
+ "client_secret",
1025
+ "refresh_token",
1026
+ ]);
1027
+
1028
+ function isCallOutboundAuthField(
1029
+ field: string,
1030
+ info: RegistryCacheAuthField,
1031
+ ): boolean {
1032
+ if (info.outbound === false) return false;
1033
+ if (info.outbound === true) return true;
1034
+ return !LEGACY_CONNECT_ONLY_FIELDS.has(field);
1035
+ }
1036
+
1037
+ function readRegistryDeclaredAuthFields(
1038
+ security: SecuritySchemeSummary | null,
1039
+ ): Record<string, RegistryCacheAuthField> | undefined {
1040
+ if (!security || typeof security !== "object") return undefined;
1041
+ const raw = (security as { authFields?: unknown }).authFields;
1042
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return undefined;
1043
+ const out: Record<string, RegistryCacheAuthField> = {};
1044
+ for (const [field, meta] of Object.entries(
1045
+ raw as Record<string, unknown>,
1046
+ )) {
1047
+ if (!meta || typeof meta !== "object" || Array.isArray(meta)) continue;
1048
+ const m = meta as Record<string, unknown>;
1049
+ if (typeof m.required !== "boolean" || typeof m.automated !== "boolean") {
1050
+ continue;
1051
+ }
1052
+ out[field] = { required: m.required, automated: m.automated };
1053
+ if (typeof m.outbound === "boolean") {
1054
+ out[field].outbound = m.outbound;
1055
+ }
1056
+ }
1057
+ return Object.keys(out).length > 0 ? out : undefined;
1058
+ }
1059
+
1060
+ async function mergeRegistryDeclaredAuthFields(
1061
+ fields: Record<string, CredentialField>,
1062
+ declared: Record<string, RegistryCacheAuthField> | undefined,
1063
+ canResolve: (field: string) => Promise<boolean>,
1064
+ configKeys: string[],
1065
+ refConfig: Record<string, unknown>,
1066
+ ): Promise<Record<string, CredentialField>> {
1067
+ if (!declared) return fields;
1068
+ const next: Record<string, CredentialField> = {};
1069
+ for (const [field, meta] of Object.entries(declared)) {
1070
+ next[field] = {
1071
+ required: meta.required,
1072
+ automated: meta.automated,
1073
+ present:
1074
+ configKeys.includes(field) || hasCredentialField(refConfig, field),
1075
+ resolvable: await canResolve(field),
1076
+ ...(meta.format && { format: meta.format }),
1077
+ ...(meta.parts && { parts: meta.parts }),
1078
+ ...(meta.outbound === false && { outbound: false }),
1079
+ };
1080
+ }
1081
+ return next;
1082
+ }
1083
+
1084
+ function bearerFieldSatisfied(
1085
+ accessToken: string | null,
1086
+ refConfig: Record<string, unknown>,
1087
+ field: string,
1088
+ ): boolean {
1089
+ if (accessToken) return true;
1090
+ return hasCredentialField(refConfig, field);
1091
+ }
1092
+
1093
+ function fallbackCallAuthFields(): Record<string, RegistryCacheAuthField> {
1094
+ return {
1095
+ access_token: { required: true, automated: true },
1096
+ api_key: { required: false, automated: true },
1097
+ token: { required: false, automated: true },
1098
+ };
1099
+ }
1100
+
1101
+ function headerFieldSatisfied(
1102
+ headers: Record<string, string>,
1103
+ field: string,
1104
+ ): boolean {
1105
+ const wanted = normalizeCredentialKey(field);
1106
+ return Object.keys(headers).some(
1107
+ (key) => normalizeCredentialKey(key) === wanted,
1108
+ );
1109
+ }
1110
+
1111
+ function resolveHeaderNameForField(
1112
+ field: string,
1113
+ refConfig: Record<string, unknown>,
1114
+ ): string {
1115
+ const wanted = normalizeCredentialKey(field);
1116
+ const configHeaders = refConfig.headers;
1117
+ if (
1118
+ configHeaders &&
1119
+ typeof configHeaders === "object" &&
1120
+ !Array.isArray(configHeaders)
1121
+ ) {
1122
+ for (const key of Object.keys(configHeaders as Record<string, unknown>)) {
1123
+ if (normalizeCredentialKey(key) === wanted) return key;
1124
+ }
1125
+ }
1126
+ // x_api_key → X-API-KEY (registry codegen declares the canonical name;
1127
+ // env-resolved keys use the normalized storage field name).
1128
+ return field
1129
+ .split("_")
1130
+ .filter(Boolean)
1131
+ .map((part) => part.toUpperCase())
1132
+ .join("-");
1133
+ }
1134
+
1135
+ /**
1136
+ * Supplement call-time credentials from `resolveCredentials` when they
1137
+ * are not already present in consumer-config. Stored values and config
1138
+ * headers win — this only fills gaps. Walks cached `authFields` as the
1139
+ * source of truth (registry-declared when auth-status has run).
1140
+ */
1141
+ async function resolveAllCallCredentials(opts: {
1142
+ ctx: CredentialResolverContext;
1143
+ refConfig: Record<string, unknown>;
1144
+ accessToken: string | null;
1145
+ resolvedHeaders: Record<string, string> | undefined;
1146
+ }): Promise<{
1147
+ accessToken: string | null;
1148
+ resolvedHeaders: Record<string, string> | undefined;
1149
+ }> {
1150
+ if (!options.resolveCredentials) {
1151
+ return {
1152
+ accessToken: opts.accessToken,
1153
+ resolvedHeaders: opts.resolvedHeaders,
1154
+ };
1155
+ }
1156
+
1157
+ let accessToken = opts.accessToken;
1158
+ let resolvedHeaders = opts.resolvedHeaders;
1159
+ const { ctx, refConfig } = opts;
1160
+
1161
+ const cache = await readRegistryCache();
1162
+ const authFields =
1163
+ cache.refs[ctx.name]?.authFields ?? fallbackCallAuthFields();
1164
+
1165
+ for (const [field, info] of Object.entries(authFields)) {
1166
+ if (!isCallOutboundAuthField(field, info)) continue;
1167
+ if (!info.required && !info.automated) continue;
1168
+
1169
+ if (isBearerAuthField(field)) {
1170
+ if (bearerFieldSatisfied(accessToken, refConfig, field)) continue;
1171
+ const value = await resolveCallCredential(ctx, field);
1172
+ if (value) accessToken = accessToken ?? value;
1173
+ continue;
1174
+ }
1175
+
1176
+ if (hasCredentialField(refConfig, field)) continue;
1177
+ if (resolvedHeaders && headerFieldSatisfied(resolvedHeaders, field)) {
1178
+ continue;
1179
+ }
1180
+
1181
+ const value = await resolveCallCredential(ctx, field);
1182
+ if (!value) continue;
1183
+
1184
+ resolvedHeaders = resolvedHeaders ?? {};
1185
+ if (!headerFieldSatisfied(resolvedHeaders, field)) {
1186
+ resolvedHeaders[resolveHeaderNameForField(field, refConfig)] = value;
1187
+ }
1188
+ }
1189
+
1190
+ if (!accessToken) {
1191
+ const username = await resolveCallCredential(ctx, "username");
1192
+ const password = await resolveCallCredential(ctx, "password");
1193
+ if (username && password) {
1194
+ accessToken = btoa(`${username}:${password}`);
1195
+ }
1196
+ }
1197
+
1198
+ return { accessToken, resolvedHeaders };
1199
+ }
1200
+
994
1201
  /**
995
1202
  * Resolve OAuth client credentials (client_id + client_secret) for a
996
1203
  * ref. Walks: `resolveCredentials` callback → per-ref VCS storage.
@@ -1023,6 +1230,7 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1023
1230
  codeVerifier: string;
1024
1231
  clientId: string;
1025
1232
  clientSecret?: string;
1233
+ clientAuthMethod?: OAuthClientAuthMethod;
1026
1234
  tokenEndpoint: string;
1027
1235
  redirectUri: string;
1028
1236
  createdAt: number;
@@ -1230,6 +1438,109 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1230
1438
  }
1231
1439
  }
1232
1440
 
1441
+ /**
1442
+ * Resolve OAuth server metadata from a registry security scheme.
1443
+ * Shared by `ref.auth` and `ref.refreshToken` so both paths discover
1444
+ * token endpoints the same way — explicit manifest URLs, RFC 8414
1445
+ * discovery via `discoveryUrl`, authorization-server discovery, and
1446
+ * finally the MCP upstream URL for redirect-mode agents.
1447
+ */
1448
+ async function resolveOAuthMetadataFromSecurity(
1449
+ security: SecuritySchemeSummary | null | undefined,
1450
+ opts?: { serverUrl?: string },
1451
+ ): Promise<import("./mcp-client.js").OAuthServerMetadata | null> {
1452
+ if (!security || security.type !== "oauth2") return null;
1453
+
1454
+ const securityExt = security as {
1455
+ discoveryUrl?: string;
1456
+ flows?: {
1457
+ authorizationCode?: {
1458
+ authorizationUrl?: string;
1459
+ tokenUrl?: string;
1460
+ refreshUrl?: string;
1461
+ scopes?: Record<string, string>;
1462
+ };
1463
+ };
1464
+ };
1465
+ const authCodeFlow = securityExt.flows?.authorizationCode;
1466
+
1467
+ const explicitEndpoint = authCodeFlow?.refreshUrl ?? authCodeFlow?.tokenUrl;
1468
+ if (explicitEndpoint) {
1469
+ const flowScopes = (authCodeFlow as Record<string, unknown> | undefined)
1470
+ ?.scopes as Record<string, string> | undefined;
1471
+ const authUrl = authCodeFlow?.authorizationUrl;
1472
+ return {
1473
+ issuer: authUrl
1474
+ ? new URL(authUrl).origin
1475
+ : new URL(explicitEndpoint).origin,
1476
+ authorization_endpoint: authUrl ?? explicitEndpoint,
1477
+ token_endpoint: explicitEndpoint,
1478
+ scopes_supported: flowScopes ? Object.keys(flowScopes) : undefined,
1479
+ };
1480
+ }
1481
+
1482
+ if (securityExt.discoveryUrl) {
1483
+ const fromDiscovery =
1484
+ (await tryFetchOAuthMetadata(securityExt.discoveryUrl)) ??
1485
+ (await discoverOAuthMetadata(securityExt.discoveryUrl));
1486
+ if (fromDiscovery) return fromDiscovery;
1487
+ }
1488
+
1489
+ const authUrl = authCodeFlow?.authorizationUrl;
1490
+ if (authUrl) {
1491
+ let metadata = await tryFetchOAuthMetadata(authUrl);
1492
+ if (!metadata) {
1493
+ metadata = await discoverOAuthMetadata(new URL(authUrl).origin);
1494
+ }
1495
+ if (metadata) return metadata;
1496
+ }
1497
+
1498
+ const serverUrl = opts?.serverUrl;
1499
+ if (serverUrl) {
1500
+ let metadata = await discoverOAuthMetadata(serverUrl);
1501
+ if (!metadata) {
1502
+ metadata = await discoverOAuthMetadata(
1503
+ serverUrl.replace(/\/(mcp|sse)$/, ""),
1504
+ );
1505
+ }
1506
+ if (metadata) return metadata;
1507
+ }
1508
+
1509
+ return null;
1510
+ }
1511
+
1512
+ function resolveClientAuthMethod(
1513
+ security: SecuritySchemeSummary | null | undefined,
1514
+ metadata: OAuthServerMetadata | null,
1515
+ ): OAuthClientAuthMethod {
1516
+ const flowAuth = (
1517
+ security as {
1518
+ flows?: {
1519
+ authorizationCode?: { clientAuth?: OAuthClientAuthMethod };
1520
+ };
1521
+ }
1522
+ ).flows?.authorizationCode?.clientAuth;
1523
+ if (flowAuth) return flowAuth;
1524
+
1525
+ const supported = metadata?.token_endpoint_auth_methods_supported;
1526
+ if (supported?.length === 1 && supported[0] === "client_secret_basic") {
1527
+ return "client_secret_basic";
1528
+ }
1529
+
1530
+ const tokenEndpoint = metadata?.token_endpoint;
1531
+ if (tokenEndpoint) {
1532
+ try {
1533
+ if (new URL(tokenEndpoint).hostname === "api.x.com") {
1534
+ return "client_secret_basic";
1535
+ }
1536
+ } catch {
1537
+ /* ignore malformed token endpoint */
1538
+ }
1539
+ }
1540
+
1541
+ return "client_secret_post";
1542
+ }
1543
+
1233
1544
  /**
1234
1545
  * Build a registryConsumer from the current config.
1235
1546
  * Decrypts secret: values in registry headers/auth before connecting.
@@ -1364,7 +1675,10 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1364
1675
  });
1365
1676
  if (!found) return false;
1366
1677
  for (const r of registries) {
1367
- if (typeof r !== "string" && (registryDisplayName(r) === nameOrUrl || registryUrl(r) === nameOrUrl)) {
1678
+ if (
1679
+ typeof r !== "string" &&
1680
+ (registryDisplayName(r) === nameOrUrl || registryUrl(r) === nameOrUrl)
1681
+ ) {
1368
1682
  await mutate(r);
1369
1683
  }
1370
1684
  }
@@ -2254,7 +2568,7 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
2254
2568
  const entry = findRef(config.refs ?? [], name);
2255
2569
  if (!entry) throw new Error(`Ref "${name}" not found`);
2256
2570
 
2257
- const accessToken =
2571
+ let accessToken =
2258
2572
  (await readRefSecret(name, "access_token")) ??
2259
2573
  (await readRefSecret(name, "api_key")) ??
2260
2574
  (await readRefSecret(name, "token"));
@@ -2306,6 +2620,17 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
2306
2620
  }
2307
2621
  }
2308
2622
 
2623
+ if (options.resolveCredentials) {
2624
+ const supplemented = await resolveAllCallCredentials({
2625
+ ctx: { name, entry, security: null },
2626
+ refConfig,
2627
+ accessToken,
2628
+ resolvedHeaders,
2629
+ });
2630
+ accessToken = supplemented.accessToken;
2631
+ resolvedHeaders = supplemented.resolvedHeaders;
2632
+ }
2633
+
2309
2634
  const doCall = async (token: string | null) => {
2310
2635
  // Direct MCP only for redirect/proxy agents with an MCP upstream.
2311
2636
  // API-mode agents must go through the registry (it does REST translation).
@@ -2460,7 +2785,7 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
2460
2785
  return (await tryResolveField(field, oauthMetadata)) !== null;
2461
2786
  }
2462
2787
 
2463
- const fields: Record<string, CredentialField> = {};
2788
+ let fields: Record<string, CredentialField> = {};
2464
2789
 
2465
2790
  if (security.type === "oauth2") {
2466
2791
  const securityExt = security as {
@@ -2506,6 +2831,7 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
2506
2831
  automated: hasRegistration,
2507
2832
  present: configKeys.includes("client_id"),
2508
2833
  resolvable: await canResolve("client_id", oauthMetadata),
2834
+ outbound: false,
2509
2835
  };
2510
2836
  if (needsSecret) {
2511
2837
  fields.client_secret = {
@@ -2513,13 +2839,14 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
2513
2839
  automated: hasRegistration,
2514
2840
  present: configKeys.includes("client_secret"),
2515
2841
  resolvable: await canResolve("client_secret", oauthMetadata),
2842
+ outbound: false,
2516
2843
  };
2517
2844
  }
2518
2845
  fields.access_token = {
2519
2846
  required: true,
2520
2847
  automated: accessTokenAutomated,
2521
2848
  present: configKeys.includes("access_token"),
2522
- resolvable: false,
2849
+ resolvable: await canResolve("access_token"),
2523
2850
  };
2524
2851
  } else if (security.type === "apiKey") {
2525
2852
  const apiKeySec = security as {
@@ -2586,7 +2913,12 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
2586
2913
  format: "basic" as const,
2587
2914
  parts: [
2588
2915
  { name: "username", label: "Username", secret: false },
2589
- { name: "password", label: "Password", secret: true, optional: true },
2916
+ {
2917
+ name: "password",
2918
+ label: "Password",
2919
+ secret: true,
2920
+ optional: true,
2921
+ },
2590
2922
  ],
2591
2923
  }),
2592
2924
  };
@@ -2607,8 +2939,16 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
2607
2939
  };
2608
2940
  }
2609
2941
 
2942
+ fields = await mergeRegistryDeclaredAuthFields(
2943
+ fields,
2944
+ readRegistryDeclaredAuthFields(security),
2945
+ canResolve,
2946
+ configKeys,
2947
+ (entry.config ?? {}) as Record<string, unknown>,
2948
+ );
2949
+
2610
2950
  const complete = Object.values(fields).every(
2611
- (f) => !f.required || f.present || f.resolvable,
2951
+ (f) => !f.required || f.automated || f.present || f.resolvable,
2612
2952
  );
2613
2953
 
2614
2954
  // Persist the slim {required, automated} per-field shape into the
@@ -2624,6 +2964,7 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
2624
2964
  automated: info.automated,
2625
2965
  ...(info.format && { format: info.format }),
2626
2966
  ...(info.parts && { parts: info.parts }),
2967
+ ...(info.outbound === false && { outbound: false }),
2627
2968
  };
2628
2969
  }
2629
2970
  await upsertRegistryCacheAuthFields(name, entry.ref, authFields);
@@ -2761,8 +3102,11 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
2761
3102
  const username =
2762
3103
  opts?.credentials?.["username"] ?? (await tryResolve("username"));
2763
3104
  const password =
2764
- opts?.credentials?.["password"] ?? (await tryResolve("password")) ?? "";
2765
- const hasUsername = username !== undefined && username !== null && username !== "";
3105
+ opts?.credentials?.["password"] ??
3106
+ (await tryResolve("password")) ??
3107
+ "";
3108
+ const hasUsername =
3109
+ username !== undefined && username !== null && username !== "";
2766
3110
  if (!hasUsername) {
2767
3111
  return {
2768
3112
  type: "http",
@@ -2823,23 +3167,9 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
2823
3167
  }
2824
3168
 
2825
3169
  const authUrl = authCodeFlow.authorizationUrl;
2826
- let metadata = await tryFetchOAuthMetadata(authUrl);
2827
- if (!metadata) {
2828
- const origin = new URL(authUrl).origin;
2829
- metadata = await discoverOAuthMetadata(origin);
2830
- }
2831
- // Fallback: construct metadata from the security scheme's explicit URLs
2832
- if (!metadata && authCodeFlow.tokenUrl) {
2833
- const flowScopes = (authCodeFlow as Record<string, unknown>).scopes as
2834
- | Record<string, string>
2835
- | undefined;
2836
- metadata = {
2837
- issuer: new URL(authUrl).origin,
2838
- authorization_endpoint: authUrl,
2839
- token_endpoint: authCodeFlow.tokenUrl,
2840
- scopes_supported: flowScopes ? Object.keys(flowScopes) : undefined,
2841
- } as import("./mcp-client.js").OAuthServerMetadata;
2842
- }
3170
+ const metadata = await resolveOAuthMetadataFromSecurity(security, {
3171
+ serverUrl: entry.url,
3172
+ });
2843
3173
  if (!metadata) {
2844
3174
  throw new Error(`Could not discover OAuth metadata from ${authUrl}`);
2845
3175
  }
@@ -2950,11 +3280,13 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
2950
3280
  });
2951
3281
 
2952
3282
  // Persist pending state so handleCallback works across processes
3283
+ const clientAuthMethod = resolveClientAuthMethod(security, metadata);
2953
3284
  await storePendingOAuth(state, {
2954
3285
  refName: name,
2955
3286
  codeVerifier,
2956
3287
  clientId,
2957
3288
  clientSecret,
3289
+ clientAuthMethod,
2958
3290
  tokenEndpoint: metadata.token_endpoint,
2959
3291
  redirectUri,
2960
3292
  createdAt: Date.now(),
@@ -3125,54 +3457,50 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
3125
3457
 
3126
3458
  const status = await ref.authStatus(name);
3127
3459
  const security = status.security;
3128
- const flows =
3129
- security && "flows" in security
3130
- ? (
3131
- security as {
3132
- flows?: Record<
3133
- string,
3134
- { tokenUrl?: string; refreshUrl?: string }
3135
- >;
3136
- }
3137
- ).flows
3138
- : undefined;
3139
- const authCodeFlow = flows?.authorizationCode;
3140
- const tokenUrl = authCodeFlow?.refreshUrl ?? authCodeFlow?.tokenUrl;
3141
- if (!tokenUrl) return null;
3142
-
3143
- const oauthClient = await resolveOAuthClient({ name, entry, security });
3144
- if (!oauthClient) return null;
3145
- const { clientId, clientSecret } = oauthClient;
3146
-
3147
- // POST to the token endpoint with grant_type=refresh_token
3148
- const body = new URLSearchParams({
3149
- grant_type: "refresh_token",
3150
- refresh_token: refreshToken,
3151
- client_id: clientId,
3460
+ const metadata = await resolveOAuthMetadataFromSecurity(security, {
3461
+ serverUrl: entry.url,
3152
3462
  });
3153
- if (clientSecret) {
3154
- body.set("client_secret", clientSecret);
3155
- }
3156
-
3157
- const res = await globalThis.fetch(tokenUrl, {
3158
- method: "POST",
3159
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
3160
- body: body.toString(),
3463
+ const tokenEndpoint = metadata?.token_endpoint;
3464
+ if (!tokenEndpoint) return null;
3465
+
3466
+ const oauthClient = await resolveOAuthClient({
3467
+ name,
3468
+ entry,
3469
+ security,
3470
+ metadata,
3161
3471
  });
3472
+ if (!oauthClient) return null;
3162
3473
 
3163
- if (!res.ok) return null;
3164
-
3165
- const data = (await res.json()) as Record<string, unknown>;
3166
- const newAccessToken = data.access_token as string | undefined;
3167
- if (!newAccessToken) return null;
3474
+ const clientAuthMethod = resolveClientAuthMethod(security, metadata);
3475
+ const fetchFn = options.fetch ?? globalThis.fetch;
3476
+ let tokens: Awaited<ReturnType<typeof refreshAccessToken>>;
3477
+ try {
3478
+ tokens = await refreshAccessToken(
3479
+ tokenEndpoint,
3480
+ {
3481
+ refreshToken,
3482
+ clientId: oauthClient.clientId,
3483
+ clientSecret: oauthClient.clientSecret,
3484
+ clientAuthMethod,
3485
+ },
3486
+ fetchFn,
3487
+ );
3488
+ } catch {
3489
+ return null;
3490
+ }
3168
3491
 
3169
- // Store the new tokens
3170
- await storeRefSecret(name, "access_token", newAccessToken);
3171
- if (data.refresh_token && typeof data.refresh_token === "string") {
3172
- await storeRefSecret(name, "refresh_token", data.refresh_token);
3492
+ await storeRefSecret(name, "access_token", tokens.accessToken);
3493
+ if (tokens.refreshToken) {
3494
+ await storeRefSecret(name, "refresh_token", tokens.refreshToken);
3495
+ }
3496
+ if (tokens.expiresIn) {
3497
+ const expiresAt = new Date(
3498
+ Date.now() + tokens.expiresIn * 1000,
3499
+ ).toISOString();
3500
+ await storeRefSecret(name, "expires_at", expiresAt);
3173
3501
  }
3174
3502
 
3175
- return { accessToken: newAccessToken };
3503
+ return { accessToken: tokens.accessToken };
3176
3504
  },
3177
3505
  };
3178
3506
 
@@ -3193,13 +3521,19 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
3193
3521
  throw new Error(`No pending OAuth flow for state "${params.state}".`);
3194
3522
  }
3195
3523
 
3196
- const tokens = await exchangeCodeForTokens(pending.tokenEndpoint, {
3197
- code: params.code,
3198
- codeVerifier: pending.codeVerifier,
3199
- clientId: pending.clientId,
3200
- clientSecret: pending.clientSecret,
3201
- redirectUri: pending.redirectUri,
3202
- });
3524
+ const fetchFn = options.fetch ?? globalThis.fetch;
3525
+ const tokens = await exchangeCodeForTokens(
3526
+ pending.tokenEndpoint,
3527
+ {
3528
+ code: params.code,
3529
+ codeVerifier: pending.codeVerifier,
3530
+ clientId: pending.clientId,
3531
+ clientSecret: pending.clientSecret,
3532
+ redirectUri: pending.redirectUri,
3533
+ clientAuthMethod: pending.clientAuthMethod,
3534
+ },
3535
+ fetchFn,
3536
+ );
3203
3537
 
3204
3538
  await storeRefSecret(pending.refName, "access_token", tokens.accessToken);
3205
3539
  if (tokens.refreshToken) {
package/src/index.ts CHANGED
@@ -352,7 +352,10 @@ export {
352
352
  exchangeCodeForTokens,
353
353
  refreshAccessToken as refreshMcpAccessToken,
354
354
  } from "./mcp-client.js";
355
- export type { OAuthServerMetadata } from "./mcp-client.js";
355
+ export type {
356
+ OAuthClientAuthMethod,
357
+ OAuthServerMetadata,
358
+ } from "./mcp-client.js";
356
359
 
357
360
  // ============================================
358
361
  // Serialized Agent Definitions