@slashfi/agents-sdk 0.67.2 → 0.68.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.
@@ -143,11 +143,29 @@ export interface OAuthResult {
143
143
  clientId: string;
144
144
  }
145
145
 
146
+ /** A field the caller needs to collect from the user */
147
+ export interface AuthChallengeField {
148
+ /** Field key (e.g. "api_key", "token") */
149
+ name: string;
150
+ /** Human-readable label (e.g. "API Key", "DD-API-KEY") */
151
+ label: string;
152
+ /** Whether this is a secret value (should be masked in UI) */
153
+ secret: boolean;
154
+ /** Optional description / help text */
155
+ description?: string;
156
+ }
157
+
146
158
  export interface AuthStartResult {
147
159
  type: string;
148
160
  complete: boolean;
149
161
  /** For OAuth: the URL to open in the browser */
150
162
  authorizeUrl?: string;
163
+ /**
164
+ * When complete=false and type is "apiKey" or "http",
165
+ * these are the fields the caller should collect from the user.
166
+ * The caller can render these as a form (Slack blocks, web modal, CLI prompts).
167
+ */
168
+ fields?: AuthChallengeField[];
151
169
  }
152
170
 
153
171
  export interface AdkRefApi {
@@ -168,8 +186,14 @@ export interface AdkRefApi {
168
186
  * adk.ref.authLocal() to spin up a local server and block.
169
187
  */
170
188
  auth(name: string, opts?: {
171
- /** For API key / bearer auth: the key/token value */
189
+ /** For API key / bearer auth: the key/token value (single-key shorthand) */
172
190
  apiKey?: string;
191
+ /**
192
+ * Credentials map for multi-field auth. Keys match the `name` field
193
+ * from AuthChallengeField (e.g. { "api_key": "xxx", "app_key": "yyy" }).
194
+ * For single-key apiKey or http bearer, `apiKey` shorthand also works.
195
+ */
196
+ credentials?: Record<string, string>;
173
197
  /** Extra context to encode in the OAuth state (e.g., tenant/user IDs for multi-tenant callbacks) */
174
198
  stateContext?: Record<string, unknown>;
175
199
  /** Additional scopes to request (e.g., optional scopes declared by the agent) */
@@ -998,12 +1022,32 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
998
1022
  resolvable: false,
999
1023
  };
1000
1024
  } else if (security.type === "apiKey") {
1001
- fields.api_key = {
1002
- required: true,
1003
- automated: false,
1004
- present: configKeys.includes("api_key"),
1005
- resolvable: await canResolve("api_key"),
1025
+ const apiKeySec = security as {
1026
+ name?: string; headers?: Record<string, { description?: string }>;
1006
1027
  };
1028
+ const toStorageKey = (headerName: string) =>
1029
+ headerName.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
1030
+
1031
+ if (apiKeySec.headers && Object.keys(apiKeySec.headers).length > 0) {
1032
+ // Multi-key mode
1033
+ for (const headerName of Object.keys(apiKeySec.headers)) {
1034
+ const storageKey = toStorageKey(headerName);
1035
+ fields[storageKey] = {
1036
+ required: true,
1037
+ automated: false,
1038
+ present: configKeys.includes(storageKey),
1039
+ resolvable: await canResolve(storageKey),
1040
+ };
1041
+ }
1042
+ } else {
1043
+ // Single-key mode (backwards compat)
1044
+ fields.api_key = {
1045
+ required: true,
1046
+ automated: false,
1047
+ present: configKeys.includes("api_key"),
1048
+ resolvable: await canResolve("api_key"),
1049
+ };
1050
+ }
1007
1051
  } else if (security.type === "http") {
1008
1052
  fields.token = {
1009
1053
  required: true,
@@ -1022,6 +1066,7 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1022
1066
 
1023
1067
  async auth(name: string, opts?: {
1024
1068
  apiKey?: string;
1069
+ credentials?: Record<string, string>;
1025
1070
  /** Extra context to encode in the OAuth state (e.g., tenant/user IDs for multi-tenant callbacks) */
1026
1071
  stateContext?: Record<string, unknown>;
1027
1072
  /** Additional scopes to request (e.g., optional scopes declared by the agent) */
@@ -1045,15 +1090,92 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1045
1090
  }
1046
1091
 
1047
1092
  if (security.type === "apiKey") {
1048
- const key = opts?.apiKey ?? await tryResolve("api_key");
1049
- if (!key) return { type: "apiKey", complete: false };
1093
+ const apiKeySec = security as {
1094
+ name?: string; prefix?: string;
1095
+ headers?: Record<string, { description?: string }>;
1096
+ };
1097
+
1098
+ const toStorageKey = (headerName: string) =>
1099
+ headerName.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
1100
+
1101
+ if (apiKeySec.headers && Object.keys(apiKeySec.headers).length > 0) {
1102
+ // Multi-key mode: iterate all declared headers
1103
+ const missingFields: AuthChallengeField[] = [];
1104
+ const resolvedKeys: Array<{ storageKey: string; value: string }> = [];
1105
+
1106
+ for (const [headerName, meta] of Object.entries(apiKeySec.headers)) {
1107
+ const storageKey = toStorageKey(headerName);
1108
+ const value = opts?.credentials?.[storageKey] ?? await tryResolve(storageKey);
1109
+
1110
+ if (value) {
1111
+ resolvedKeys.push({ storageKey, value });
1112
+ } else {
1113
+ missingFields.push({
1114
+ name: storageKey,
1115
+ label: headerName,
1116
+ secret: true,
1117
+ description: meta.description,
1118
+ });
1119
+ }
1120
+ }
1121
+
1122
+ if (missingFields.length > 0) {
1123
+ return { type: "apiKey", complete: false, fields: missingFields };
1124
+ }
1125
+ for (const { storageKey, value } of resolvedKeys) {
1126
+ await storeRefSecret(name, storageKey, value);
1127
+ }
1128
+ return { type: "apiKey", complete: true };
1129
+ }
1130
+
1131
+ // Single-key mode (backwards compat)
1132
+ const key = opts?.credentials?.["api_key"] ?? opts?.apiKey ?? await tryResolve("api_key");
1133
+ if (!key) {
1134
+ return {
1135
+ type: "apiKey",
1136
+ complete: false,
1137
+ fields: [{
1138
+ name: "api_key",
1139
+ label: apiKeySec.name ?? "API Key",
1140
+ secret: true,
1141
+ description: apiKeySec.prefix
1142
+ ? `Value sent as "${apiKeySec.prefix} <key>"`
1143
+ : undefined,
1144
+ }],
1145
+ };
1146
+ }
1050
1147
  await storeRefSecret(name, "api_key", key);
1051
1148
  return { type: "apiKey", complete: true };
1052
1149
  }
1053
1150
 
1054
1151
  if (security.type === "http") {
1055
- const token = opts?.apiKey ?? await tryResolve("token");
1056
- if (!token) return { type: "http", complete: false };
1152
+ const httpSec = security as { scheme?: string };
1153
+ const isBasic = httpSec.scheme === "basic";
1154
+
1155
+ if (isBasic) {
1156
+ const username = opts?.credentials?.["username"] ?? await tryResolve("username");
1157
+ const password = opts?.credentials?.["password"] ?? await tryResolve("password");
1158
+ if (!username || !password) {
1159
+ const missingFields: AuthChallengeField[] = [];
1160
+ if (!username) missingFields.push({ name: "username", label: "Username", secret: false });
1161
+ if (!password) missingFields.push({ name: "password", label: "Password", secret: true });
1162
+ return { type: "http", complete: false, fields: missingFields };
1163
+ }
1164
+ // Store as base64 encoded basic auth token
1165
+ const token = btoa(`${username}:${password}`);
1166
+ await storeRefSecret(name, "token", token);
1167
+ return { type: "http", complete: true };
1168
+ }
1169
+
1170
+ // Bearer token
1171
+ const token = opts?.credentials?.["token"] ?? opts?.apiKey ?? await tryResolve("token");
1172
+ if (!token) {
1173
+ return {
1174
+ type: "http",
1175
+ complete: false,
1176
+ fields: [{ name: "token", label: "Bearer Token", secret: true }],
1177
+ };
1178
+ }
1057
1179
  await storeRefSecret(name, "token", token);
1058
1180
  return { type: "http", complete: true };
1059
1181
  }
@@ -1062,7 +1184,14 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1062
1184
  const flows = (security as { flows?: { authorizationCode?: { authorizationUrl?: string; tokenUrl?: string } } }).flows;
1063
1185
  const authCodeFlow = flows?.authorizationCode;
1064
1186
  if (!authCodeFlow?.authorizationUrl) {
1065
- return { type: "oauth2", complete: false };
1187
+ return {
1188
+ type: "oauth2",
1189
+ complete: false,
1190
+ fields: [
1191
+ { name: "client_id", label: "Client ID", secret: false },
1192
+ { name: "client_secret", label: "Client Secret", secret: true },
1193
+ ],
1194
+ };
1066
1195
  }
1067
1196
 
1068
1197
  const authUrl = authCodeFlow.authorizationUrl;
@@ -1116,9 +1245,14 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
1116
1245
  }
1117
1246
 
1118
1247
  if (!clientId) {
1119
- throw new Error(
1120
- "Could not obtain client_id. Provide via resolveCredentials callback or store manually.",
1121
- );
1248
+ // Return fields telling the caller what OAuth credentials to provide
1249
+ const missingFields: AuthChallengeField[] = [];
1250
+ if (!clientId) {
1251
+ missingFields.push({ name: "client_id", label: "Client ID", secret: false });
1252
+ }
1253
+ // Always ask for client_secret alongside client_id — most providers need it
1254
+ missingFields.push({ name: "client_secret", label: "Client Secret", secret: true });
1255
+ return { type: "oauth2", complete: false, fields: missingFields };
1122
1256
  }
1123
1257
 
1124
1258
  // State ties the callback back to this ref. Encode as base64 JSON
package/src/index.ts CHANGED
@@ -417,6 +417,7 @@ export type {
417
417
  RefAuthStatus,
418
418
  CredentialField,
419
419
  AuthStartResult,
420
+ AuthChallengeField,
420
421
  OAuthResult,
421
422
  ResolveCredentials,
422
423
  ResolveCredentialsContext,
package/src/types.ts CHANGED
@@ -204,17 +204,35 @@ export interface OAuth2SecurityScheme {
204
204
 
205
205
  /**
206
206
  * API key authentication.
207
- * Used by agents that wrap APIs using a single key
207
+ * Used by agents that wrap APIs using one or more keys
208
208
  * (e.g. OpenAI, Anthropic, Stripe, Datadog).
209
+ *
210
+ * @example
211
+ * // Single key (backwards-compatible)
212
+ * security: { type: 'apiKey', in: 'header', name: 'Authorization', prefix: 'Bearer' }
213
+ *
214
+ * // Multiple keys (e.g. Datadog)
215
+ * security: {
216
+ * type: 'apiKey',
217
+ * headers: {
218
+ * 'DD-API-KEY': { description: 'Your Datadog API key' },
219
+ * 'DD-APPLICATION-KEY': { description: 'Your Datadog application key' },
220
+ * }
221
+ * }
209
222
  */
210
223
  export interface ApiKeySecurityScheme {
211
224
  type: "apiKey";
212
- /** Where the key is sent */
213
- in: "header" | "query";
214
- /** Header or query parameter name (e.g. "X-API-Key", "Authorization") */
215
- name: string;
216
- /** Optional prefix (e.g. "Bearer" for Authorization header) */
225
+ /** Where the key is sent (single-key mode) */
226
+ in?: "header" | "query";
227
+ /** Header or query parameter name (single-key mode, e.g. "X-API-Key") */
228
+ name?: string;
229
+ /** Optional prefix (single-key mode, e.g. "Bearer") */
217
230
  prefix?: string;
231
+ /**
232
+ * Named headers the user must provide values for (multi-key mode).
233
+ * When present, this is the source of truth — `in`/`name`/`prefix` are ignored.
234
+ */
235
+ headers?: Record<string, { description?: string }>;
218
236
  }
219
237
 
220
238
  /**